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..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 @@ -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,16 @@ 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. + // 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 key) -> + keyPrefix(key).toLowerCase(Locale.ROOT)) + .thenComparing((String key) -> key.toLowerCase(Locale.ROOT)) + .reversed()); for (String evidenceKey : evidenceKeys) { String prefix = keyPrefix(evidenceKey); @@ -572,7 +592,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 +614,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 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..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 @@ -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,62 @@ 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"), + Arguments.of("ab-c.Value", "ab.value", "value", "B"), + Arguments.of("a.Value", "_x.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. 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") + 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