From ce6e78fb9c1f2aa035877d039a590e0950ecfd06 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 16:38:30 +0000 Subject: [PATCH 01/17] Test the web integration switches, the Sequence Element data and usage sharing characters The tests cover the init parameters that set the JavaScript and JSON paths and switch the Sequence Element and the Set Headers Element, the Element Data the Sequence Element writes, how the JSON Builder Element shows that data, and the characters XML 1.0 allows in a shared Evidence value. --- .../flowelements/ReplacedStringTest.java | 71 +++++++--- .../flowelements/SequenceElementTests.java | 114 ++++++++++++++++ .../jsonbuilder/JsonBuilderTests.java | 33 +++++ .../web/FiftyOneJSServiceCoreTests.java | 123 ++++++++++++++++++ .../pipeline/web/PipelineFilterTests.java | 116 +++++++++++++++++ 5 files changed, 438 insertions(+), 19 deletions(-) create mode 100644 pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/flowelements/SequenceElementTests.java create mode 100644 pipeline/pipeline.web.packages/pipeline.web/src/test/java/fiftyone/pipeline/web/FiftyOneJSServiceCoreTests.java diff --git a/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/flowelements/ReplacedStringTest.java b/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/flowelements/ReplacedStringTest.java index c98691cca..95176820b 100644 --- a/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/flowelements/ReplacedStringTest.java +++ b/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/flowelements/ReplacedStringTest.java @@ -39,7 +39,7 @@ */ class ReplacedStringTest { - // is transparent to legal characters + // is transparent to the three control characters XML 1.0 allows @Test public void testNormal(){ String aString = IntStream.of(0x9, 0xA, 0xD) @@ -47,8 +47,8 @@ public void testNormal(){ .collect(joining()); ReplacedString replacedString = new ReplacedString(aString); assertFalse(replacedString.isTruncated()); - assertTrue(replacedString.isReplaced()); - assertEquals(3, replacedString.toString().length()); + assertFalse(replacedString.isReplaced()); + assertEquals(aString, replacedString.toString()); } // is transparent to legal characters except \ @Test @@ -123,31 +123,66 @@ public void testEscapedBiggerStill(){ assertEquals(5, replacedString.toString().length()); } - @Test () - public void testEscapedEvenBigger(){ - String aString = IntStream.range(0,5).mapToObj(i -> "\u0100").collect(joining()); + // does not replace characters above U+00FF, which XML 1.0 allows + @Test + public void testNonLatinKept(){ + String aString = "\u0100 \u0416 \u6F22\u5B57 \uD7FF \uE000 \uFFFD"; ReplacedString replacedString = new ReplacedString(aString); assertFalse(replacedString.isTruncated()); - assertTrue(replacedString.isReplaced()); - System.out.println(replacedString); - assertEquals(5, replacedString.toString().length()); - + assertFalse(replacedString.isReplaced()); + assertEquals(aString, replacedString.toString()); + } + // does not replace the controls from U+007F to U+009F, which XML 1.0 allows + @Test + public void testC1ControlsKept(){ + String aString = IntStream.range(0x7F, 0xA0) + .mapToObj(i -> Character.toString((char)i)) + .collect(joining()); + ReplacedString replacedString = new ReplacedString(aString); + assertFalse(replacedString.isReplaced()); + assertEquals(aString, replacedString.toString()); + } + // keeps a pair of surrogates, which is one character above U+FFFF + @Test + public void testSurrogatePairKept(){ + String aString = "a\uD83D\uDE00b"; + ReplacedString replacedString = new ReplacedString(aString); + assertFalse(replacedString.isReplaced()); + assertEquals(aString, replacedString.toString()); + } + // replaces a surrogate on its own, U+FFFE and U+FFFF, which XML 1.0 forbids + @Test + public void testForbiddenCharactersReplaced(){ + String[] forbidden = { "\uD83D", "\uDE00", "\uFFFE", "\uFFFF" }; + for (String character : forbidden) { + ReplacedString replacedString = new ReplacedString( + "a" + character + "b"); + assertTrue(replacedString.isReplaced()); + assertEquals("a\uFFFDb", replacedString.toString()); + } + // A low surrogate before a high one is two surrogates on their own. + ReplacedString reversed = new ReplacedString("\uDE00\uD83D"); + assertTrue(reversed.isReplaced()); + assertEquals("\uFFFD\uFFFD", reversed.toString()); } private final XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newInstance(); private final XMLInputFactory xmlInputFactory =XMLInputFactory.newInstance(); + // text built from the characters XML 1.0 allows survives writing and reading @Test public void testXmlSerialise() throws XMLStreamException { xmlOutputFactory.setProperty("escapeCharacters", true); - String text = ReplacedString.VALID_XML_CHARS.stream() - .map(i->Character.toString((char)i.intValue())) - .collect(joining()); + String text = "\t\n a\u00E9\u0100\u0416\u6F22\u5B57\uD83D\uDE00" + + "\u007F\u0080\u009F\uD7FF\uE000\uFFFD <&>"; + String sanitised = new ReplacedString(text).toString(); + assertEquals(text, sanitised); + ByteArrayOutputStream os = new ByteArrayOutputStream(); XMLStreamWriter writer = xmlOutputFactory.createXMLStreamWriter(os, "UTF-8"); - writer.writeStartDocument("UTF-8", "1.1"); + writer.writeStartDocument("UTF-8", "1.0"); writer.writeStartElement("test"); - writer.writeCharacters(text); + writer.writeCharacters(sanitised); writer.writeEndElement(); writer.close(); @@ -155,10 +190,8 @@ public void testXmlSerialise() throws XMLStreamException { XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(is); reader.next(); String readText = reader.getElementText(); - System.out.println(text); - System.out.println(readText); reader.close(); - assertEquals(readText, text); + assertEquals(text, readText); } -} \ No newline at end of file +} 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..5b66ddbac --- /dev/null +++ b/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/flowelements/SequenceElementTests.java @@ -0,0 +1,114 @@ +/* ********************************************************************* + * 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.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-element"); + assertNotNull(data, "Expected Element Data under 'sequence-element'"); + 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()); + } + } +} 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..313ddd608 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-element"); + 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/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..7d21a4e87 --- /dev/null +++ b/pipeline/pipeline.web.packages/pipeline.web/src/test/java/fiftyone/pipeline/web/FiftyOneJSServiceCoreTests.java @@ -0,0 +1,123 @@ +/* ********************************************************************* + * 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("/51Degrees.core.json"); + assertTrue(service.serveJson(request, response)); + + verify(clientside, times(1)).serveJavascript(request, response); + verify(clientside, times(1)).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("/51Degrees.core.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("/51Degrees.core.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..722b99564 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,12 @@ 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.flowelements.JavaScriptBuilderElement; +import fiftyone.pipeline.jsonbuilder.flowelements.JsonBuilderElement; import org.junit.After; import org.junit.Before; @@ -36,18 +42,23 @@ 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.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 +194,109 @@ 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("/51Degrees.core.json", endpointOf(elements)); + } + + @Test + public void PipelineFilter_SequenceElementDisabled_NotAdded() + throws Exception { + filter = initFilterWith("sequence-element-enabled", "false"); + + List elements = elementsOf(filter); + assertFalse(hasElement(elements, SequenceElement.class)); + assertTrue(hasElement(elements, SetHeadersElement.class)); + } + + @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 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", "/51dpipeline/json"); + + assertEquals("/51dpipeline/json", endpointOf(elementsOf(filter))); + } } From d3030292c38b3ef5c92bd4500fa2e99101063b88 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 16:38:31 +0000 Subject: [PATCH 02/17] Add switches and paths to the web integration, and Element Data to the Sequence Element The filter takes init parameters for the JavaScript path, the JSON path, and whether the Sequence Element and the Set Headers Element are added. The defaults keep today's paths and today's behaviour. The Sequence Element writes session-id and sequence as Element Data as well as Evidence. Usage sharing keeps every character XML 1.0 allows instead of replacing everything above U+00FF. --- .../engines/fiftyone/data/SequenceData.java | 86 +++++++++++++++++++ .../flowelements/SequenceElement.java | 46 +++++++++- .../flowelements/ShareUsageElement.java | 55 ++++++------ .../java/fiftyone/pipeline/web/Constants.java | 32 +++++++ .../fiftyone/pipeline/web/PipelineFilter.java | 44 +++++++++- .../fiftyone/pipeline/web/StartupHelpers.java | 50 ++++++++++- .../web/services/FiftyOneJSServiceCore.java | 32 ++++++- 7 files changed, 307 insertions(+), 38 deletions(-) create mode 100644 pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/data/SequenceData.java 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 39ca21a6f..41bcffc48 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,8 +23,10 @@ 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.util.ArrayList; import java.util.List; import java.util.Map; @@ -38,12 +40,35 @@ public class SequenceElement extends FlowElementBase { + private final List properties; + /** * 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 @@ -63,6 +88,21 @@ protected void processInternal(FlowData data) throws Exception { int seq = (int) sequence; data.addEvidence(Constants.EVIDENCE_SEQUENCE, seq + 1); } + addElementData(data); + } + + /** + * Write the session id and sequence now in Evidence to the element data. + */ + private void addElementData(FlowData data) { + Map evidence = data.getEvidence().asKeyMap(); + SequenceData sequenceData = (SequenceData) data.getOrAdd( + getElementDataKey(), + getDataFactory()); + sequenceData.setSessionId( + String.valueOf(evidence.get(Constants.EVIDENCE_SESSIONID))); + sequenceData.setSequence( + ((Number) evidence.get(Constants.EVIDENCE_SEQUENCE)).intValue()); } @Override @@ -77,7 +117,7 @@ public EvidenceKeyFilter getEvidenceKeyFilter() { @Override public List getProperties() { - return new ArrayList<>(); + return properties; } @Override diff --git a/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/flowelements/ShareUsageElement.java b/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/flowelements/ShareUsageElement.java index c56e032f8..7d4a545dc 100644 --- a/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/flowelements/ShareUsageElement.java +++ b/pipeline/pipeline.engines.fiftyone/src/main/java/fiftyone/pipeline/engines/fiftyone/flowelements/ShareUsageElement.java @@ -40,9 +40,6 @@ import java.net.URI; import java.util.*; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; import java.util.zip.GZIPOutputStream; import static fiftyone.pipeline.engines.fiftyone.flowelements.Constants.SHARE_USAGE_DEFAULT_HTTP_POST_TIMEOUT; @@ -488,18 +485,19 @@ protected void writeXmlElement(XMLStreamWriter writer, * replace characters that cause problems in XML with the "Replacement character" */ public static class ReplacedString { - // a set of valid XML character values (ignoring valid controls x09, x0a, x0d, x85) - static final Set VALID_XML_CHARS = - Stream.of( IntStream.range(0x20,0x7F), - IntStream.range(0xA0, 0x100)) - .flatMapToInt(i -> i) - .boxed() - .collect(Collectors.toCollection(HashSet::new)); - // an array describing whether a character value is valid - static final Boolean[] IS_VALID_XML_CHAR = IntStream - .range(0, 0x100) - .mapToObj(VALID_XML_CHARS::contains) - .toArray(Boolean[]::new); + /** + * Whether the character is allowed in an XML 1.0 document, as the + * Char production in section 2.2 of the XML 1.0 specification says. + * A surrogate is allowed only as half of a pair, which the + * constructor checks. + * @param c the UTF-16 code unit to check + * @return true if the character is allowed in XML 1.0 + */ + static boolean isXmlChar(char c) { + return c == 0x9 || c == 0xA || c == 0xD || + (c >= 0x20 && c <= 0xD7FF) || + (c >= 0xE000 && c <= 0xFFFD); + } // maximum string length static final int MAX_LENGTH = SHARE_USAGE_MAX_EVIDENCE_LENGTH; @@ -515,18 +513,23 @@ public ReplacedString(String text) { // error in Jdk 8. // return; } else { - builder = new StringBuilder(Math.min(text.length(), MAX_LENGTH)); + int limit = Math.min(text.length(), MAX_LENGTH); + builder = new StringBuilder(limit); truncated = text.length() > MAX_LENGTH; - text.chars() - .limit(MAX_LENGTH) - .forEach(c -> { - if (c < IS_VALID_XML_CHAR.length && IS_VALID_XML_CHAR[c]) { - builder.append((char) c); - } else { - builder.append((char) 0xFFFD); - replaced = true; - } - }); + for (int i = 0; i < limit; i++) { + char c = text.charAt(i); + if (Character.isHighSurrogate(c) && + i + 1 < limit && + Character.isLowSurrogate(text.charAt(i + 1))) { + // A pair is one character above U+FFFF. + builder.append(c).append(text.charAt(++i)); + } else if (isXmlChar(c)) { + builder.append(c); + } else { + builder.append((char) 0xFFFD); + replaced = true; + } + } } } 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..3a3ec559a 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 @@ -74,4 +74,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..4d99bcdc2 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.CORE_JSON_NAME; +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, CORE_JSON_NAME); + 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..79568d07a 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.CORE_JSON_NAME, + 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 && @@ -169,7 +210,8 @@ public static Pipeline buildFromConfiguration( } // Add the set-headers element. - if (getElementIndex( + 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..01f67118b 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 @@ -71,9 +71,12 @@ boolean serveJson(HttpServletRequest request, HttpServletResponse response) class Default implements FiftyOneJSServiceCore { protected final ClientsidePropertyServiceCore clientsidePropertyServiceCore; protected boolean enabled; + protected final String javaScriptPath; + protected final 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 +84,31 @@ 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 = javaScriptPath == null || + javaScriptPath.isEmpty() ? CORE_JS_NAME : javaScriptPath; + this.jsonPath = jsonPath == null || jsonPath.isEmpty() ? + CORE_JSON_NAME : jsonPath; } @Override @@ -91,7 +117,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; } @@ -120,7 +146,7 @@ public boolean serveJson( boolean result = false; if (request.getRequestURI().toString().toLowerCase() - .endsWith(CORE_JSON_NAME.toLowerCase())) { + .endsWith(jsonPath.toLowerCase())) { serveCoreJson(request, response); result = true; } From 34e66a4423d3cc03df2c3ae63ed1ed02a385c609 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 17:52:48 +0000 Subject: [PATCH 03/17] Test the MVC integration's paths and element switches --- .../web/mvc/FiftyOneInterceptorTests.java | 107 ++++++++++++++++++ .../src/test/resources/NoSequence.xml | 10 ++ 2 files changed, 117 insertions(+) create mode 100644 pipeline/pipeline.web.packages/pipeline.web.mvc/src/test/resources/NoSequence.xml 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..a66106632 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 @@ -22,11 +22,15 @@ package fiftyone.pipeline.web.mvc; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +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; @@ -36,6 +40,10 @@ import org.junit.runner.RunWith; import fiftyone.pipeline.core.data.FlowData; +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; @@ -138,4 +146,103 @@ public void FiftyOneInterceptor_PreHandle_ServesJavaScript_ClosesFlowData() 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 { + FiftyOneInterceptor created = newInterceptor( + newConfig(false, false, null, null), mock(FiftyOneJSService.class)); + + assertFalse(hasElement(created, SequenceElement.class)); + assertFalse(hasElement(created, SetHeadersElement.class)); + } + + /** + * 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, "51Degrees.core.json"); + } } 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 + + + From f9556fd58def001475dfe7a93a371861c39faa97 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 17:52:55 +0000 Subject: [PATCH 04/17] Give the MVC integration the filter's paths and element switches, and say what the Lite file lacks The MVC configuration takes the JavaScript path, the JSON path and the two element switches the filter takes, with defaults that keep today's behaviour. The example comments about the Lite data file say it has a small number of Properties, as the specification does. --- .../console/GettingStartedOnPrem.java | 4 +- .../examples/console/MetadataOnPrem.java | 4 +- .../examples/console/OfflineProcessing.java | 2 +- .../examples/console/UpdateDataFile.java | 2 +- .../mvc/components/FiftyOneInterceptor.java | 39 ++++++++++++++-- .../FiftyOneInterceptorConfig.java | 34 ++++++++++++++ .../FiftyOneInterceptorConfigDefault.java | 44 +++++++++++++++++++ .../web/mvc/services/FiftyOneJSService.java | 10 +++++ .../web/services/FiftyOneJSServiceCore.java | 16 ++++++- 9 files changed, 144 insertions(+), 11 deletions(-) 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/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..5aa9bedef 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 @@ -93,6 +93,10 @@ public FiftyOneInterceptor( configFileName = Constants.DEFAULT_CONFIG_FILE; } + String jsonPath = config.getJsonPath() == null || + config.getJsonPath().isEmpty() ? + CORE_JSON_NAME : config.getJsonPath(); + File configFile = new File(configFileName); PipelineBuilder builder = new PipelineBuilder() .addService(new DataUpdateServiceDefault( @@ -104,9 +108,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,6 +122,7 @@ public FiftyOneInterceptor( resultService.setPipeline(pipeline); clientsidePropertyService.setPipeline(pipeline); fiftyOneJsService.enable(config.getClientsidePropertiesEnabled()); + fiftyOneJsService.setPaths(config.getJavaScriptPath(), jsonPath); } public static void enableClientsideProperties(ViewControllerRegistry viewControllerRegistry) { @@ -121,6 +130,30 @@ public static void enableClientsideProperties(ViewControllerRegistry viewControl 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() ? + CORE_JSON_NAME : jsonPath)); + } + + 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/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 01f67118b..191e1cceb 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 @@ -71,8 +71,8 @@ boolean serveJson(HttpServletRequest request, HttpServletResponse response) class Default implements FiftyOneJSServiceCore { protected final ClientsidePropertyServiceCore clientsidePropertyServiceCore; protected boolean enabled; - protected final String javaScriptPath; - protected final String jsonPath; + protected String javaScriptPath; + protected String jsonPath; /** * Construct a new instance that serves the JavaScript and the JSON @@ -105,6 +105,18 @@ public Default( String jsonPath) { this.clientsidePropertyServiceCore = clientsidePropertyServiceCore; this.enabled = enabled; + setPaths(javaScriptPath, jsonPath); + } + + /** + * 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 = javaScriptPath == null || javaScriptPath.isEmpty() ? CORE_JS_NAME : javaScriptPath; this.jsonPath = jsonPath == null || jsonPath.isEmpty() ? From f0aa52bddacb1d2c24dcf12b36aff731d6147996 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 17:55:59 +0000 Subject: [PATCH 05/17] Test the Sequence Element data under the key sequence --- .../engines/fiftyone/flowelements/SequenceElementTests.java | 4 ++-- .../java/fiftyone/pipeline/jsonbuilder/JsonBuilderTests.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 index 5b66ddbac..f23b3bc6a 100644 --- 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 @@ -56,8 +56,8 @@ private Pipeline newPipeline() throws Exception { } private ElementData sequenceData(FlowData flowData) throws Exception { - ElementData data = flowData.get("sequence-element"); - assertNotNull(data, "Expected Element Data under 'sequence-element'"); + ElementData data = flowData.get("sequence"); + assertNotNull(data, "Expected Element Data under 'sequence'"); return data; } 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 313ddd608..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 @@ -509,7 +509,7 @@ public void JsonBuilder_SequenceElementData_InJson() throws Exception { JsonBuilderData jsonResult = flowData.get(JsonBuilderData.class); JSONObject jsonData = new JSONObject(jsonResult.getJson()); - JSONObject sequence = jsonData.getJSONObject("sequence-element"); + JSONObject sequence = jsonData.getJSONObject("sequence"); assertEquals(1, sequence.getLong("sequence")); assertFalse(sequence.getString("session-id").isEmpty()); } From 44554b1bc042df5eb447d1aec460ff75a202181a Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 17:56:03 +0000 Subject: [PATCH 06/17] Key the Sequence Element data as sequence, as the other languages do --- .../pipeline/engines/fiftyone/flowelements/SequenceElement.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 41bcffc48..c79c5dacf 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 @@ -107,7 +107,7 @@ private void addElementData(FlowData data) { @Override public String getElementDataKey() { - return "sequence-element"; + return "sequence"; } @Override From a6303a4cfb934dc86f20ac2f50bbaafe200bea7c Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 18:07:05 +0000 Subject: [PATCH 07/17] Do not call an overridable method from the constructor Java 21 warns about it, and the build treats warnings as errors. --- .../web/services/FiftyOneJSServiceCore.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 191e1cceb..42bf6c584 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 @@ -105,7 +105,12 @@ public Default( String jsonPath) { this.clientsidePropertyServiceCore = clientsidePropertyServiceCore; this.enabled = enabled; - setPaths(javaScriptPath, jsonPath); + this.javaScriptPath = orDefault(javaScriptPath, CORE_JS_NAME); + this.jsonPath = orDefault(jsonPath, CORE_JSON_NAME); + } + + private static String orDefault(String path, String defaultPath) { + return path == null || path.isEmpty() ? defaultPath : path; } /** @@ -117,10 +122,8 @@ public Default( * served for, or null or empty for the default */ public void setPaths(String javaScriptPath, String jsonPath) { - this.javaScriptPath = javaScriptPath == null || - javaScriptPath.isEmpty() ? CORE_JS_NAME : javaScriptPath; - this.jsonPath = jsonPath == null || jsonPath.isEmpty() ? - CORE_JSON_NAME : jsonPath; + this.javaScriptPath = orDefault(javaScriptPath, CORE_JS_NAME); + this.jsonPath = orDefault(jsonPath, CORE_JSON_NAME); } @Override From bad4a612588347adeea3a2afcb1ea9e3842c01d8 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 18:14:28 +0000 Subject: [PATCH 08/17] Test that the JSON is served from /51dpipeline/json and from the old path --- .../web/mvc/FiftyOneInterceptorTests.java | 18 +++++++++++- .../web/FiftyOneJSServiceCoreTests.java | 23 +++++++++++++-- .../pipeline/web/PipelineFilterTests.java | 28 ++++++++++++++++++- 3 files changed, 64 insertions(+), 5 deletions(-) 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 a66106632..34b14fa64 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 @@ -40,6 +40,7 @@ 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; @@ -243,6 +244,21 @@ public void FiftyOneInterceptor_NoPaths_DefaultPaths() throws Exception { FiftyOneJSService jsService = mock(FiftyOneJSService.class); newInterceptor(newConfig(true, true, null, null), jsService); - verify(jsService).setPaths(null, "51Degrees.core.json"); + 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/src/test/java/fiftyone/pipeline/web/FiftyOneJSServiceCoreTests.java b/pipeline/pipeline.web.packages/pipeline.web/src/test/java/fiftyone/pipeline/web/FiftyOneJSServiceCoreTests.java index 7d21a4e87..ec67818df 100644 --- 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 @@ -69,13 +69,30 @@ public void DefaultPaths_JavaScriptAndJsonServed() throws IOException { requestPath("/51Degrees.core.js"); assertTrue(service.serveJS(request, response)); - requestPath("/51Degrees.core.json"); + 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 { @@ -89,7 +106,7 @@ public void ConfiguredPaths_ServedInsteadOfTheDefaults() requestPath("/51Degrees.core.js"); assertFalse(service.serveJS(request, response)); - requestPath("/51Degrees.core.json"); + requestPath("/other/json"); assertFalse(service.serveJson(request, response)); verify(clientside, times(1)).serveJavascript(request, response); @@ -103,7 +120,7 @@ public void EmptyPaths_UseTheDefaults() throws IOException { requestPath("/51Degrees.core.js"); assertTrue(service.serveJS(request, response)); - requestPath("/51Degrees.core.json"); + requestPath("/51dpipeline/json"); assertTrue(service.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 722b99564..f9dff72d9 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 @@ -28,6 +28,7 @@ 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; @@ -249,7 +250,7 @@ public void PipelineFilter_Defaults_AddsSequenceAndSetHeaders() assertTrue(hasElement(elements, SetHeadersElement.class)); assertTrue(hasElement(elements, JsonBuilderElement.class)); assertTrue(hasElement(elements, JavaScriptBuilderElement.class)); - assertEquals("/51Degrees.core.json", endpointOf(elements)); + assertEquals("/51dpipeline/json", endpointOf(elements)); } @Test @@ -288,6 +289,31 @@ public void PipelineFilter_ClientSideDisabled_KeepsSequenceAndSetHeaders() 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. From d31b99ca93ba3fe7c06961af288d5973cdfccfbd Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 18:15:28 +0000 Subject: [PATCH 09/17] Serve the JSON from /51dpipeline/json by default, and from the old path as well The specification's JSON path becomes the default in the filter and the MVC integration, and the JavaScript they serve calls it. The path 51Degrees.core.json is still answered, and json-path still sets the path. --- .../src/main/webapp/WEB-INF/51Degrees-Cloud.xml | 2 +- .../src/main/webapp/WEB-INF/51Degrees-OnPrem.xml | 2 +- .../src/main/webapp/WEB-INF/51Degrees-Mixed.xml | 2 +- .../web/mvc/components/FiftyOneInterceptor.java | 7 +++++-- .../src/main/java/fiftyone/pipeline/web/Constants.java | 9 ++++++++- .../java/fiftyone/pipeline/web/PipelineFilter.java | 4 ++-- .../java/fiftyone/pipeline/web/StartupHelpers.java | 2 +- .../pipeline/web/services/FiftyOneJSServiceCore.java | 10 ++++++---- 8 files changed, 25 insertions(+), 13 deletions(-) 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.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 5aa9bedef..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. @@ -95,7 +96,7 @@ public FiftyOneInterceptor( String jsonPath = config.getJsonPath() == null || config.getJsonPath().isEmpty() ? - CORE_JSON_NAME : config.getJsonPath(); + DEFAULT_JSON_PATH : config.getJsonPath(); File configFile = new File(configFileName); PipelineBuilder builder = new PipelineBuilder() @@ -127,6 +128,7 @@ public FiftyOneInterceptor( public static void enableClientsideProperties(ViewControllerRegistry viewControllerRegistry) { viewControllerRegistry.addViewController("/" + CORE_JS_NAME); + viewControllerRegistry.addViewController(DEFAULT_JSON_PATH); viewControllerRegistry.addViewController("/" + CORE_JSON_NAME); } @@ -147,7 +149,8 @@ public static void enableClientsideProperties( CORE_JS_NAME : javaScriptPath)); viewControllerRegistry.addViewController("/" + withoutSlash( jsonPath == null || jsonPath.isEmpty() ? - CORE_JSON_NAME : jsonPath)); + DEFAULT_JSON_PATH : jsonPath)); + viewControllerRegistry.addViewController("/" + CORE_JSON_NAME); } private static String withoutSlash(String path) { 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 3a3ec559a..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. */ 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 4d99bcdc2..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,7 +40,7 @@ import java.io.File; import java.io.IOException; -import static fiftyone.pipeline.web.Constants.CORE_JSON_NAME; +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; @@ -99,7 +99,7 @@ public void init(FilterConfig filterConfig) throws ServletException { String javaScriptPath = getPathInitParameter( filterConfig, INIT_PARAM_JAVASCRIPT_PATH, CORE_JS_NAME); String jsonPath = getPathInitParameter( - filterConfig, INIT_PARAM_JSON_PATH, CORE_JSON_NAME); + filterConfig, INIT_PARAM_JSON_PATH, DEFAULT_JSON_PATH); ServletContext context = config.getServletContext(); 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 79568d07a..29a7460e1 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 @@ -101,7 +101,7 @@ public static Pipeline buildFromConfiguration( String contextRoot) throws Exception { return StartupHelpers.buildFromConfiguration(builder, options, - clientSideEvidenceEnabled, contextRoot, Constants.CORE_JSON_NAME, + clientSideEvidenceEnabled, contextRoot, Constants.DEFAULT_JSON_PATH, Constants.DEFAULT_ELEMENT_ENABLED, Constants.DEFAULT_ELEMENT_ENABLED); } 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 42bf6c584..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. @@ -106,7 +107,7 @@ public Default( this.clientsidePropertyServiceCore = clientsidePropertyServiceCore; this.enabled = enabled; this.javaScriptPath = orDefault(javaScriptPath, CORE_JS_NAME); - this.jsonPath = orDefault(jsonPath, CORE_JSON_NAME); + this.jsonPath = orDefault(jsonPath, DEFAULT_JSON_PATH); } private static String orDefault(String path, String defaultPath) { @@ -123,7 +124,7 @@ private static String orDefault(String path, String defaultPath) { */ public void setPaths(String javaScriptPath, String jsonPath) { this.javaScriptPath = orDefault(javaScriptPath, CORE_JS_NAME); - this.jsonPath = orDefault(jsonPath, CORE_JSON_NAME); + this.jsonPath = orDefault(jsonPath, DEFAULT_JSON_PATH); } @Override @@ -160,8 +161,9 @@ public boolean serveJson( boolean result = false; - if (request.getRequestURI().toString().toLowerCase() - .endsWith(jsonPath.toLowerCase())) { + String uri = request.getRequestURI().toString().toLowerCase(); + if (uri.endsWith(jsonPath.toLowerCase()) || + uri.endsWith(CORE_JSON_NAME.toLowerCase())) { serveCoreJson(request, response); result = true; } From 572ecd775c7edbf65eec03bde5f80dc165c56ba3 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 18:45:51 +0000 Subject: [PATCH 10/17] Test the Sequence Element with session ids and sequences of every kind --- .../flowelements/SequenceElementTests.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) 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 index f23b3bc6a..1240e11c0 100644 --- 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 @@ -35,6 +35,8 @@ 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; @@ -111,4 +113,114 @@ public void Properties_AreSessionIdAndSequence() throws Exception { 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]); + } } From 2261027341a6be47a4ef4bb3a58d04e816e8a009 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 18:46:26 +0000 Subject: [PATCH 11/17] Treat the session id and the sequence independently, as the specification says A session id that is not a non-empty string, and a sequence that is not a whole number or text of decimal digits that fits in an int with room to add one, count as absent. The Evidence and the Element Data are written from the same values. --- .../flowelements/SequenceElement.java | 76 +++++++++++++------ 1 file changed, 52 insertions(+), 24 deletions(-) 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 c79c5dacf..83301b02a 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 @@ -27,10 +27,12 @@ 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.List; import java.util.Map; import java.util.UUID; +import java.util.regex.Pattern; import org.slf4j.Logger; /** @@ -40,6 +42,8 @@ public class SequenceElement extends FlowElementBase { + private static final Pattern DIGITS = Pattern.compile("[0-9]+"); + private final List properties; /** @@ -74,35 +78,59 @@ public ElementData create( @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 { - Object sequence = evidence.get(Constants.EVIDENCE_SEQUENCE); - int seq = (int) sequence; - data.addEvidence(Constants.EVIDENCE_SEQUENCE, seq + 1); - } - addElementData(data); - } + // 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); - /** - * Write the session id and sequence now in Evidence to the element data. - */ - private void addElementData(FlowData data) { - Map evidence = data.getEvidence().asKeyMap(); SequenceData sequenceData = (SequenceData) data.getOrAdd( getElementDataKey(), getDataFactory()); - sequenceData.setSessionId( - String.valueOf(evidence.get(Constants.EVIDENCE_SESSIONID))); - sequenceData.setSequence( - ((Number) evidence.get(Constants.EVIDENCE_SEQUENCE)).intValue()); + sequenceData.setSessionId(sessionId); + sequenceData.setSequence(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 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 From 125e056d01f74caba3fd2439c6abdd8729eaf419 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 19:26:40 +0000 Subject: [PATCH 12/17] Test that the usage sharing filter never includes the web request --- .../data/EvidenceKeyFilterShareUsageTests.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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(); From 16f58d0bedb5ccfc16f7316ad299d64f1ee0176b Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 19:26:55 +0000 Subject: [PATCH 13/17] Keep the web request out of the usage sharing filter, as .NET does --- .../engines/fiftyone/data/EvidenceKeyFilterShareUsage.java | 5 +++++ 1 file changed, 5 insertions(+) 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) || From 2d741ef66819324f95d94b28f42a716b3ff4832e Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 20:41:30 +0000 Subject: [PATCH 14/17] Expect a new session id when the Sequence Element is given an empty one --- .../SessionIdAndSequenceTests.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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)); } From acbb8c7bf193676ecf773bb98b5a3d4d5b350213 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 23:24:01 +0000 Subject: [PATCH 15/17] Test that a build with the JSON Builder Element and no Sequence Element is refused The json-path test uses a path that is not the default. --- .../web/mvc/FiftyOneInterceptorTests.java | 19 +++++++++++++- .../pipeline/web/PipelineFilterTests.java | 26 +++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) 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 34b14fa64..7590a5dd2 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 @@ -24,6 +24,7 @@ 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.*; @@ -205,13 +206,29 @@ public void FiftyOneInterceptor_ElementsEnabled_Added() throws Exception { @Test public void FiftyOneInterceptor_ElementsDisabled_NotAdded() throws Exception { + FiftyOneInterceptorConfig config = newConfig(false, false, null, null); + when(config.getClientsidePropertiesEnabled()).thenReturn(false); FiftyOneInterceptor created = newInterceptor( - newConfig(false, false, null, null), mock(FiftyOneJSService.class)); + 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 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 f9dff72d9..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 @@ -54,6 +54,7 @@ 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.*; @@ -256,6 +257,8 @@ public void PipelineFilter_Defaults_AddsSequenceAndSetHeaders() @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); @@ -263,6 +266,25 @@ public void PipelineFilter_SequenceElementDisabled_NotAdded() 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 { @@ -321,8 +343,8 @@ public void PipelineFilter_Defaults_ScriptCallsTheNewJsonPath() @Test public void PipelineFilter_JsonPath_IsTheCallbackEndpoint() throws Exception { - filter = initFilterWith("json-path", "/51dpipeline/json"); + filter = initFilterWith("json-path", "/custom/json"); - assertEquals("/51dpipeline/json", endpointOf(elementsOf(filter))); + assertEquals("/custom/json", endpointOf(elementsOf(filter))); } } From 26292306fe8a3612447c5f1dac8947cebbf94c2a Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 23:24:18 +0000 Subject: [PATCH 16/17] Refuse a build with the JSON Builder Element and no Sequence Element The JSON Builder Element fails on every request without the sequence number, so the build says which option to change. --- .../fiftyone/pipeline/web/StartupHelpers.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 29a7460e1..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 @@ -209,6 +209,23 @@ public static Pipeline buildFromConfiguration( } + // 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( From f11445617e4584020cbca2078ee76880099cbfbe Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 23:43:15 +0000 Subject: [PATCH 17/17] Use a configuration without the JSON Builder Element when client-side properties are off --- .../web/mvc/FiftyOneInterceptorTests.java | 564 +++++++++--------- .../src/test/resources/NoJson.xml | 7 + 2 files changed, 290 insertions(+), 281 deletions(-) create mode 100644 pipeline/pipeline.web.packages/pipeline.web.mvc/src/test/resources/NoJson.xml 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 7590a5dd2..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,281 +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.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); - 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"); - } -} +/* ********************************************************************* + * 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 + + +