From 9170cab0f570cb3e6a78e50236ae5a8380224e78 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Wed, 11 Nov 2026 13:02:18 +0000 Subject: [PATCH 1/4] Test that one Evidence name in two letter cases is sent once by precedence Evidence keys ignore letter case, so header.User-Agent and query.user-agent name one field, which is sent once with the name and value of the key that wins by the precedence in evidence.md: query, header, cookie, server, fiftyone and location, then any other prefix alphabetically. --- .../flowelements/CloudRequestEngineTests.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/pipeline/pipeline.cloudrequestengine/src/test/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineTests.java b/pipeline/pipeline.cloudrequestengine/src/test/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineTests.java index 994408d1..9e283467 100644 --- a/pipeline/pipeline.cloudrequestengine/src/test/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineTests.java +++ b/pipeline/pipeline.cloudrequestengine/src/test/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineTests.java @@ -51,6 +51,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -539,6 +540,59 @@ public void getFormData(String name, Map evidence, String expect } } + private static Stream oneNameInTwoCasesArgs() { + return Stream.of( + Arguments.of("header.User-Agent", "query.user-agent", "user-agent", "B"), + Arguments.of("query.User-Agent", "header.user-agent", "User-Agent", "A"), + Arguments.of("cookie.X-Thing", "header.x-thing", "x-thing", "B"), + Arguments.of("location.Value", "server.value", "value", "B"), + Arguments.of("location.Value", "fiftyone.value", "value", "B"), + Arguments.of("fiftyone.Value", "server.value", "value", "B"), + Arguments.of("server.Value", "a.value", "Value", "A"), + Arguments.of("b.Value", "a.value", "value", "B")); + } + + /** + * Evidence keys ignore letter case, so one name under two prefixes in + * two letter cases is one field. It is sent once, with the name and + * value of the key that wins by the precedence in evidence.md, being + * query, header, cookie, server, fiftyone and location, then any other + * prefix alphabetically. + */ + @ParameterizedTest(name = "{0} and {1}") + @MethodSource("oneNameInTwoCasesArgs") + public void getFormData_OneNameInTwoCases_SentOnceByPrecedence( + String first, + String second, + String expectedName, + String expectedValue) throws Exception { + acceptEvidence(Arrays.asList(first, second)); + configureMockedClient(); + CloudRequestEngineDefault engine = (CloudRequestEngineDefault) + new CloudRequestEngineBuilder(loggerFactory, httpClient) + .setResourceKey("resourcekey") + .build(); + + try (Pipeline pipeline = new PipelineBuilder(loggerFactory) + .addFlowElement(engine) + .build(); + FlowData data = pipeline.createFlowData()) { + data.addEvidence(first, "A"); + data.addEvidence(second, "B"); + + Map result = engine.getFormData(data); + List names = new ArrayList<>(); + for (String key : result.keySet()) { + if (key.equalsIgnoreCase(expectedName)) { + names.add(key); + } + } + assertEquals(Collections.singletonList(expectedName), names, + "The name must be sent once: " + result); + assertEquals(expectedValue, result.get(expectedName)); + } + } + /** * The web integration adds the whole web request as server.http-request * for usage sharing to read. It holds every header and cookie, so it is From 21eb64f43e1b9894a1caf4eb8a786d54e2b08d93 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Wed, 11 Nov 2026 13:03:23 +0000 Subject: [PATCH 2/4] Send one Evidence name once, whatever its letter case, by precedence Evidence keys ignore letter case, so the form fields are now keyed without regard to case. One name under two prefixes, such as header.User-Agent and query.user-agent, is sent once, with the name and value of the key that wins, where before both were sent. The other prefixes now follow evidence.md's order, being server, fiftyone and location, then any other prefix alphabetically, where before server, fiftyone and location were ranked alphabetically among the rest. --- .../CloudRequestEngineDefault.java | 54 +++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/pipeline/pipeline.cloudrequestengine/src/main/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineDefault.java b/pipeline/pipeline.cloudrequestengine/src/main/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineDefault.java index 2e257108..8e5830de 100644 --- a/pipeline/pipeline.cloudrequestengine/src/main/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineDefault.java +++ b/pipeline/pipeline.cloudrequestengine/src/main/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineDefault.java @@ -131,6 +131,13 @@ public class CloudRequestEngineDefault private final CloudEvidenceKeyFilter evidenceKeyFilter = new CloudEvidenceKeyFilter(); + /** + * The prefixes after query, header and cookie, highest precedence + * first, as evidence.md lists them. + */ + private static final String[] OTHER_PREFIX_PRECEDENCE = + { "server", "fiftyone", "location" }; + /** * Held while the discovery requests are made, so that concurrent first * uses make them once. @@ -469,9 +476,11 @@ private byte[] getContent(FlowData data) throws UnsupportedEncodingException { /** * The Evidence to send, keyed by the name the cloud service expects. - * Only Evidence that {@link #isSent(String)} allows is included, and - * where two prefixes give the same name the precedence order decides, - * query first, then header, then cookie, then any other prefix. + * Only Evidence that {@link #isSent(String)} allows is included. Keys + * ignore letter case, so one name under two prefixes is one field, + * sent with the name and value of the key that wins by the precedence + * in evidence.md, being query, header, cookie, server, fiftyone and + * location, then any other prefix alphabetically. * Evidence with no prefix ranks with the other prefixes, unless the * whole key is query, header or cookie, as {@link #keyPrefix(String)} * gives the whole key as its prefix. @@ -486,9 +495,12 @@ Map getFormData(FlowData flowData) { evidence.put(entry.getKey(), entry.getValue()); } } - Map formData = new HashMap<>(); + Map formData = + new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - // Add evidence in reverse alphabetical order, excluding special keys. + // Add the other evidence lowest precedence first, being other + // prefixes in reverse alphabetical order, then location, fiftyone + // and server. addFormData(formData, evidence, getSelectedEvidence(evidence, "other")); // Add cookie evidence. addFormData(formData, evidence, getSelectedEvidence(evidence, "cookie")); @@ -552,8 +564,12 @@ private void addFormData( Map allEvidence, Map evidence) { - List evidenceKeys = Arrays.asList(evidence.keySet().toArray(new String[0])); - Collections.sort(evidenceKeys, Collections.reverseOrder()); + // Lowest precedence first, so a later key replaces an earlier one. + List evidenceKeys = new ArrayList<>(evidence.keySet()); + evidenceKeys.sort(Comparator + .comparingInt((String key) -> otherPrefixRank(keyPrefix(key))) + .thenComparing(String.CASE_INSENSITIVE_ORDER) + .reversed()); for (String evidenceKey : evidenceKeys) { String prefix = keyPrefix(evidenceKey); @@ -572,7 +588,9 @@ private void addFormData( if (prefix.equalsIgnoreCase("query") == false) { Map conflicts = new HashMap<>(); for (String key : allEvidence.keySet()) { - if (key.equals(evidenceKey) == false && key.contains(suffix)) { + if (key.equals(evidenceKey) == false && + key.toLowerCase(Locale.ROOT).contains( + suffix.toLowerCase(Locale.ROOT))) { conflicts.put(key, allEvidence.get(key)); } } @@ -592,12 +610,30 @@ private void addFormData( conflictStr.toString()); logger.warn(warningMessage); } - // Overwrite the existing queryParameter value. + // Replace the existing field, name and value, so the name + // sent is the winning key's. + formData.remove(suffix); formData.put(suffix, evidence.get(evidenceKey)); } } } + /** + * The precedence of a prefix that is not query, header or cookie, + * lower first, being server, fiftyone and location as evidence.md + * lists them, then any other prefix. + * @param prefix the prefix + * @return the rank, where a lower rank wins + */ + private static int otherPrefixRank(String prefix) { + for (int i = 0; i < OTHER_PREFIX_PRECEDENCE.length; i++) { + if (OTHER_PREFIX_PRECEDENCE[i].equalsIgnoreCase(prefix)) { + return i; + } + } + return OTHER_PREFIX_PRECEDENCE.length; + } + /** * Get evidence with specified prefix. * @param evidence the Evidence being sent From a1f1fd9b56f76c95391cd3897993331f5962abf2 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Wed, 11 Nov 2026 16:15:28 +0000 Subject: [PATCH 3/4] Test that two other prefixes are compared as prefixes ab.value ranks before ab-c.Value, because the prefix ab sorts before ab-c, where comparing the whole key put ab-c first, as a hyphen sorts before a dot. _x ranks before a, as the prefixes compare in lower case. --- .../flowelements/CloudRequestEngineTests.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pipeline/pipeline.cloudrequestengine/src/test/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineTests.java b/pipeline/pipeline.cloudrequestengine/src/test/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineTests.java index 9e283467..3a64e5f5 100644 --- a/pipeline/pipeline.cloudrequestengine/src/test/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineTests.java +++ b/pipeline/pipeline.cloudrequestengine/src/test/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineTests.java @@ -549,7 +549,9 @@ private static Stream oneNameInTwoCasesArgs() { Arguments.of("location.Value", "fiftyone.value", "value", "B"), Arguments.of("fiftyone.Value", "server.value", "value", "B"), Arguments.of("server.Value", "a.value", "Value", "A"), - Arguments.of("b.Value", "a.value", "value", "B")); + Arguments.of("b.Value", "a.value", "value", "B"), + Arguments.of("ab-c.Value", "ab.value", "value", "B"), + Arguments.of("a.Value", "_x.value", "value", "B")); } /** @@ -557,7 +559,8 @@ private static Stream oneNameInTwoCasesArgs() { * two letter cases is one field. It is sent once, with the name and * value of the key that wins by the precedence in evidence.md, being * query, header, cookie, server, fiftyone and location, then any other - * prefix alphabetically. + * prefix alphabetically. Two other prefixes are compared as prefixes, + * in lower case, so ab ranks before ab-c and _x before a. */ @ParameterizedTest(name = "{0} and {1}") @MethodSource("oneNameInTwoCasesArgs") From 1f9e4d5052048477a7ca43abfb966b6929834daa Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Wed, 11 Nov 2026 16:17:39 +0000 Subject: [PATCH 4/4] Compare two other prefixes as prefixes, in lower case Two keys under other prefixes were ordered by the whole key, so ab-c.n ranked before ab.n because a hyphen sorts before a dot. evidence.md ranks prefixes, so the prefixes are compared, in lower case as Go, Node, PHP and Python compare them, and the whole key only breaks a tie. --- .../flowelements/CloudRequestEngineDefault.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pipeline/pipeline.cloudrequestengine/src/main/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineDefault.java b/pipeline/pipeline.cloudrequestengine/src/main/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineDefault.java index 8e5830de..9ba26c7e 100644 --- a/pipeline/pipeline.cloudrequestengine/src/main/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineDefault.java +++ b/pipeline/pipeline.cloudrequestengine/src/main/java/fiftyone/pipeline/cloudrequestengine/flowelements/CloudRequestEngineDefault.java @@ -565,10 +565,14 @@ private void addFormData( Map evidence) { // Lowest precedence first, so a later key replaces an earlier one. + // Other prefixes are compared as prefixes, in lower case, then the + // whole key, so that the order is the same whatever the map gives. List evidenceKeys = new ArrayList<>(evidence.keySet()); evidenceKeys.sort(Comparator .comparingInt((String key) -> otherPrefixRank(keyPrefix(key))) - .thenComparing(String.CASE_INSENSITIVE_ORDER) + .thenComparing((String key) -> + keyPrefix(key).toLowerCase(Locale.ROOT)) + .thenComparing((String key) -> key.toLowerCase(Locale.ROOT)) .reversed()); for (String evidenceKey : evidenceKeys) {