diff --git a/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/GettingStartedOnPrem.java b/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/GettingStartedOnPrem.java index 57c898e34..6cb6e8433 100644 --- a/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/GettingStartedOnPrem.java +++ b/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/GettingStartedOnPrem.java @@ -64,8 +64,8 @@ public class GettingStartedOnPrem { cxx/data, and is downloaded if it is missing, or you may specify another file as a command line parameter. - Note that the Lite data file is only used for illustration, and has limited accuracy and - capabilities. The example shows the properties only the Enterprise data file has as needing + Note that the Lite data file is only used for illustration, and has a small number of + Properties. The example shows the properties only the Enterprise data file has as needing it. Contact us for a copy of the Enterprise data file: https://51degrees.com/contact-us?utm_source=code&utm_medium=example&utm_campaign=51degrees-java&utm_content=console-src-main-java-fiftyone-devicedetection-examples-console-gettingstartedonprem.java&utm_term=lite_v_4_1_hash */ public static String LITE_V_4_1_HASH = "device-detection/51Degrees-LiteV4.1.hash"; public static String ENTERPRISE_HASH = "TAC-HashV41.hash"; diff --git a/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/MetadataOnPrem.java b/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/MetadataOnPrem.java index 8d8a06bcf..c85d852b8 100644 --- a/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/MetadataOnPrem.java +++ b/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/MetadataOnPrem.java @@ -51,8 +51,8 @@ public class MetadataOnPrem { cxx/data, and is downloaded if it is missing, or you may specify another file as a command line parameter. - Note that the Lite data file is only used for illustration, and has limited accuracy and - capabilities. Contact us for a copy of the Enterprise data file: https://51degrees.com/contact-us?utm_source=code&utm_medium=example&utm_campaign=51degrees-java&utm_content=console-src-main-java-fiftyone-devicedetection-examples-console-metadataonprem.java&utm_term=lite_v_4_1_hash */ + Note that the Lite data file is only used for illustration, and has a small number of + Properties. Contact us for a copy of the Enterprise data file: https://51degrees.com/contact-us?utm_source=code&utm_medium=example&utm_campaign=51degrees-java&utm_content=console-src-main-java-fiftyone-devicedetection-examples-console-metadataonprem.java&utm_term=lite_v_4_1_hash */ public static String LITE_V_4_1_HASH = "device-detection/51Degrees-LiteV4.1.hash"; /** diff --git a/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/OfflineProcessing.java b/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/OfflineProcessing.java index cba0c7e89..fbcdee51b 100644 --- a/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/OfflineProcessing.java +++ b/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/OfflineProcessing.java @@ -72,7 +72,7 @@ public class OfflineProcessing { // cxx/data, and is downloaded if it is missing // // Note that the Lite data file is only used for illustration, and has - // limited accuracy and capabilities. Contact us for a copy of the + // a small number of Properties. Contact us for a copy of the // Enterprise data file: https://51degrees.com/contact-us?utm_source=code&utm_medium=example&utm_campaign=51degrees-java&utm_content=console-src-main-java-fiftyone-devicedetection-examples-console-offlineprocessing.java&utm_term=lite_v_4_1_hash public static final String LITE_V_4_1_HASH = "device-detection/51Degrees-LiteV4.1.hash"; diff --git a/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/UpdateDataFile.java b/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/UpdateDataFile.java index 9748b15f5..ebffdef86 100644 --- a/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/UpdateDataFile.java +++ b/examples/device-detection/console/src/main/java/fiftyone/devicedetection/examples/console/UpdateDataFile.java @@ -75,7 +75,7 @@ * https://distributor.51degrees.com/api/v2/download?LicenseKeys=&Type=27&Download=True&Product=22 * ``` * ## Lite Data File - * Lite data files (free-to-use, limited capabilities, no license key required) are created roughly + * Lite data files (free-to-use, a small number of Properties, no license key required) are created roughly * once a month and cannot be updated using auto-update, they may be downloaded with the * scripts in the [data repository](https://github.com/51Degrees/data) on GitHub, which is * the cxx/data folder of the source. diff --git a/examples/device-detection/web/getting-started.cloud/src/main/webapp/WEB-INF/51Degrees-Cloud.xml b/examples/device-detection/web/getting-started.cloud/src/main/webapp/WEB-INF/51Degrees-Cloud.xml index dacfeb006..ced491606 100644 --- a/examples/device-detection/web/getting-started.cloud/src/main/webapp/WEB-INF/51Degrees-Cloud.xml +++ b/examples/device-detection/web/getting-started.cloud/src/main/webapp/WEB-INF/51Degrees-Cloud.xml @@ -42,7 +42,7 @@ JsonBuilderElement diff --git a/examples/device-detection/web/getting-started.onprem/src/main/webapp/WEB-INF/51Degrees-OnPrem.xml b/examples/device-detection/web/getting-started.onprem/src/main/webapp/WEB-INF/51Degrees-OnPrem.xml index bba6dd60e..b74ec24f8 100644 --- a/examples/device-detection/web/getting-started.onprem/src/main/webapp/WEB-INF/51Degrees-OnPrem.xml +++ b/examples/device-detection/web/getting-started.onprem/src/main/webapp/WEB-INF/51Degrees-OnPrem.xml @@ -39,7 +39,7 @@ JsonBuilderElement diff --git a/examples/ip-intelligence/web/getting-started.mixed/src/main/webapp/WEB-INF/51Degrees-Mixed.xml b/examples/ip-intelligence/web/getting-started.mixed/src/main/webapp/WEB-INF/51Degrees-Mixed.xml index 127b4665d..83f577d3e 100644 --- a/examples/ip-intelligence/web/getting-started.mixed/src/main/webapp/WEB-INF/51Degrees-Mixed.xml +++ b/examples/ip-intelligence/web/getting-started.mixed/src/main/webapp/WEB-INF/51Degrees-Mixed.xml @@ -42,7 +42,7 @@ IPIntelligenceOnPremiseEngine - + JsonBuilderElement diff --git a/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/data/EvidenceKeyFilterShareUsage.java b/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/data/EvidenceKeyFilterShareUsage.java index 07a21856e..db1091fbe 100644 --- a/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/data/EvidenceKeyFilterShareUsage.java +++ b/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/data/EvidenceKeyFilterShareUsage.java @@ -136,6 +136,11 @@ public EvidenceKeyFilterShareUsage( @Override public boolean include(String key) { + // The Usage Sharing Element reads the whole web request itself, and + // the request is never a value to share. + if (ShareUsageEvidence.HTTP_REQUEST_KEY.equalsIgnoreCase(key)) { + return false; + } String lowerKey = key.toLowerCase(); for (String neverShared : NEVER_SHARED_EVIDENCE_KEYS) { if (lowerKey.equals(neverShared) || diff --git a/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/data/SequenceData.java b/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/data/SequenceData.java new file mode 100644 index 000000000..e6f18024d --- /dev/null +++ b/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/data/SequenceData.java @@ -0,0 +1,86 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.pipeline.engines.fiftyone.data; + +import fiftyone.pipeline.core.data.ElementDataBase; +import fiftyone.pipeline.core.data.FlowData; +import fiftyone.pipeline.engines.fiftyone.flowelements.SequenceElement; +import org.slf4j.Logger; + +/** + * Data containing the result of {@link SequenceElement}. + * @see Specification + */ +public class SequenceData extends ElementDataBase { + + /** + * Name of the property holding the session id. + */ + public static final String SESSION_ID_PROPERTY_NAME = "session-id"; + + /** + * Name of the property holding the sequence. + */ + public static final String SEQUENCE_PROPERTY_NAME = "sequence"; + + /** + * Construct a new instance. + * @param logger the logger to use + * @param flowData the flow data the data belongs to + */ + public SequenceData(Logger logger, FlowData flowData) { + super(logger, flowData); + } + + /** + * Get the identifier of the session. + * @return the session id + */ + public String getSessionId() { + return getAs(SESSION_ID_PROPERTY_NAME, String.class); + } + + /** + * Set the identifier of the session. + * @param sessionId the session id + */ + public void setSessionId(String sessionId) { + put(SESSION_ID_PROPERTY_NAME, sessionId); + } + + /** + * Get the number of consecutive callbacks made in the session. + * @return the sequence + */ + public Integer getSequence() { + return getAs(SEQUENCE_PROPERTY_NAME, Integer.class); + } + + /** + * Set the number of consecutive callbacks made in the session. + * @param sequence the sequence + */ + public void setSequence(int sequence) { + put(SEQUENCE_PROPERTY_NAME, sequence); + } +} diff --git a/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/flowelements/SequenceElement.java b/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/flowelements/SequenceElement.java index 5542e99f9..049798a9c 100644 --- a/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/flowelements/SequenceElement.java +++ b/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/flowelements/SequenceElement.java @@ -23,13 +23,17 @@ package fiftyone.pipeline.engines.fiftyone.flowelements; import fiftyone.pipeline.core.data.*; +import fiftyone.pipeline.core.data.factories.ElementDataFactory; import fiftyone.pipeline.core.flowelements.FlowElement; import fiftyone.pipeline.core.flowelements.FlowElementBase; +import fiftyone.pipeline.engines.fiftyone.data.SequenceData; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.regex.Pattern; import org.slf4j.Logger; /** @@ -39,6 +43,10 @@ public class SequenceElement extends FlowElementBase { + private static final Pattern DIGITS = Pattern.compile("[0-9]+"); + + private final List properties; + /** * The session id and sequence of a callback, which the specification * lists as this element's accepted Evidence. Without them the web @@ -55,48 +63,92 @@ public class SequenceElement * Construct a new instance of the {@link FlowElement}. * @param logger logger instance to use for logging */ - public SequenceElement(Logger logger) { - super(logger, null); + @SuppressWarnings("this-escape") + public SequenceElement(final Logger logger) { + super(logger, new ElementDataFactory() { + @Override + public ElementData create( + FlowData flowData, + FlowElement flowElement) { + return new SequenceData(logger, flowData); + } + }); + properties = new ArrayList<>(); + properties.add(new ElementPropertyMetaDataDefault( + SequenceData.SESSION_ID_PROPERTY_NAME, + this, + "", + String.class, + true)); + properties.add(new ElementPropertyMetaDataDefault( + SequenceData.SEQUENCE_PROPERTY_NAME, + this, + "", + Integer.class, + true)); } @Override protected void processInternal(FlowData data) throws Exception { Map evidence = data.getEvidence().asKeyMap(); - - // If the evidence does not contain a session id then create a new one. - if (evidence.containsKey(Constants.EVIDENCE_SESSIONID) == false) { - data.addEvidence(Constants.EVIDENCE_SESSIONID, getNewSessionId()); - } - // If the evidence does not have a sequence then add one. - if (evidence.containsKey(Constants.EVIDENCE_SEQUENCE) == false) { - data.addEvidence(Constants.EVIDENCE_SEQUENCE, 1); - } else { - data.addEvidence( - Constants.EVIDENCE_SEQUENCE, - getSequence(evidence.get(Constants.EVIDENCE_SEQUENCE)) + 1); - } + // The session id and the sequence are each used only when they are + // valid, and otherwise count as absent. + Object givenSession = evidence.get(Constants.EVIDENCE_SESSIONID); + String sessionId = + givenSession instanceof String && + ((String) givenSession).isEmpty() == false ? + (String) givenSession : getNewSessionId(); + Integer previous = toSequence(evidence.get(Constants.EVIDENCE_SEQUENCE)); + int sequence = previous == null ? 1 : previous + 1; + + data.addEvidence(Constants.EVIDENCE_SESSIONID, sessionId); + data.addEvidence(Constants.EVIDENCE_SEQUENCE, sequence); + + SequenceData sequenceData = (SequenceData) data.getOrAdd( + getElementDataKey(), + getDataFactory()); + sequenceData.setSessionId(sessionId); + sequenceData.setSequence(sequence); } /** - * The sequence in Evidence, which is a number when an element set it and - * text when it came from the query string of a callback. Anything that is - * not a number counts as no sequence. + * The sequence in Evidence, which is a whole number or text made only of + * decimal digits. A value that is anything else, does not fit in an int + * or has no larger int after it counts as no sequence, so that adding + * one never overflows. + * @return the sequence, or null if there is none */ - private static int getSequence(Object sequence) { - if (sequence instanceof Number) { - return ((Number) sequence).intValue(); - } - try { - return Integer.parseInt(String.valueOf(sequence).trim()); - } catch (NumberFormatException e) { - return 0; + private static Integer toSequence(Object sequence) { + long value; + if (sequence instanceof Integer || sequence instanceof Long || + sequence instanceof Short || sequence instanceof Byte) { + value = ((Number) sequence).longValue(); + } else if (sequence instanceof BigInteger) { + BigInteger big = (BigInteger) sequence; + if (big.bitLength() > 31) { + return null; + } + value = big.longValue(); + } else if (sequence instanceof String && + DIGITS.matcher((String) sequence).matches()) { + // More digits than any int has cannot fit, however many zeros + // come first, so only the value of a short string is read. + String digits = ((String) sequence).replaceFirst("^0+(?=.)", ""); + if (digits.length() > 10) { + return null; + } + value = Long.parseLong(digits); + } else { + return null; } + return value < Integer.MAX_VALUE && value >= Integer.MIN_VALUE ? + Integer.valueOf((int) value) : null; } @Override public String getElementDataKey() { - return "sequence-element"; + return "sequence"; } @Override @@ -106,7 +158,7 @@ public EvidenceKeyFilter getEvidenceKeyFilter() { @Override public List getProperties() { - return new ArrayList<>(); + return properties; } @Override diff --git a/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/data/EvidenceKeyFilterShareUsageTests.java b/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/data/EvidenceKeyFilterShareUsageTests.java index c0c97c288..33eacefc6 100644 --- a/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/data/EvidenceKeyFilterShareUsageTests.java +++ b/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/data/EvidenceKeyFilterShareUsageTests.java @@ -61,6 +61,18 @@ private static EvidenceKeyFilterShareUsage defaults() { * values and query string parameters only when their names start with * 51D_, and any other evidence. */ + /** + * The whole web request is read by the Usage Sharing Element and never + * shared as a value, so the filter does not include it. + */ + @Test + public void webRequestIsNeverIncluded() { + EvidenceKeyFilterShareUsage filter = defaults(); + + assertFalse(filter.include("server.http-request")); + assertFalse(filter.include("Server.Http-Request")); + } + @Test public void defaultRules() { EvidenceKeyFilterShareUsage filter = defaults(); diff --git a/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/flowelements/SequenceElementTests.java b/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/flowelements/SequenceElementTests.java new file mode 100644 index 000000000..1240e11c0 --- /dev/null +++ b/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/flowelements/SequenceElementTests.java @@ -0,0 +1,226 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.pipeline.engines.fiftyone.flowelements; + +import fiftyone.pipeline.core.data.ElementData; +import fiftyone.pipeline.core.data.ElementPropertyMetaData; +import fiftyone.pipeline.core.data.FlowData; +import fiftyone.pipeline.core.flowelements.Pipeline; +import fiftyone.pipeline.core.flowelements.PipelineBuilder; +import org.junit.jupiter.api.Test; +import org.slf4j.ILoggerFactory; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The session id and sequence are written to Element Data as well as + * Evidence. + * @see Specification + */ +public class SequenceElementTests { + + private static final String SESSION_ID_EVIDENCE = "query.session-id"; + private static final String SEQUENCE_EVIDENCE = "query.sequence"; + + private Pipeline newPipeline() throws Exception { + ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); + return new PipelineBuilder(loggerFactory) + .addFlowElement(new SequenceElementBuilder(loggerFactory).build()) + .build(); + } + + private ElementData sequenceData(FlowData flowData) throws Exception { + ElementData data = flowData.get("sequence"); + assertNotNull(data, "Expected Element Data under 'sequence'"); + return data; + } + + @Test + public void NoEvidence_NewSessionAndSequenceOne() throws Exception { + try (Pipeline pipeline = newPipeline(); + FlowData flowData = pipeline.createFlowData()) { + flowData.process(); + + ElementData data = sequenceData(flowData); + assertNotNull(data.get("session-id")); + assertEquals(1, data.get("sequence")); + assertEquals( + data.get("session-id"), + flowData.getEvidence().asKeyMap().get(SESSION_ID_EVIDENCE)); + assertEquals( + 1, + flowData.getEvidence().asKeyMap().get(SEQUENCE_EVIDENCE)); + } + } + + @Test + public void EvidenceGiven_SessionKeptAndSequenceIncreased() + throws Exception { + try (Pipeline pipeline = newPipeline(); + FlowData flowData = pipeline.createFlowData()) { + flowData.addEvidence(SESSION_ID_EVIDENCE, "abc"); + flowData.addEvidence(SEQUENCE_EVIDENCE, 2); + flowData.process(); + + ElementData data = sequenceData(flowData); + assertEquals("abc", data.get("session-id")); + assertEquals(3, data.get("sequence")); + assertEquals( + 3, + flowData.getEvidence().asKeyMap().get(SEQUENCE_EVIDENCE)); + } + } + + @Test + public void Properties_AreSessionIdAndSequence() throws Exception { + try (Pipeline pipeline = newPipeline()) { + SequenceElement element = + (SequenceElement) pipeline.getFlowElements().get(0); + List names = new ArrayList<>(); + for (ElementPropertyMetaData property : element.getProperties()) { + names.add(property.getName()); + } + assertTrue(names.contains("session-id")); + assertTrue(names.contains("sequence")); + assertEquals(2, names.size()); + } + } + + /** + * Process with the given Evidence values, where null means none, and + * check that the Evidence and the Element Data hold the same values. + * @return the session id and the sequence + */ + private Object[] run(Object sessionId, Object sequence) throws Exception { + try (Pipeline pipeline = newPipeline(); + FlowData flowData = pipeline.createFlowData()) { + if (sessionId != null) { + flowData.addEvidence(SESSION_ID_EVIDENCE, sessionId); + } + if (sequence != null) { + flowData.addEvidence(SEQUENCE_EVIDENCE, sequence); + } + flowData.process(); + + ElementData data = sequenceData(flowData); + Object evidenceSession = + flowData.getEvidence().asKeyMap().get(SESSION_ID_EVIDENCE); + Object evidenceSequence = + flowData.getEvidence().asKeyMap().get(SEQUENCE_EVIDENCE); + assertEquals(evidenceSession, data.get("session-id")); + assertEquals(evidenceSequence, data.get("sequence")); + return new Object[] {evidenceSession, evidenceSequence}; + } + } + + private void assertNewSession(Object sessionId, Object sequence, + int expectedSequence) throws Exception { + Object[] result = run(sessionId, sequence); + assertTrue(result[0] instanceof String); + assertFalse(((String) result[0]).isEmpty()); + if (sessionId != null) { + assertNotEquals(sessionId, result[0]); + } + assertEquals(expectedSequence, result[1]); + } + + private void assertSequence(Object sequence, int expected) + throws Exception { + Object[] result = run("abc", sequence); + assertEquals("abc", result[0]); + assertEquals(expected, result[1]); + } + + @Test + public void SequenceAsText_Digits_CountsUp() throws Exception { + assertSequence("2", 3); + assertSequence("0", 1); + assertSequence("00012", 13); + } + + @Test + public void SequenceAsOtherWholeNumbers_CountsUp() throws Exception { + assertSequence(2L, 3); + assertSequence((short) 2, 3); + assertSequence(new java.math.BigInteger("2"), 3); + } + + @Test + public void SequenceNotAWholeNumber_CountsAsAbsent() throws Exception { + assertSequence("abc", 1); + assertSequence("", 1); + assertSequence(" 2", 1); + assertSequence("2 ", 1); + assertSequence("-2", 1); + assertSequence("+2", 1); + assertSequence("2.5", 1); + assertSequence(2.5, 1); + assertSequence(new Object(), 1); + } + + @Test + public void SequenceOutOfRange_CountsAsAbsent() throws Exception { + assertSequence("99999999999999999999", 1); + assertSequence("2147483648", 1); + assertSequence(2147483648L, 1); + // One more would not fit in an int either. + assertSequence("2147483647", 1); + assertSequence(2147483647, 1); + assertSequence("2147483646", 2147483647); + } + + @Test + public void SessionIdNotANonEmptyString_CountsAsAbsent() throws Exception { + assertNewSession("", 2, 3); + assertNewSession(5, 2, 3); + assertNewSession(new Object(), null, 1); + } + + @Test + public void SessionIdZero_Kept() throws Exception { + Object[] result = run("0", 4); + assertEquals("0", result[0]); + assertEquals(5, result[1]); + } + + @Test + public void SequenceWithoutSessionId_CountsUpWithNewSession() + throws Exception { + assertNewSession(null, 4, 5); + } + + @Test + public void SessionIdWithoutSequence_GivesOne() throws Exception { + Object[] result = run("abc", null); + assertEquals("abc", result[0]); + assertEquals(1, result[1]); + } +} diff --git a/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/SessionIdAndSequenceTests.java b/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/SessionIdAndSequenceTests.java index b7bbe2e9b..8c74887c4 100644 --- a/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/SessionIdAndSequenceTests.java +++ b/pipeline/pipeline.javascriptbuilder/src/test/java/fiftyone/pipeline/javascriptbuilder/SessionIdAndSequenceTests.java @@ -154,15 +154,22 @@ public void SessionId_Invalid_RenderedEmpty(String id) throws Exception { } /** - * The Sequence Element keeps a session id that is already in the - * evidence, so an invalid one reaches the builder that way too. + * The Sequence Element keeps a session id that is a non-empty string in + * the evidence, so an invalid one reaches the builder that way too. */ @ParameterizedTest @MethodSource("invalidSessionIds") public void SessionId_InvalidWithSequenceElement_RenderedEmpty(String id) throws Exception { String script = render(true, id, 1); - assertEquals("", sessionId(script)); + if (id.isEmpty()) { + // The Sequence Element counts an empty session id as absent + // and creates a new one. + assertTrue(sessionId(script).matches("[A-Za-z0-9-]{1,64}"), + sessionId(script)); + } else { + assertEquals("", sessionId(script)); + } assertEquals("2", sequence(script)); } diff --git a/pipeline/pipeline.jsonbuilder/src/test/java/fiftyone/pipeline/jsonbuilder/JsonBuilderTests.java b/pipeline/pipeline.jsonbuilder/src/test/java/fiftyone/pipeline/jsonbuilder/JsonBuilderTests.java index 965526329..e9cbb452a 100644 --- a/pipeline/pipeline.jsonbuilder/src/test/java/fiftyone/pipeline/jsonbuilder/JsonBuilderTests.java +++ b/pipeline/pipeline.jsonbuilder/src/test/java/fiftyone/pipeline/jsonbuilder/JsonBuilderTests.java @@ -482,6 +482,39 @@ public void JsonBuilder_LazyLoading() throws Exception { } } + /** + * The Sequence Element's Element Data is in the JSON like any other + * element's, because the JSON Builder Element does not exclude it. + * @throws Exception + */ + @Test + public void JsonBuilder_SequenceElementData_InJson() throws Exception { + EmptyEngine engine = new EmptyEngineBuilder(loggerFactory) + .setLazyLoading(new LazyLoadingConfiguration(1000)) + .setProcessCost(500) + .build(); + JsonBuilderElement jsonBuilder = new JsonBuilderElementBuilder(loggerFactory) + .build(); + SequenceElement sequenceElement = new SequenceElementBuilder(loggerFactory) + .build(); + try (Pipeline pipeline = new PipelineBuilder(loggerFactory) + .addFlowElement(sequenceElement) + .addFlowElement(engine) + .addFlowElement(jsonBuilder) + .setAutoCloseElements(true) + .build(); + FlowData flowData = pipeline.createFlowData()) { + + flowData.process(); + + JsonBuilderData jsonResult = flowData.get(JsonBuilderData.class); + JSONObject jsonData = new JSONObject(jsonResult.getJson()); + JSONObject sequence = jsonData.getJSONObject("sequence"); + assertEquals(1, sequence.getLong("sequence")); + assertFalse(sequence.getString("session-id").isEmpty()); + } + } + private String testIteration(int iteration) throws Exception { return testIteration(iteration, null); } diff --git a/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/components/FiftyOneInterceptor.java b/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/components/FiftyOneInterceptor.java index 309b7669b..61022376d 100644 --- a/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/components/FiftyOneInterceptor.java +++ b/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/components/FiftyOneInterceptor.java @@ -51,6 +51,7 @@ import static fiftyone.pipeline.web.Constants.CORE_JS_NAME; import static fiftyone.pipeline.web.Constants.CORE_JSON_NAME; +import static fiftyone.pipeline.web.Constants.DEFAULT_JSON_PATH; /** * The 51Degrees middleware component. @@ -93,6 +94,10 @@ public FiftyOneInterceptor( configFileName = Constants.DEFAULT_CONFIG_FILE; } + String jsonPath = config.getJsonPath() == null || + config.getJsonPath().isEmpty() ? + DEFAULT_JSON_PATH : config.getJsonPath(); + File configFile = new File(configFileName); PipelineBuilder builder = new PipelineBuilder() .addService(new DataUpdateServiceDefault( @@ -104,9 +109,13 @@ public FiftyOneInterceptor( // Bind the configuration to a pipeline options instance PipelineOptions options = (PipelineOptions) unmarshaller.unmarshal(configFile); pipeline = StartupHelpers.buildFromConfiguration( - builder, - options, - config.getClientsidePropertiesEnabled()); + builder, + options, + config.getClientsidePropertiesEnabled(), + null, + jsonPath, + config.getSequenceElementEnabled(), + config.getSetHeadersElementEnabled()); } catch (Exception e) { throw new RuntimeException(e); } @@ -114,13 +123,40 @@ public FiftyOneInterceptor( resultService.setPipeline(pipeline); clientsidePropertyService.setPipeline(pipeline); fiftyOneJsService.enable(config.getClientsidePropertiesEnabled()); + fiftyOneJsService.setPaths(config.getJavaScriptPath(), jsonPath); } public static void enableClientsideProperties(ViewControllerRegistry viewControllerRegistry) { viewControllerRegistry.addViewController("/" + CORE_JS_NAME); + viewControllerRegistry.addViewController(DEFAULT_JSON_PATH); + viewControllerRegistry.addViewController("/" + CORE_JSON_NAME); + } + + /** + * Map the JavaScript and JSON paths that are not the defaults, which + * the application sets in its {@link FiftyOneInterceptorConfig}. + * @param viewControllerRegistry the registry to add the paths to + * @param javaScriptPath the JavaScript path, or null or empty for the + * default + * @param jsonPath the JSON path, or null or empty for the default + */ + public static void enableClientsideProperties( + ViewControllerRegistry viewControllerRegistry, + String javaScriptPath, + String jsonPath) { + viewControllerRegistry.addViewController("/" + withoutSlash( + javaScriptPath == null || javaScriptPath.isEmpty() ? + CORE_JS_NAME : javaScriptPath)); + viewControllerRegistry.addViewController("/" + withoutSlash( + jsonPath == null || jsonPath.isEmpty() ? + DEFAULT_JSON_PATH : jsonPath)); viewControllerRegistry.addViewController("/" + CORE_JSON_NAME); } + private static String withoutSlash(String path) { + return path.startsWith("/") ? path.substring(1) : path; + } + @Override public boolean preHandle( HttpServletRequest request, diff --git a/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/configuration/FiftyOneInterceptorConfig.java b/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/configuration/FiftyOneInterceptorConfig.java index c2d6ddfbc..5dcdf3cd2 100644 --- a/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/configuration/FiftyOneInterceptorConfig.java +++ b/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/configuration/FiftyOneInterceptorConfig.java @@ -40,4 +40,38 @@ public interface FiftyOneInterceptorConfig { * @return true if client-side properties are enabled */ boolean getClientsidePropertiesEnabled(); + + /** + * Get the end of the request path the JavaScript is served for. + * @return the path, or null or empty for the default + */ + default String getJavaScriptPath() { + return null; + } + + /** + * Get the end of the request path the JSON is served for. + * @return the path, or null or empty for the default + */ + default String getJsonPath() { + return null; + } + + /** + * Get whether the Sequence Element is added to the Pipeline when the + * configuration does not include it. + * @return true to add the Sequence Element + */ + default boolean getSequenceElementEnabled() { + return true; + } + + /** + * Get whether the Set Headers Element is added to the Pipeline when the + * configuration does not include it. + * @return true to add the Set Headers Element + */ + default boolean getSetHeadersElementEnabled() { + return true; + } } diff --git a/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/configuration/FiftyOneInterceptorConfigDefault.java b/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/configuration/FiftyOneInterceptorConfigDefault.java index 4b3172ff2..bd22601cf 100644 --- a/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/configuration/FiftyOneInterceptorConfigDefault.java +++ b/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/configuration/FiftyOneInterceptorConfigDefault.java @@ -34,6 +34,14 @@ public class FiftyOneInterceptorConfigDefault implements FiftyOneInterceptorConf private boolean clientsidePropertiesEnabled; + private String javaScriptPath; + + private String jsonPath; + + private boolean sequenceElementEnabled = true; + + private boolean setHeadersElementEnabled = true; + @Autowired public ServletContext servletContext; @@ -45,6 +53,42 @@ public void setClientsidePropertiesEnabled(boolean clientsidePropertiesEnabled) this.clientsidePropertiesEnabled = clientsidePropertiesEnabled; } + public void setJavaScriptPath(String javaScriptPath) { + this.javaScriptPath = javaScriptPath; + } + + public void setJsonPath(String jsonPath) { + this.jsonPath = jsonPath; + } + + public void setSequenceElementEnabled(boolean sequenceElementEnabled) { + this.sequenceElementEnabled = sequenceElementEnabled; + } + + public void setSetHeadersElementEnabled(boolean setHeadersElementEnabled) { + this.setHeadersElementEnabled = setHeadersElementEnabled; + } + + @Override + public String getJavaScriptPath() { + return javaScriptPath; + } + + @Override + public String getJsonPath() { + return jsonPath; + } + + @Override + public boolean getSequenceElementEnabled() { + return sequenceElementEnabled; + } + + @Override + public boolean getSetHeadersElementEnabled() { + return setHeadersElementEnabled; + } + @Override public String getDataFilePath() { return dataFilePath; diff --git a/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/services/FiftyOneJSService.java b/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/services/FiftyOneJSService.java index 57c8ff3d8..974b1be61 100644 --- a/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/services/FiftyOneJSService.java +++ b/pipeline/pipeline.web.packages/pipeline.web.mvc/src/main/java/fiftyone/pipeline/web/mvc/services/FiftyOneJSService.java @@ -34,6 +34,16 @@ public interface FiftyOneJSService extends FiftyOneJSServiceCore { void enable(boolean enabled); + /** + * Set the paths the JavaScript and the JSON are served from. + * @param javaScriptPath the end of the request path that the JavaScript + * is served for, or null or empty for the default + * @param jsonPath the end of the request path that the JSON is served + * for, or null or empty for the default + */ + default void setPaths(String javaScriptPath, String jsonPath) { + } + @Service class Default extends FiftyOneJSServiceCore.Default implements FiftyOneJSService { diff --git a/pipeline/pipeline.web.packages/pipeline.web.mvc/src/test/java/fiftyone/pipeline/web/mvc/FiftyOneInterceptorTests.java b/pipeline/pipeline.web.packages/pipeline.web.mvc/src/test/java/fiftyone/pipeline/web/mvc/FiftyOneInterceptorTests.java index 077a88b46..3f000fa3b 100644 --- a/pipeline/pipeline.web.packages/pipeline.web.mvc/src/test/java/fiftyone/pipeline/web/mvc/FiftyOneInterceptorTests.java +++ b/pipeline/pipeline.web.packages/pipeline.web.mvc/src/test/java/fiftyone/pipeline/web/mvc/FiftyOneInterceptorTests.java @@ -1,141 +1,283 @@ -/* ********************************************************************* - * This Original Work is copyright of 51 Degrees Mobile Experts Limited. - * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, - * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. - * - * This Original Work is licensed under the European Union Public Licence - * (EUPL) v.1.2 and is subject to its terms as set out below. - * - * If a copy of the EUPL was not distributed with this file, You can obtain - * one at https://opensource.org/licenses/EUPL-1.2. - * - * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be - * amended by the European Commission) shall be deemed incompatible for - * the purposes of the Work and the provisions of the compatibility - * clause in Article 5 of the EUPL shall not apply. - * - * If using the Work as, or as part of, a network application, by - * including the attribution notice(s) required under Article 5 of the EUPL - * in the end user terms of the application under an appropriate heading, - * such notice(s) shall fulfill the requirements of that article. - * ********************************************************************* */ - -package fiftyone.pipeline.web.mvc; - -import static org.junit.Assert.assertFalse; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -import java.io.IOException; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import fiftyone.pipeline.core.data.FlowData; -import fiftyone.pipeline.web.mvc.components.FiftyOneInterceptor; -import fiftyone.pipeline.web.mvc.components.FlowDataProvider; -import fiftyone.pipeline.web.mvc.configuration.FiftyOneInterceptorConfig; -import fiftyone.pipeline.web.mvc.services.ClientsidePropertyService; -import fiftyone.pipeline.web.mvc.services.FiftyOneJSService; -import fiftyone.pipeline.web.mvc.services.PipelineResultService; -import fiftyone.pipeline.web.mvc.services.UACHService; - -public class FiftyOneInterceptorTests { - private HttpServletRequest request; - private HttpServletResponse response; - private Object handler; - - private FiftyOneInterceptor interceptor; - private PipelineResultService resultService; - private FlowData flowData; - - @Before - public void init() throws IOException { - FiftyOneInterceptorConfig config = - mock(FiftyOneInterceptorConfig.class); - when(config.getDataFilePath()) - .thenReturn("src/test/resources/Test.xml"); - - FlowDataProvider flowDataProvider = mock(FlowDataProvider.class); - ClientsidePropertyService clientsidePropertyService = - mock(ClientsidePropertyService.class); - - FiftyOneJSService fiftyOneJsService = mock(FiftyOneJSService.class); - when(fiftyOneJsService.serveJS( - any(HttpServletRequest.class), - any(HttpServletResponse.class))) - .thenReturn(true); - when(fiftyOneJsService.serveJson( - any(HttpServletRequest.class), - any(HttpServletResponse.class))) - .thenReturn(true); - - UACHService uachService = mock(UACHService.class); - - resultService = mock(PipelineResultService.class); - flowData = mock(FlowData.class); - - // Mock request, response and handler object - request = mock(HttpServletRequest.class); - response = mock(HttpServletResponse.class); - handler = mock(Object.class); - - when(flowDataProvider.getFlowData(any(HttpServletRequest.class))) - .thenReturn(flowData); - - // Create the interceptor object for testing - interceptor = new FiftyOneInterceptor( - config, - resultService, - flowDataProvider, - clientsidePropertyService, - fiftyOneJsService, - uachService); - } - - /** - * Check that 'resultService.process()' is called during - * interceptor's preHandle process - * @throws Exception - */ - @Test - public void FiftyOneInterceptor_PreHandle() throws Exception { - interceptor.preHandle(request, response, handler); - verify(resultService, times(1)) - .process(any(HttpServletRequest.class)); - } - - /** - * Check that 'close()' is called on the FlowData instance in the - * interceptor's afterCompletion processed - * @throws Exception - */ - @Test - public void FiftyOneInterceptor_AfterCompletion() throws Exception { - interceptor.afterCompletion( - request, - response, - handler, - new Exception()); - verify(flowData, times(1)) - .close(); - } - - /** - * Check that the FlowData is closed when preHandle serves the JavaScript - * itself, as Spring does not call afterCompletion when preHandle returns - * false. - * @throws Exception - */ - @Test - public void FiftyOneInterceptor_PreHandle_ServesJavaScript_ClosesFlowData() - throws Exception { - assertFalse(interceptor.preHandle(request, response, handler)); - verify(flowData, times(1)) - .close(); - } -} +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.pipeline.web.mvc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import fiftyone.pipeline.core.data.FlowData; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import fiftyone.pipeline.core.flowelements.FlowElement; +import fiftyone.pipeline.engines.fiftyone.flowelements.SequenceElement; +import fiftyone.pipeline.engines.fiftyone.flowelements.SetHeadersElement; +import fiftyone.pipeline.javascriptbuilder.flowelements.JavaScriptBuilderElement; +import fiftyone.pipeline.web.mvc.components.FiftyOneInterceptor; +import fiftyone.pipeline.web.mvc.components.FlowDataProvider; +import fiftyone.pipeline.web.mvc.configuration.FiftyOneInterceptorConfig; +import fiftyone.pipeline.web.mvc.services.ClientsidePropertyService; +import fiftyone.pipeline.web.mvc.services.FiftyOneJSService; +import fiftyone.pipeline.web.mvc.services.PipelineResultService; +import fiftyone.pipeline.web.mvc.services.UACHService; + +public class FiftyOneInterceptorTests { + private HttpServletRequest request; + private HttpServletResponse response; + private Object handler; + + private FiftyOneInterceptor interceptor; + private PipelineResultService resultService; + private FlowData flowData; + + @Before + public void init() throws IOException { + FiftyOneInterceptorConfig config = + mock(FiftyOneInterceptorConfig.class); + when(config.getDataFilePath()) + .thenReturn("src/test/resources/Test.xml"); + + FlowDataProvider flowDataProvider = mock(FlowDataProvider.class); + ClientsidePropertyService clientsidePropertyService = + mock(ClientsidePropertyService.class); + + FiftyOneJSService fiftyOneJsService = mock(FiftyOneJSService.class); + when(fiftyOneJsService.serveJS( + any(HttpServletRequest.class), + any(HttpServletResponse.class))) + .thenReturn(true); + when(fiftyOneJsService.serveJson( + any(HttpServletRequest.class), + any(HttpServletResponse.class))) + .thenReturn(true); + + UACHService uachService = mock(UACHService.class); + + resultService = mock(PipelineResultService.class); + flowData = mock(FlowData.class); + + // Mock request, response and handler object + request = mock(HttpServletRequest.class); + response = mock(HttpServletResponse.class); + handler = mock(Object.class); + + when(flowDataProvider.getFlowData(any(HttpServletRequest.class))) + .thenReturn(flowData); + + // Create the interceptor object for testing + interceptor = new FiftyOneInterceptor( + config, + resultService, + flowDataProvider, + clientsidePropertyService, + fiftyOneJsService, + uachService); + } + + /** + * Check that 'resultService.process()' is called during + * interceptor's preHandle process + * @throws Exception + */ + @Test + public void FiftyOneInterceptor_PreHandle() throws Exception { + interceptor.preHandle(request, response, handler); + verify(resultService, times(1)) + .process(any(HttpServletRequest.class)); + } + + /** + * Check that 'close()' is called on the FlowData instance in the + * interceptor's afterCompletion processed + * @throws Exception + */ + @Test + public void FiftyOneInterceptor_AfterCompletion() throws Exception { + interceptor.afterCompletion( + request, + response, + handler, + new Exception()); + verify(flowData, times(1)) + .close(); + } + + /** + * Check that the FlowData is closed when preHandle serves the JavaScript + * itself, as Spring does not call afterCompletion when preHandle returns + * false. + * @throws Exception + */ + @Test + public void FiftyOneInterceptor_PreHandle_ServesJavaScript_ClosesFlowData() + throws Exception { + assertFalse(interceptor.preHandle(request, response, handler)); + verify(flowData, times(1)) + .close(); + } + + private FiftyOneInterceptor newInterceptor( + FiftyOneInterceptorConfig config, + FiftyOneJSService jsService) throws IOException { + return new FiftyOneInterceptor( + config, + mock(PipelineResultService.class), + mock(FlowDataProvider.class), + mock(ClientsidePropertyService.class), + jsService, + mock(UACHService.class)); + } + + private FiftyOneInterceptorConfig newConfig( + boolean sequence, + boolean setHeaders, + String jsonPath, + String javaScriptPath) { + FiftyOneInterceptorConfig config = mock(FiftyOneInterceptorConfig.class); + when(config.getDataFilePath()) + .thenReturn("src/test/resources/NoSequence.xml"); + when(config.getClientsidePropertiesEnabled()).thenReturn(true); + when(config.getSequenceElementEnabled()).thenReturn(sequence); + when(config.getSetHeadersElementEnabled()).thenReturn(setHeaders); + when(config.getJsonPath()).thenReturn(jsonPath); + when(config.getJavaScriptPath()).thenReturn(javaScriptPath); + return config; + } + + private static boolean hasElement( + FiftyOneInterceptor interceptor, + Class type) { + for (FlowElement element : interceptor.pipeline.getFlowElements()) { + if (type.isInstance(element)) { + return true; + } + } + return false; + } + + /** + * Check that both elements are added when the configuration allows it. + */ + @Test + public void FiftyOneInterceptor_ElementsEnabled_Added() throws Exception { + FiftyOneInterceptor created = newInterceptor( + newConfig(true, true, null, null), mock(FiftyOneJSService.class)); + + assertTrue(hasElement(created, SequenceElement.class)); + assertTrue(hasElement(created, SetHeadersElement.class)); + } + + /** + * Check that the switches stop each element being added. + */ + @Test + public void FiftyOneInterceptor_ElementsDisabled_NotAdded() + throws Exception { + FiftyOneInterceptorConfig config = newConfig(false, false, null, null); + when(config.getClientsidePropertiesEnabled()).thenReturn(false); + when(config.getDataFilePath()) + .thenReturn("src/test/resources/NoJson.xml"); + FiftyOneInterceptor created = newInterceptor( + config, mock(FiftyOneJSService.class)); + + assertFalse(hasElement(created, SequenceElement.class)); + assertFalse(hasElement(created, SetHeadersElement.class)); + } + + /** + * Check that the build is refused when the JSON Builder Element would + * have no Sequence Element before it. + */ + @Test + public void FiftyOneInterceptor_NoSequenceElementWithJson_Refused() { + RuntimeException e = assertThrows(RuntimeException.class, + () -> newInterceptor( + newConfig(false, true, null, null), + mock(FiftyOneJSService.class))); + + assertTrue(e.getCause().getMessage().contains("Sequence Element")); + } + + /** + * Check that the JSON path is the JavaScript Builder Element's callback + * endpoint and, with the JavaScript path, goes to the service that serves + * them. + */ + @Test + public void FiftyOneInterceptor_Paths_UsedByBuilderAndService() + throws Exception { + FiftyOneJSService jsService = mock(FiftyOneJSService.class); + FiftyOneInterceptor created = newInterceptor( + newConfig(true, true, "/51dpipeline/json", "/scripts/51d.js"), + jsService); + + verify(jsService).setPaths("/scripts/51d.js", "/51dpipeline/json"); + for (FlowElement element : created.pipeline.getFlowElements()) { + if (element instanceof JavaScriptBuilderElement) { + Field field = + JavaScriptBuilderElement.class.getDeclaredField("endpoint"); + field.setAccessible(true); + assertEquals("/51dpipeline/json", field.get(element)); + } + } + } + + /** + * Check that no paths in the configuration mean the default paths. + */ + @Test + public void FiftyOneInterceptor_NoPaths_DefaultPaths() throws Exception { + FiftyOneJSService jsService = mock(FiftyOneJSService.class); + newInterceptor(newConfig(true, true, null, null), jsService); + + verify(jsService).setPaths(null, "/51dpipeline/json"); + } + + /** + * Check that the paths the application can call are mapped, the old JSON + * path included. + */ + @Test + public void FiftyOneInterceptor_EnableClientsideProperties_MapsBothJsonPaths() { + ViewControllerRegistry registry = mock(ViewControllerRegistry.class); + + FiftyOneInterceptor.enableClientsideProperties(registry); + + verify(registry).addViewController("/51Degrees.core.js"); + verify(registry).addViewController("/51dpipeline/json"); + verify(registry).addViewController("/51Degrees.core.json"); + } +} diff --git a/pipeline/pipeline.web.packages/pipeline.web.mvc/src/test/resources/NoJson.xml b/pipeline/pipeline.web.packages/pipeline.web.mvc/src/test/resources/NoJson.xml new file mode 100644 index 000000000..eece81fbe --- /dev/null +++ b/pipeline/pipeline.web.packages/pipeline.web.mvc/src/test/resources/NoJson.xml @@ -0,0 +1,7 @@ + + + + JavaScriptBuilderElement + + + diff --git a/pipeline/pipeline.web.packages/pipeline.web.mvc/src/test/resources/NoSequence.xml b/pipeline/pipeline.web.packages/pipeline.web.mvc/src/test/resources/NoSequence.xml new file mode 100644 index 000000000..3fd181f26 --- /dev/null +++ b/pipeline/pipeline.web.packages/pipeline.web.mvc/src/test/resources/NoSequence.xml @@ -0,0 +1,10 @@ + + + + JsonBuilderElement + + + JavaScriptBuilderElement + + + diff --git a/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/Constants.java b/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/Constants.java index acd9791db..7298dd10e 100644 --- a/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/Constants.java +++ b/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/Constants.java @@ -30,10 +30,17 @@ public class Constants { public static final String CORE_JS_NAME = "51Degrees.core.js"; /** - * The name of the JSON served to the client. + * The name the JSON was served under before {@link #DEFAULT_JSON_PATH} + * became the default. It is still answered. */ public static final String CORE_JSON_NAME = "51Degrees.core.json"; + /** + * The default path the JSON is served from, which the specification + * gives. + */ + public static final String DEFAULT_JSON_PATH = "/51dpipeline/json"; + /** * Key prefix used for 51Degrees data stored in the HTTP context. */ @@ -74,4 +81,36 @@ public class Constants { */ public static final boolean DEFAULT_CLIENTSIDE_ENABLED = true; + /** + * Default value for the options that add the Sequence Element and the + * Set Headers Element. + */ + public static final boolean DEFAULT_ELEMENT_ENABLED = true; + + /** + * Name of the filter init parameter that sets the path the JavaScript is + * served from. + */ + public static final String INIT_PARAM_JAVASCRIPT_PATH = "javascript-path"; + + /** + * Name of the filter init parameter that sets the path the JSON is + * served from. + */ + public static final String INIT_PARAM_JSON_PATH = "json-path"; + + /** + * Name of the filter init parameter that sets whether the Sequence + * Element is added to the Pipeline when it is missing. + */ + public static final String INIT_PARAM_SEQUENCE_ELEMENT_ENABLED = + "sequence-element-enabled"; + + /** + * Name of the filter init parameter that sets whether the Set Headers + * Element is added to the Pipeline when it is missing. + */ + public static final String INIT_PARAM_SET_HEADERS_ELEMENT_ENABLED = + "set-headers-element-enabled"; + } diff --git a/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/PipelineFilter.java b/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/PipelineFilter.java index 098da0926..b1090a6ef 100644 --- a/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/PipelineFilter.java +++ b/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/PipelineFilter.java @@ -40,8 +40,15 @@ import java.io.File; import java.io.IOException; +import static fiftyone.pipeline.web.Constants.DEFAULT_JSON_PATH; +import static fiftyone.pipeline.web.Constants.CORE_JS_NAME; import static fiftyone.pipeline.web.Constants.DEFAULT_CLIENTSIDE_ENABLED; import static fiftyone.pipeline.web.Constants.DEFAULT_CONFIG_FILE; +import static fiftyone.pipeline.web.Constants.DEFAULT_ELEMENT_ENABLED; +import static fiftyone.pipeline.web.Constants.INIT_PARAM_JAVASCRIPT_PATH; +import static fiftyone.pipeline.web.Constants.INIT_PARAM_JSON_PATH; +import static fiftyone.pipeline.web.Constants.INIT_PARAM_SEQUENCE_ELEMENT_ENABLED; +import static fiftyone.pipeline.web.Constants.INIT_PARAM_SET_HEADERS_ELEMENT_ENABLED; /** * Servlet filter used to intercept HTTP requests and process them using the @@ -81,6 +88,19 @@ public void init(FilterConfig filterConfig) throws ServletException { clientsideEnabled = Boolean.parseBoolean(clientsideEnabledString); } + boolean sequenceElementEnabled = getBooleanInitParameter( + filterConfig, + INIT_PARAM_SEQUENCE_ELEMENT_ENABLED, + DEFAULT_ELEMENT_ENABLED); + boolean setHeadersElementEnabled = getBooleanInitParameter( + filterConfig, + INIT_PARAM_SET_HEADERS_ELEMENT_ENABLED, + DEFAULT_ELEMENT_ENABLED); + String javaScriptPath = getPathInitParameter( + filterConfig, INIT_PARAM_JAVASCRIPT_PATH, CORE_JS_NAME); + String jsonPath = getPathInitParameter( + filterConfig, INIT_PARAM_JSON_PATH, DEFAULT_JSON_PATH); + ServletContext context = config.getServletContext(); File configFile = new File(context.getRealPath(configFileName)); @@ -96,7 +116,8 @@ public void init(FilterConfig filterConfig) throws ServletException { PipelineOptions options = (PipelineOptions) unmarshaller.unmarshal(configFile); pipeline = StartupHelpers.buildFromConfiguration(builder, options, - clientsideEnabled, context.getContextPath()); + clientsideEnabled, context.getContextPath(), jsonPath, + sequenceElementEnabled, setHeadersElementEnabled); } catch (Exception e) { throw new ServletException(e); } @@ -110,7 +131,26 @@ public void init(FilterConfig filterConfig) throws ServletException { pipeline); jsService = new FiftyOneJSServiceCore.Default( clientsidePropertyServiceCore, - clientsideEnabled); + clientsideEnabled, + javaScriptPath, + jsonPath); + } + + private static boolean getBooleanInitParameter( + FilterConfig filterConfig, + String name, + boolean defaultValue) { + String value = filterConfig.getInitParameter(name); + return value == null ? defaultValue : Boolean.parseBoolean(value); + } + + private static String getPathInitParameter( + FilterConfig filterConfig, + String name, + String defaultValue) { + String value = filterConfig.getInitParameter(name); + return value == null || value.trim().isEmpty() ? + defaultValue : value.trim(); } @Override diff --git a/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/StartupHelpers.java b/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/StartupHelpers.java index 77a373429..8641e84f8 100644 --- a/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/StartupHelpers.java +++ b/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/StartupHelpers.java @@ -100,6 +100,43 @@ public static Pipeline buildFromConfiguration( boolean clientSideEvidenceEnabled, String contextRoot) throws Exception { + return StartupHelpers.buildFromConfiguration(builder, options, + clientSideEvidenceEnabled, contextRoot, Constants.DEFAULT_JSON_PATH, + Constants.DEFAULT_ELEMENT_ENABLED, + Constants.DEFAULT_ELEMENT_ENABLED); + } + + /** + * Configure the extra web elements required and build the Pipeline using + * the {@link PipelineBuilder#buildFromConfiguration(PipelineOptions)} + * method. + * @param builder to build the Pipeline + * @param options to build the Pipeline with + * @param clientSideEvidenceEnabled true if client-side evidence is enabled + * in the configuration. This will add JSON + * and JavaScript elements to the Pipeline + * @param contextRoot The context-root setting from the web.xml. This is + * needed in order to create the correct callback URL + * for the JavascriptBuilder. + * @param jsonPath the path the JSON is served from, which the + * JavascriptBuilder calls back to + * @param sequenceElementEnabled true to add the Sequence Element when the + * configuration does not include one + * @param setHeadersElementEnabled true to add the Set Headers Element + * when the configuration does not + * include one + * @return new {@link Pipeline} instance + * @throws Exception if there was an error building the Pipeline + */ + public static Pipeline buildFromConfiguration( + PipelineBuilder builder, + PipelineOptions options, + boolean clientSideEvidenceEnabled, + String contextRoot, + String jsonPath, + boolean sequenceElementEnabled, + boolean setHeadersElementEnabled) throws Exception { + if (options == null || options.elements == null || options.elements.size() == 0) { @@ -107,8 +144,12 @@ public static Pipeline buildFromConfiguration( "Could not find pipeline configuration information"); } + String endpoint = "/" + (jsonPath.startsWith("/") ? + jsonPath.substring(1) : jsonPath); + // Add the sequence element. - if (getElementIndex( + if (sequenceElementEnabled && + getElementIndex( options, SequenceElement.class.getSimpleName()) == -1) { // The sequence element is not included so add it. @@ -147,7 +188,7 @@ public static Pipeline buildFromConfiguration( // The js builder element is not included so add it. ElementOptions javaScriptBuilderElement = new ElementOptions(); javaScriptBuilderElement.builderName = JavaScriptBuilderElement.class.getSimpleName(); - javaScriptBuilderElement.buildParameters.put("Endpoint", "/" + Constants.CORE_JSON_NAME); + javaScriptBuilderElement.buildParameters.put("Endpoint", endpoint); if(contextRoot != null) { javaScriptBuilderElement.buildParameters.put("ContextRoot", contextRoot); } @@ -157,7 +198,7 @@ public static Pipeline buildFromConfiguration( // There is already a JavaScript builder config so check if // the endpoint is specified. If not, add it. if (options.elements.get(jsIndex).buildParameters.containsKey("Endpoint") == false) { - options.elements.get(jsIndex).buildParameters.put("Endpoint", "/" + Constants.CORE_JSON_NAME); + options.elements.get(jsIndex).buildParameters.put("Endpoint", endpoint); } // Same for context root. if(options.elements.get(jsIndex).buildParameters.containsKey("ContextRoot") == false && @@ -168,8 +209,26 @@ public static Pipeline buildFromConfiguration( } - // Add the set-headers element. + // The JSON Builder Element fails on every request without a + // sequence number, which only the Sequence Element supplies. if (getElementIndex( + options, + JsonBuilderElement.class.getSimpleName()) != -1 && + getElementIndex( + options, + SequenceElement.class.getSimpleName()) == -1) { + throw new PipelineConfigurationException( + "The Pipeline has a JSON Builder Element but no Sequence " + + "Element, and the JSON Builder Element needs the " + + "sequence number the Sequence Element supplies. Turn " + + "sequence-element-enabled back on, add a Sequence " + + "Element to the configuration, or turn off " + + "client-side properties."); + } + + // Add the set-headers element. + if (setHeadersElementEnabled && + getElementIndex( options, SetHeadersElement.class.getSimpleName()) == -1) { // The set-headers element is not included so add it. diff --git a/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/services/FiftyOneJSServiceCore.java b/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/services/FiftyOneJSServiceCore.java index f1737d9b0..b769d4dbc 100644 --- a/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/services/FiftyOneJSServiceCore.java +++ b/pipeline/pipeline.web.packages/pipeline.web/src/main/java/fiftyone/pipeline/web/services/FiftyOneJSServiceCore.java @@ -30,6 +30,7 @@ import static fiftyone.pipeline.web.Constants.CORE_JS_NAME; import static fiftyone.pipeline.web.Constants.CORE_JSON_NAME; +import static fiftyone.pipeline.web.Constants.DEFAULT_JSON_PATH; /** * Service that provides the 51Degrees javascript when requested. @@ -71,9 +72,12 @@ boolean serveJson(HttpServletRequest request, HttpServletResponse response) class Default implements FiftyOneJSServiceCore { protected final ClientsidePropertyServiceCore clientsidePropertyServiceCore; protected boolean enabled; + protected String javaScriptPath; + protected String jsonPath; /** - * Construct a new instance. + * Construct a new instance that serves the JavaScript and the JSON + * from the default paths. * @param clientsidePropertyServiceCore used to get the JavaScript to * add to the returned file * @param enabled true if the service should be enabled @@ -81,8 +85,46 @@ class Default implements FiftyOneJSServiceCore { public Default( ClientsidePropertyServiceCore clientsidePropertyServiceCore, boolean enabled) { + this(clientsidePropertyServiceCore, enabled, null, null); + } + + /** + * Construct a new instance. + * @param clientsidePropertyServiceCore used to get the JavaScript to + * add to the returned file + * @param enabled true if the service should be enabled + * @param javaScriptPath the end of the request path that the + * JavaScript is served for, or null or empty + * for the default + * @param jsonPath the end of the request path that the JSON is + * served for, or null or empty for the default + */ + public Default( + ClientsidePropertyServiceCore clientsidePropertyServiceCore, + boolean enabled, + String javaScriptPath, + String jsonPath) { this.clientsidePropertyServiceCore = clientsidePropertyServiceCore; this.enabled = enabled; + this.javaScriptPath = orDefault(javaScriptPath, CORE_JS_NAME); + this.jsonPath = orDefault(jsonPath, DEFAULT_JSON_PATH); + } + + private static String orDefault(String path, String defaultPath) { + return path == null || path.isEmpty() ? defaultPath : path; + } + + /** + * Set the paths the JavaScript and the JSON are served from. + * @param javaScriptPath the end of the request path that the + * JavaScript is served for, or null or empty + * for the default + * @param jsonPath the end of the request path that the JSON is + * served for, or null or empty for the default + */ + public void setPaths(String javaScriptPath, String jsonPath) { + this.javaScriptPath = orDefault(javaScriptPath, CORE_JS_NAME); + this.jsonPath = orDefault(jsonPath, DEFAULT_JSON_PATH); } @Override @@ -91,7 +133,7 @@ public boolean serveJS( HttpServletResponse response) throws IOException { boolean result = false; if (request.getRequestURL().toString().toLowerCase() - .endsWith(CORE_JS_NAME.toLowerCase())) { + .endsWith(javaScriptPath.toLowerCase())) { serveCoreJS(request, response); result = true; } @@ -119,8 +161,9 @@ public boolean serveJson( boolean result = false; - if (request.getRequestURI().toString().toLowerCase() - .endsWith(CORE_JSON_NAME.toLowerCase())) { + String uri = request.getRequestURI().toString().toLowerCase(); + if (uri.endsWith(jsonPath.toLowerCase()) || + uri.endsWith(CORE_JSON_NAME.toLowerCase())) { serveCoreJson(request, response); result = true; } diff --git a/pipeline/pipeline.web.packages/pipeline.web/src/test/java/fiftyone/pipeline/web/FiftyOneJSServiceCoreTests.java b/pipeline/pipeline.web.packages/pipeline.web/src/test/java/fiftyone/pipeline/web/FiftyOneJSServiceCoreTests.java new file mode 100644 index 000000000..ec67818df --- /dev/null +++ b/pipeline/pipeline.web.packages/pipeline.web/src/test/java/fiftyone/pipeline/web/FiftyOneJSServiceCoreTests.java @@ -0,0 +1,140 @@ +/* ********************************************************************* + * This Original Work is copyright of 51 Degrees Mobile Experts Limited. + * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, + * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. + * + * This Original Work is licensed under the European Union Public Licence + * (EUPL) v.1.2 and is subject to its terms as set out below. + * + * If a copy of the EUPL was not distributed with this file, You can obtain + * one at https://opensource.org/licenses/EUPL-1.2. + * + * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be + * amended by the European Commission) shall be deemed incompatible for + * the purposes of the Work and the provisions of the compatibility + * clause in Article 5 of the EUPL shall not apply. + * + * If using the Work as, or as part of, a network application, by + * including the attribution notice(s) required under Article 5 of the EUPL + * in the end user terms of the application under an appropriate heading, + * such notice(s) shall fulfill the requirements of that article. + * ********************************************************************* */ + +package fiftyone.pipeline.web; + +import fiftyone.pipeline.web.services.ClientsidePropertyServiceCore; +import fiftyone.pipeline.web.services.FiftyOneJSServiceCore; +import org.junit.Before; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The paths the JavaScript and the JSON are served from. + * @see Specification + */ +public class FiftyOneJSServiceCoreTests { + + private ClientsidePropertyServiceCore clientside; + private HttpServletRequest request; + private HttpServletResponse response; + + @Before + public void init() { + clientside = mock(ClientsidePropertyServiceCore.class); + request = mock(HttpServletRequest.class); + response = mock(HttpServletResponse.class); + } + + private void requestPath(String path) { + when(request.getRequestURL()) + .thenReturn(new StringBuffer("http://example.com" + path)); + when(request.getRequestURI()).thenReturn(path); + } + + @Test + public void DefaultPaths_JavaScriptAndJsonServed() throws IOException { + FiftyOneJSServiceCore service = + new FiftyOneJSServiceCore.Default(clientside, true); + + requestPath("/51Degrees.core.js"); + assertTrue(service.serveJS(request, response)); + requestPath("/51dpipeline/json"); + assertTrue(service.serveJson(request, response)); + + verify(clientside, times(1)).serveJavascript(request, response); + verify(clientside, times(1)).serveJson(request, response); + } + + /** + * The path the JSON was served from before the specification's path + * became the default is still answered. + */ + @Test + public void DefaultPaths_OldJsonPathStillServed() throws IOException { + FiftyOneJSServiceCore service = + new FiftyOneJSServiceCore.Default(clientside, true); + + requestPath("/51Degrees.core.json"); + assertTrue(service.serveJson(request, response)); + requestPath("/app/51Degrees.core.json"); + assertTrue(service.serveJson(request, response)); + + verify(clientside, times(2)).serveJson(request, response); + } + + @Test + public void ConfiguredPaths_ServedInsteadOfTheDefaults() + throws IOException { + FiftyOneJSServiceCore service = new FiftyOneJSServiceCore.Default( + clientside, true, "/scripts/51d.js", "/51dpipeline/json"); + + requestPath("/scripts/51d.js"); + assertTrue(service.serveJS(request, response)); + requestPath("/51dpipeline/json"); + assertTrue(service.serveJson(request, response)); + + requestPath("/51Degrees.core.js"); + assertFalse(service.serveJS(request, response)); + requestPath("/other/json"); + assertFalse(service.serveJson(request, response)); + + verify(clientside, times(1)).serveJavascript(request, response); + verify(clientside, times(1)).serveJson(request, response); + } + + @Test + public void EmptyPaths_UseTheDefaults() throws IOException { + FiftyOneJSServiceCore service = new FiftyOneJSServiceCore.Default( + clientside, true, "", null); + + requestPath("/51Degrees.core.js"); + assertTrue(service.serveJS(request, response)); + requestPath("/51dpipeline/json"); + assertTrue(service.serveJson(request, response)); + } + + @Test + public void ClientSideDisabled_NothingWritten() throws IOException { + FiftyOneJSServiceCore service = new FiftyOneJSServiceCore.Default( + clientside, false, "/51d.js", "/51dpipeline/json"); + + requestPath("/51d.js"); + assertTrue(service.serveJS(request, response)); + requestPath("/51dpipeline/json"); + assertTrue(service.serveJson(request, response)); + + verify(clientside, never()).serveJavascript(request, response); + verify(clientside, never()).serveJson(request, response); + } +} diff --git a/pipeline/pipeline.web.packages/pipeline.web/src/test/java/fiftyone/pipeline/web/PipelineFilterTests.java b/pipeline/pipeline.web.packages/pipeline.web/src/test/java/fiftyone/pipeline/web/PipelineFilterTests.java index e0f7b7508..c83089011 100644 --- a/pipeline/pipeline.web.packages/pipeline.web/src/test/java/fiftyone/pipeline/web/PipelineFilterTests.java +++ b/pipeline/pipeline.web.packages/pipeline.web/src/test/java/fiftyone/pipeline/web/PipelineFilterTests.java @@ -24,6 +24,13 @@ import fiftyone.pipeline.core.data.ElementData; import fiftyone.pipeline.core.data.FlowData; +import fiftyone.pipeline.core.flowelements.FlowElement; +import fiftyone.pipeline.core.flowelements.Pipeline; +import fiftyone.pipeline.engines.fiftyone.flowelements.SequenceElement; +import fiftyone.pipeline.engines.fiftyone.flowelements.SetHeadersElement; +import fiftyone.pipeline.javascriptbuilder.data.JavaScriptBuilderData; +import fiftyone.pipeline.javascriptbuilder.flowelements.JavaScriptBuilderElement; +import fiftyone.pipeline.jsonbuilder.flowelements.JsonBuilderElement; import org.junit.After; import org.junit.Before; @@ -36,18 +43,24 @@ import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; import java.util.HashMap; +import java.util.List; import java.util.Map; import static fiftyone.pipeline.engines.fiftyone.data.SetHeadersData.*; import static fiftyone.pipeline.engines.fiftyone.flowelements.SetHeadersElement.*; import static fiftyone.pipeline.web.Constants.*; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +@SuppressWarnings("rawtypes") public class PipelineFilterTests { private FilterConfig config; @@ -183,4 +196,155 @@ public void PipelineFilter_CloseFlowData_ChainFails() throws Exception { } verify(flowData, times(1)).close(); } + + private List elementsOf(PipelineFilter initialised) + throws Exception { + Field field = PipelineFilter.class.getDeclaredField("pipeline"); + field.setAccessible(true); + return ((Pipeline) field.get(initialised)).getFlowElements(); + } + + private static boolean hasElement( + List elements, + Class type) { + for (FlowElement element : elements) { + if (type.isInstance(element)) { + return true; + } + } + return false; + } + + private static String endpointOf(List elements) + throws Exception { + for (FlowElement element : elements) { + if (element instanceof JavaScriptBuilderElement) { + Field field = + JavaScriptBuilderElement.class.getDeclaredField("endpoint"); + field.setAccessible(true); + return (String) field.get(element); + } + } + return null; + } + + private PipelineFilter initFilterWith(String name, String value) + throws Exception { + doReturn(value).when(config).getInitParameter(name); + PipelineFilter initialised = new PipelineFilter(); + initialised.init(config); + return initialised; + } + + /** + * Check that the Sequence Element and the Set Headers Element are added + * when the init parameters do not say otherwise. + */ + @Test + public void PipelineFilter_Defaults_AddsSequenceAndSetHeaders() + throws Exception { + filter = new PipelineFilter(); + filter.init(config); + + List elements = elementsOf(filter); + assertTrue(hasElement(elements, SequenceElement.class)); + assertTrue(hasElement(elements, SetHeadersElement.class)); + assertTrue(hasElement(elements, JsonBuilderElement.class)); + assertTrue(hasElement(elements, JavaScriptBuilderElement.class)); + assertEquals("/51dpipeline/json", endpointOf(elements)); + } + + @Test + public void PipelineFilter_SequenceElementDisabled_NotAdded() + throws Exception { + doReturn("false").when(config) + .getInitParameter("clientside-properties-enabled"); + filter = initFilterWith("sequence-element-enabled", "false"); + + List elements = elementsOf(filter); + assertFalse(hasElement(elements, SequenceElement.class)); + assertTrue(hasElement(elements, SetHeadersElement.class)); + } + + /** + * Check that the build is refused when the JSON Builder Element would + * have no Sequence Element before it, as the JSON Builder Element then + * fails on every request. + */ + @Test + public void PipelineFilter_SequenceElementDisabledWithClientSide_Refused() { + doReturn("false").when(config) + .getInitParameter("sequence-element-enabled"); + filter = new PipelineFilter(); + + ServletException e = assertThrows( + ServletException.class, () -> filter.init(config)); + + assertTrue(e.getCause().getMessage().contains("Sequence Element")); + assertTrue(e.getCause().getMessage() + .contains("sequence-element-enabled")); + } + + @Test + public void PipelineFilter_SetHeadersElementDisabled_NotAdded() + throws Exception { + filter = initFilterWith("set-headers-element-enabled", "false"); + + List elements = elementsOf(filter); + assertTrue(hasElement(elements, SequenceElement.class)); + assertFalse(hasElement(elements, SetHeadersElement.class)); + } + + /** + * Check that the client-side switch still removes only the JSON Builder + * Element and the JavaScript Builder Element. + */ + @Test + public void PipelineFilter_ClientSideDisabled_KeepsSequenceAndSetHeaders() + throws Exception { + filter = initFilterWith("clientside-properties-enabled", "false"); + + List elements = elementsOf(filter); + assertTrue(hasElement(elements, SequenceElement.class)); + assertTrue(hasElement(elements, SetHeadersElement.class)); + assertFalse(hasElement(elements, JsonBuilderElement.class)); + assertFalse(hasElement(elements, JavaScriptBuilderElement.class)); + } + + /** + * Check that the script the JavaScript Builder Element renders calls back + * to the specification's JSON path when the init parameter is not set. + */ + @Test + public void PipelineFilter_Defaults_ScriptCallsTheNewJsonPath() + throws Exception { + filter = new PipelineFilter(); + filter.init(config); + Field field = PipelineFilter.class.getDeclaredField("pipeline"); + field.setAccessible(true); + Pipeline built = (Pipeline) field.get(filter); + + try (FlowData processed = built.createFlowData()) { + processed.addEvidence("header.Host", "example.com"); + processed.addEvidence("header.protocol", "https"); + processed.process(); + + String script = processed + .get(JavaScriptBuilderData.class) + .getJavaScript(); + assertTrue(script.contains("https://example.com/51dpipeline/json")); + } + } + + /** + * Check that the JavaScript Builder Element calls back to the JSON path + * set in the init parameter. + */ + @Test + public void PipelineFilter_JsonPath_IsTheCallbackEndpoint() + throws Exception { + filter = initFilterWith("json-path", "/custom/json"); + + assertEquals("/custom/json", endpointOf(elementsOf(filter))); + } }