diff --git a/model/base/src/main/java/org/eclipse/ditto/model/base/common/Placeholders.java b/model/base/src/main/java/org/eclipse/ditto/model/base/common/Placeholders.java index 3951464089..2b2918070c 100644 --- a/model/base/src/main/java/org/eclipse/ditto/model/base/common/Placeholders.java +++ b/model/base/src/main/java/org/eclipse/ditto/model/base/common/Placeholders.java @@ -112,7 +112,7 @@ private static boolean containsLegacyRequestSubjectIdPlaceholder(final CharSeque * null, instead it should throw a specific exception if a placeholder cannot be replaced. * @param unresolvedInputHandler exception handler providing a exception which is thrown when placeholders * remain unresolved, e.g. when brackets have the wrong order. - * the replaced input, if the input contains placeholders; the (same) input object, if no placeholders were + * @return the replaced input, if the input contains placeholders; the (same) input object, if no placeholders were * contained in the input. * @throws IllegalStateException if {@code placeholderReplacerFunction} returns null * @throws DittoRuntimeException the passed in {@code unresolvedInputHandler} will be used in order to throw the diff --git a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/EnforcementFactoryFactory.java b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/EnforcementFactoryFactory.java index 9b68059ca5..7f5b80d9d8 100644 --- a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/EnforcementFactoryFactory.java +++ b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/EnforcementFactoryFactory.java @@ -43,8 +43,7 @@ public static EnforcementFilterFactory newEnforcementFilterFactor */ public static EnforcementFilterFactory newEnforcementFilterFactory(final Enforcement enforcement, final Placeholder inputFilter) { - return new ImmutableEnforcementFilterFactory<>(enforcement, inputFilter, - PlaceholderFactory.newThingPlaceholder()); + return newEnforcementFilterFactory(enforcement, inputFilter, PlaceholderFactory.newThingPlaceholder()); } private EnforcementFactoryFactory() { diff --git a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/ExpressionResolver.java b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/ExpressionResolver.java index 661141841b..1410fb3554 100644 --- a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/ExpressionResolver.java +++ b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/ExpressionResolver.java @@ -32,7 +32,7 @@ public interface ExpressionResolver { /** - * Resolves a complete expression template starting with a {@link Placeholder}s followed by optional pipeline stages + * Resolves a complete expression template starting with a {@link Placeholder} followed by optional pipeline stages * (e.g. functions). * * @param expressionTemplate the expressionTemplate to resolve {@link Placeholder}s and and execute optional diff --git a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/ImmutableExpressionResolver.java b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/ImmutableExpressionResolver.java index de8bf0b87f..f9456fc4d8 100644 --- a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/ImmutableExpressionResolver.java +++ b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/ImmutableExpressionResolver.java @@ -41,8 +41,7 @@ final class ImmutableExpressionResolver implements ExpressionResolver { private final List> placeholderResolvers; - ImmutableExpressionResolver( - final List> placeholderResolvers) { + ImmutableExpressionResolver(final List> placeholderResolvers) { this.placeholderResolvers = Collections.unmodifiableList(new ArrayList<>(placeholderResolvers)); } @@ -58,8 +57,9 @@ public String resolve(final String expressionTemplate, final boolean allowUnreso makePlaceholderReplacerFunction(placeholderResolver); placeholdersIdx++; + final boolean isLastPlaceholderResolver = placeholdersIdx == placeholderResolvers.size(); templateInWork = Placeholders.substitute(templateInWork, placeholderReplacerFunction, - UNRESOLVED_INPUT_HANDLER, placeholdersIdx < placeholderResolvers.size() || allowUnresolved); + UNRESOLVED_INPUT_HANDLER, !isLastPlaceholderResolver || allowUnresolved); } return templateInWork; @@ -91,10 +91,10 @@ private Function> makePlaceholderReplacerFunction( final String placeholderTemplate = pipelineStagesExpressions.get(0); // the first pipeline stage has to start with a placeholder - final List pipelineStages = - pipelineStagesExpressions.subList(1, pipelineStagesExpressions.size()).stream() - .map(String::trim) - .collect(Collectors.toList()); + final List pipelineStages = pipelineStagesExpressions.stream() + .skip(1) + .map(String::trim) + .collect(Collectors.toList()); final Pipeline pipeline = new ImmutablePipeline(ImmutableFunctionExpression.INSTANCE, pipelineStages); final Optional pipelineInput = resolvePlaceholder(placeholderResolver, placeholderTemplate); @@ -141,4 +141,5 @@ public String toString() { "placeholderResolvers=" + placeholderResolvers + "]"; } + } diff --git a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/ImmutableFunctionExpression.java b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/ImmutableFunctionExpression.java index 1017a843b9..e5717f8ec6 100644 --- a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/ImmutableFunctionExpression.java +++ b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/ImmutableFunctionExpression.java @@ -10,7 +10,8 @@ */ package org.eclipse.ditto.model.placeholders; -import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -28,16 +29,13 @@ final class ImmutableFunctionExpression implements FunctionExpression { */ static final ImmutableFunctionExpression INSTANCE = new ImmutableFunctionExpression(); - private static final List SUPPORTED; - - static { - SUPPORTED = new ArrayList<>(); - SUPPORTED.add(new PipelineFunctionDefault()); // fn:default('fallback value') - SUPPORTED.add(new PipelineFunctionSubstringBefore()); // fn:substring-before(':') - SUPPORTED.add(new PipelineFunctionSubstringAfter()); // fn:substring-after(':') - SUPPORTED.add(new PipelineFunctionLower()); // fn:lower() - SUPPORTED.add(new PipelineFunctionUpper()); // fn:upper() - } + private static final List SUPPORTED = Collections.unmodifiableList(Arrays.asList( + new PipelineFunctionDefault(), // fn:default('fallback value') + new PipelineFunctionSubstringBefore(), // fn:substring-before(':') + new PipelineFunctionSubstringAfter(), // fn:substring-after(':') + new PipelineFunctionLower(), // fn:lower() + new PipelineFunctionUpper() // fn:upper() + )); @Override public String getPrefix() { @@ -55,8 +53,8 @@ public List getSupportedNames() { @Override public boolean supports(final String expression) { - // it is sufficient that the passed in name starts with the function name, e.g.: default('foo') - // the function validates itself whether the remaining part is valid + // it is sufficient that the passed in name starts with the function name and an opening parentheses, + // e.g.: default('foo'). the function validates itself whether the remaining part is valid. return SUPPORTED.stream() .map(PipelineFunction::getName) .map(psfName -> psfName.replaceFirst(getPrefix() + ":", "")) @@ -73,7 +71,8 @@ public Optional resolve(final String expression, final Optional return SUPPORTED.stream() .filter(pf -> expression.startsWith(getPrefix() + ":" + pf.getName() + "(")) - .map(pf -> pf.apply(resolvedInputValue, expression.replaceFirst(getPrefix() + ":" + pf.getName(), "").trim(), + .map(pf -> pf.apply(resolvedInputValue, + expression.replaceFirst(getPrefix() + ":" + pf.getName(), "").trim(), expressionResolver) ) .findFirst() diff --git a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionDefault.java b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionDefault.java index 192f5d2cbc..dbb119289b 100644 --- a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionDefault.java +++ b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionDefault.java @@ -47,19 +47,18 @@ public DefaultFunctionSignature getSignature() { public Optional apply(final Optional value, final String paramsIncludingParentheses, final ExpressionResolver expressionResolver) { - // parse + resolve the specified default value: - final ResolvedFunctionParameter resolvedDefaultParam = - parseAndResolve(paramsIncludingParentheses, expressionResolver).get(0); - if (value.isPresent()) { // if previous stage was non-empty: proceed with that return value; } else { + // parse + resolve the specified default value: + final ResolvedFunctionParameter resolvedDefaultParam = + parseAndResolve(paramsIncludingParentheses, expressionResolver); return Optional.of(resolvedDefaultParam.getValue()); } } - private List parseAndResolve(final String paramsIncludingParentheses, + private ResolvedFunctionParameter parseAndResolve(final String paramsIncludingParentheses, final ExpressionResolver expressionResolver) { final ParameterDefinition defaultValueParam = getSignature().getParameterDefinition(0); @@ -69,15 +68,13 @@ private List parseAndResolve(final String paramsInclu String constant = matcher.group("singleQuotedConstant"); constant = constant != null ? constant : matcher.group("doubleQuotedConstant"); if (constant != null) { - return Collections.singletonList( - new ResolvedDefaultValueParam(defaultValueParam, constant)); + return new ResolvedDefaultValueParam(defaultValueParam, constant); } final String placeholder = matcher.group("placeholder"); if (placeholder != null) { final Optional resolved = expressionResolver.resolveSinglePlaceholder(placeholder); - return Collections.singletonList( - new ResolvedDefaultValueParam(defaultValueParam, resolved.orElse(placeholder))); + return new ResolvedDefaultValueParam(defaultValueParam, resolved.orElse(placeholder)); } } diff --git a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionLower.java b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionLower.java index edc43947c7..62df2fa84a 100644 --- a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionLower.java +++ b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionLower.java @@ -44,20 +44,19 @@ public Optional apply(final Optional value, final String paramsI final ExpressionResolver expressionResolver) { // check if signature matches (empty params!) - parseAndResolve(paramsIncludingParentheses); + validateOrThrow(paramsIncludingParentheses); return value.map(String::toLowerCase); } - private List parseAndResolve(final String paramsIncludingParentheses) { + private void validateOrThrow(final String paramsIncludingParentheses) { final Matcher matcher = OVERALL_PATTERN.matcher(paramsIncludingParentheses); - if (matcher.matches()) { + if (!matcher.matches()) { - return Collections.emptyList(); + throw PlaceholderFunctionSignatureInvalidException.newBuilder(paramsIncludingParentheses, this) + .build(); } - throw PlaceholderFunctionSignatureInvalidException.newBuilder(paramsIncludingParentheses, this) - .build(); } /** diff --git a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionSubstringAfter.java b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionSubstringAfter.java index 2e3bf282b5..76ceca7b93 100644 --- a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionSubstringAfter.java +++ b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionSubstringAfter.java @@ -45,8 +45,7 @@ public Signature getSignature() { public Optional apply(final Optional value, final String paramsIncludingParentheses, final ExpressionResolver expressionResolver) { - final ResolvedFunctionParameter resolvedSubstringBeforeParam = - parseAndResolve(paramsIncludingParentheses).get(0); + final ResolvedFunctionParameter resolvedSubstringBeforeParam = parseAndResolve(paramsIncludingParentheses); final String splitValue = resolvedSubstringBeforeParam.getValue(); return value.map(previousStage -> { @@ -58,7 +57,7 @@ public Optional apply(final Optional value, final String paramsI }); } - private List parseAndResolve(final String paramsIncludingParentheses) { + private ResolvedFunctionParameter parseAndResolve(final String paramsIncludingParentheses) { final ParameterDefinition givenStringParam = getSignature().getParameterDefinition(0); final Matcher matcher = OVERALL_PATTERN.matcher(paramsIncludingParentheses); @@ -67,7 +66,7 @@ private List parseAndResolve(final String paramsInclu String constant = matcher.group("singleQuotedConstant"); constant = constant != null ? constant : matcher.group("doubleQuotedConstant"); if (constant != null) { - return Collections.singletonList(new ResolvedGivenStringParam(givenStringParam, constant)); + return new ResolvedGivenStringParam(givenStringParam, constant); } } diff --git a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionSubstringBefore.java b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionSubstringBefore.java index 796b81294f..1954bea119 100644 --- a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionSubstringBefore.java +++ b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionSubstringBefore.java @@ -45,8 +45,7 @@ public Signature getSignature() { public Optional apply(final Optional value, final String paramsIncludingParentheses, final ExpressionResolver expressionResolver) { - final ResolvedFunctionParameter resolvedSubstringBeforeParam = - parseAndResolve(paramsIncludingParentheses).get(0); + final ResolvedFunctionParameter resolvedSubstringBeforeParam = parseAndResolve(paramsIncludingParentheses); final String splitValue = resolvedSubstringBeforeParam.getValue(); return value.map(previousStage -> { @@ -58,7 +57,7 @@ public Optional apply(final Optional value, final String paramsI }); } - private List parseAndResolve(final String paramsIncludingParentheses) { + private ResolvedFunctionParameter parseAndResolve(final String paramsIncludingParentheses) { final ParameterDefinition givenStringParam = getSignature().getParameterDefinition(0); final Matcher matcher = OVERALL_PATTERN.matcher(paramsIncludingParentheses); @@ -67,7 +66,7 @@ private List parseAndResolve(final String paramsInclu String constant = matcher.group("singleQuotedConstant"); constant = constant != null ? constant : matcher.group("doubleQuotedConstant"); if (constant != null) { - return Collections.singletonList(new ResolvedGivenStringParam(givenStringParam, constant)); + return new ResolvedGivenStringParam(givenStringParam, constant); } } diff --git a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionUpper.java b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionUpper.java index b5c0b21817..ce57649f37 100644 --- a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionUpper.java +++ b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PipelineFunctionUpper.java @@ -44,20 +44,17 @@ public Optional apply(final Optional value, final String paramsI final ExpressionResolver expressionResolver) { // check if signature matches (empty params!) - parseAndResolve(paramsIncludingParentheses); + validateOrThrow(paramsIncludingParentheses); return value.map(String::toUpperCase); } - private List parseAndResolve(final String paramsIncludingParentheses) { + private void validateOrThrow(final String paramsIncludingParentheses) { final Matcher matcher = OVERALL_PATTERN.matcher(paramsIncludingParentheses); - if (matcher.matches()) { - - return Collections.emptyList(); + if (!matcher.matches()) { + throw PlaceholderFunctionSignatureInvalidException.newBuilder(paramsIncludingParentheses, this) + .build(); } - - throw PlaceholderFunctionSignatureInvalidException.newBuilder(paramsIncludingParentheses, this) - .build(); } /** diff --git a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/Placeholder.java b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/Placeholder.java index 22bacf00ac..f71b2c1a22 100644 --- a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/Placeholder.java +++ b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/Placeholder.java @@ -22,7 +22,7 @@ public interface Placeholder extends Expression { /** * Resolves the placeholder variable by name. * - * @param placeholderSource the source from which to the placeholder is resolved + * @param placeholderSource the source from which to the placeholder is resolved, e.g. a Thing id. * @param name the placeholder variable name (i. e., the part after ':'). * @return value of the placeholder variable if the placeholder name is supported, or an empty optional otherwise. */ diff --git a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PlaceholderFilter.java b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PlaceholderFilter.java index 043e7e2d15..ac8e10ef2c 100644 --- a/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PlaceholderFilter.java +++ b/model/placeholders/src/main/java/org/eclipse/ditto/model/placeholders/PlaceholderFilter.java @@ -160,17 +160,14 @@ public static String apply(final String template, final ExpressionResolver expre } /** - * Validates that the passed {@code template} is both valid and depending on the {@code allowUnresolved} boolean - * that the placeholders in the passed {@code template} are completely replaceable by the provided - * {@code placeholders}. + * Validates that the passed {@code template} is valid and that the placeholders in the passed {@code template} + * are completely replaceable by the provided {@code placeholders}. * * @param template a string potentially containing placeholders to replace - * @param allowUnresolved whether to allow if there could be placeholders in the template left unreplaced * @param placeholders the {@link Placeholder}s to use for replacement * @throws UnresolvedPlaceholderException in case the template's placeholders could not completely be resolved */ - public static void validate(final String template, final boolean allowUnresolved, - final Placeholder... placeholders) { + public static void validate(final String template, final Placeholder... placeholders) { String replaced = template; for (int i = 0; i < placeholders.length; i++) { boolean isNotLastPlaceholder = i < placeholders.length - 1; @@ -178,7 +175,7 @@ public static void validate(final String template, final boolean allowUnresolved final ExpressionResolver expressionResolver = PlaceholderFactory .newExpressionResolverForValidation(thePlaceholder); - replaced = doApply(replaced, expressionResolver, allowUnresolved || isNotLastPlaceholder); + replaced = doApply(replaced, expressionResolver, isNotLastPlaceholder); } } @@ -186,9 +183,7 @@ private static String doApply(final String template, final ExpressionResolver expressionResolver, final boolean allowUnresolved) { - String templateInWork = template; - templateInWork = expressionResolver.resolve(templateInWork, allowUnresolved); - return templateInWork; + return expressionResolver.resolve(template, allowUnresolved); } static String checkAllPlaceholdersResolved(final String input) { diff --git a/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutableExpressionResolverTest.java b/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutableExpressionResolverTest.java index 811f741e9a..53d09c60e2 100644 --- a/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutableExpressionResolverTest.java +++ b/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutableExpressionResolverTest.java @@ -14,9 +14,9 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.util.Arrays; -import java.util.HashMap; import java.util.Map; +import org.eclipse.ditto.model.base.headers.DittoHeaders; import org.eclipse.ditto.model.connectivity.UnresolvedPlaceholderException; import org.eclipse.ditto.protocoladapter.ProtocolFactory; import org.eclipse.ditto.protocoladapter.TopicPath; @@ -36,6 +36,11 @@ public class ImmutableExpressionResolverTest { private static final String THING_NAME = "foobar199"; private static final String THING_ID = "org.eclipse.ditto:" + THING_NAME; private static final String KNOWN_TOPIC = "org.eclipse.ditto/" + THING_NAME + "/things/twin/commands/modify"; + private static final Map KNOWN_HEADERS = + DittoHeaders.newBuilder().putHeader("one", "1").putHeader("two", "2").build(); + public static final String UNKNOWN_HEADER_EXPRESSION = "{{ header:missing }}"; + public static final String UNKNOWN_THING_EXPRESSION = "{{ thing:missing }}"; + public static final String UNKNOWN_TOPIC_EXPRESSION = "{{ topic:missing }}"; private static ImmutableExpressionResolver underTest; @@ -55,70 +60,82 @@ public void testHashCodeAndEquals() { @BeforeClass public static void setupClass() { - final Map inputMap = new HashMap<>(); - inputMap.put("one", "1"); - inputMap.put("two", "2"); - final TopicPath topic = ProtocolFactory.newTopicPath(KNOWN_TOPIC); - final ImmutablePlaceholderResolver> placeholderResolver1 = - new ImmutablePlaceholderResolver<>( - PlaceholderFactory.newHeadersPlaceholder(), inputMap, false); - final ImmutablePlaceholderResolver placeholderResolver2 = new ImmutablePlaceholderResolver<>( + final ImmutablePlaceholderResolver> headersResolver = + new ImmutablePlaceholderResolver<>(PlaceholderFactory.newHeadersPlaceholder(), KNOWN_HEADERS, false); + final ImmutablePlaceholderResolver thingResolver = new ImmutablePlaceholderResolver<>( PlaceholderFactory.newThingPlaceholder(), THING_ID, false); - final ImmutablePlaceholderResolver placeholderResolver3 = new ImmutablePlaceholderResolver<>( + final ImmutablePlaceholderResolver topicPathResolver = new ImmutablePlaceholderResolver<>( PlaceholderFactory.newTopicPathPlaceholder(), topic, false); - underTest = new ImmutableExpressionResolver( - Arrays.asList(placeholderResolver1, placeholderResolver2, placeholderResolver3)); + underTest = new ImmutableExpressionResolver(Arrays.asList(headersResolver, thingResolver, topicPathResolver)); } @Test public void testSuccessfulPlaceholderResolvement() { assertThat(underTest.resolve("{{ header:one }}", false)) - .contains("1"); + .isEqualTo(KNOWN_HEADERS.get("one")); assertThat(underTest.resolve("{{ header:two }}", false)) - .contains("2"); + .isEqualTo(KNOWN_HEADERS.get("two")); assertThat(underTest.resolve("{{ thing:id }}", false)) - .contains(THING_ID); + .isEqualTo(THING_ID); assertThat(underTest.resolve("{{ thing:name }}", false)) - .contains(THING_NAME); + .isEqualTo(THING_NAME); assertThat(underTest.resolve("{{ topic:full }}", false)) - .contains(KNOWN_TOPIC); + .isEqualTo(KNOWN_TOPIC); assertThat(underTest.resolve("{{ topic:entityId }}", false)) - .contains(THING_NAME); + .isEqualTo(THING_NAME); + + // verify different whitespacing + assertThat(underTest.resolve("{{topic:entityId }}", false)) + .isEqualTo(THING_NAME); + assertThat(underTest.resolve("{{topic:entityId}}", false)) + .isEqualTo(THING_NAME); + assertThat(underTest.resolve("{{ topic:entityId}}", false)) + .isEqualTo(THING_NAME); } @Test public void testPlaceholderResolvementAllowingUnresolvedPlaceholders() { - assertThat(underTest.resolve("{{ header:missing }}", true)) - .contains("{{ header:missing }}"); - assertThat(underTest.resolve("{{ thing:missing }}", true)) - .contains("{{ thing:missing }}"); - assertThat(underTest.resolve("{{ topic:missing }}", true)) - .contains("{{ topic:missing }}"); + assertThat(underTest.resolve(UNKNOWN_HEADER_EXPRESSION, true)) + .isEqualTo(UNKNOWN_HEADER_EXPRESSION); + assertThat(underTest.resolve(UNKNOWN_THING_EXPRESSION, true)) + .isEqualTo(UNKNOWN_THING_EXPRESSION); + assertThat(underTest.resolve(UNKNOWN_TOPIC_EXPRESSION, true)) + .isEqualTo(UNKNOWN_TOPIC_EXPRESSION); } @Test public void testUnsuccessfulPlaceholderResolvement() { assertThatExceptionOfType(UnresolvedPlaceholderException.class).isThrownBy(() -> - underTest.resolve("{{ header:missing }}", false)); + underTest.resolve(UNKNOWN_HEADER_EXPRESSION, false)); assertThatExceptionOfType(UnresolvedPlaceholderException.class).isThrownBy(() -> - underTest.resolve("{{ thing:missing }}", false)); + underTest.resolve(UNKNOWN_THING_EXPRESSION, false)); assertThatExceptionOfType(UnresolvedPlaceholderException.class).isThrownBy(() -> - underTest.resolve("{{ topic:missing }}", false)); + underTest.resolve(UNKNOWN_TOPIC_EXPRESSION, false)); } @Test public void testSuccessfulFunctionBasedOnPlaceholderInput() { assertThat(underTest.resolve("{{ header:unknown | fn:default('fallback') }}", false)) - .contains("fallback"); + .isEqualTo("fallback"); assertThat(underTest.resolve("{{ thing:bar | fn:default('bar') | fn:upper() }}", false)) - .contains("BAR"); + .isEqualTo("BAR"); + + // verify different whitespacing + assertThat(underTest.resolve("{{thing:bar |fn:default('bar')| fn:upper() }}", false)) + .isEqualTo("BAR"); + assertThat(underTest.resolve("{{ thing:bar | fn:default('bar') |fn:upper()}}", false)) + .isEqualTo("BAR"); + assertThat(underTest.resolve("{{ thing:bar | fn:default( 'bar' ) |fn:upper( ) }}", false)) + .isEqualTo("BAR"); + assertThat(underTest.resolve("{{ thing:id | fn:substring-before(\"|\") | fn:default('bAz') | fn:lower() }}", false)) + .isEqualTo("baz"); } @Test @@ -130,4 +147,22 @@ public void testUnsuccessfulFunctionBasedOnPlaceholderInput() { underTest.resolve("{{ thing:bar | fn:upper() }}", false)); } + @Test + public void testSuccessfulSinglePlaceholderResolvement() { + assertThat(underTest.resolveSinglePlaceholder("header:one")) + .contains(KNOWN_HEADERS.get("one")); + assertThat(underTest.resolveSinglePlaceholder("thing:id")) + .contains(THING_ID); + } + + @Test + public void testUnsuccessfulSinglePlaceholderResolvement() { + assertThat(underTest.resolveSinglePlaceholder("header:unknown")) + .isEmpty(); + assertThat(underTest.resolveSinglePlaceholder("fn:default('fallback')")) + .isEmpty(); + assertThat(underTest.resolveSinglePlaceholder("fn:substring-before()")) + .isEmpty(); + } + } diff --git a/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutableFunctionExpressionTest.java b/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutableFunctionExpressionTest.java index 49ae31c4c6..546e119e2f 100644 --- a/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutableFunctionExpressionTest.java +++ b/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutableFunctionExpressionTest.java @@ -13,9 +13,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.Map; import java.util.Optional; +import java.util.Set; import org.junit.Test; import org.mutabilitydetector.unittesting.MutabilityAssert; @@ -29,6 +32,13 @@ */ public class ImmutableFunctionExpressionTest { + private static final Set EXPECTED_FUNCTION_NAMES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "default", + "substring-before", + "substring-after", + "lower", + "upper" + ))); private static final HeadersPlaceholder HEADERS_PLACEHOLDER = PlaceholderFactory.newHeadersPlaceholder(); private static final ThingPlaceholder THING_PLACEHOLDER = PlaceholderFactory.newThingPlaceholder(); @@ -60,13 +70,16 @@ public void testHashCodeAndEquals() { } @Test - public void testCompletenessOfRegisteredFunctions() { + public void testSupportedNames() { + assertThat(UNDER_TEST.getSupportedNames()).containsExactlyInAnyOrder( + EXPECTED_FUNCTION_NAMES.toArray(new String[0])); + } - assertThat(UNDER_TEST.supports(PipelineFunctionDefault.FUNCTION_NAME + "(")).isTrue(); - assertThat(UNDER_TEST.supports(PipelineFunctionSubstringBefore.FUNCTION_NAME + "(")).isTrue(); - assertThat(UNDER_TEST.supports(PipelineFunctionSubstringAfter.FUNCTION_NAME + "(")).isTrue(); - assertThat(UNDER_TEST.supports(PipelineFunctionLower.FUNCTION_NAME + "(")).isTrue(); - assertThat(UNDER_TEST.supports(PipelineFunctionUpper.FUNCTION_NAME + "(")).isTrue(); + @Test + public void testCompletenessOfRegisteredFunctions() { + EXPECTED_FUNCTION_NAMES.stream() + .map(name -> name + "(") + .forEach(fn -> assertThat(UNDER_TEST.supports(fn)).isTrue()); } @Test diff --git a/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutableHeadersPlaceholderTest.java b/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutableHeadersPlaceholderTest.java index fed8574e7c..5a1abd6c4d 100644 --- a/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutableHeadersPlaceholderTest.java +++ b/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutableHeadersPlaceholderTest.java @@ -15,6 +15,7 @@ import java.util.HashMap; import java.util.Map; +import org.junit.BeforeClass; import org.junit.Test; import org.mutabilitydetector.unittesting.MutabilityAssert; import org.mutabilitydetector.unittesting.MutabilityMatchers; @@ -32,7 +33,8 @@ public class ImmutableHeadersPlaceholderTest { private static final String DEVICE_ID = "eclipse:ditto:device1234"; - static { + @BeforeClass + public static void setUp() { HEADERS.put("device_id", DEVICE_ID); HEADERS.put("correlation_id", "4205833931151659498"); } diff --git a/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutablePipelineTest.java b/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutablePipelineTest.java index 7887098e18..379c6ae5d6 100644 --- a/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutablePipelineTest.java +++ b/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/ImmutablePipelineTest.java @@ -10,7 +10,19 @@ */ package org.eclipse.ditto.model.placeholders; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; import org.mutabilitydetector.unittesting.AllowedReason; import org.mutabilitydetector.unittesting.MutabilityAssert; import org.mutabilitydetector.unittesting.MutabilityMatchers; @@ -20,8 +32,25 @@ /** * Tests {@link ImmutablePipeline}. */ +@RunWith(MockitoJUnitRunner.class) public class ImmutablePipelineTest { + private static final List STAGES = Arrays.asList( + "thing:name", + "fn:substring-before(':')", + "fn:default(thing:id)" + ); + private static final Optional PIPELINE_INPUT = Optional.of("my-gateway:my-thing"); + private static final List> RESPONSES = Arrays.asList( + Optional.of("my-gateway"), + Optional.of("my-gateway") + ); + + @Mock + private FunctionExpression functionExpression; + @Mock + private ExpressionResolver expressionResolver; + @Test public void assertImmutability() { MutabilityAssert.assertInstancesOf(ImmutablePipeline.class, MutabilityMatchers.areImmutable(), @@ -34,4 +63,35 @@ public void testHashCodeAndEquals() { .usingGetClass() .verify(); } + + @Test + public void execute() { + prepareFunctionExpressionResponses(); + + final ImmutablePipeline pipeline = new ImmutablePipeline(functionExpression, STAGES); + final Optional result = pipeline.execute(PIPELINE_INPUT, expressionResolver); + + verifyResultEqualsLastResponse(result); + verifyFunctionExpressionWasCalledWithIntermediateValues(); + } + + private void prepareFunctionExpressionResponses() { + Mockito.when(functionExpression.resolve(anyString(), any(Optional.class), any(ExpressionResolver.class))) + .thenReturn(RESPONSES.get(0), RESPONSES.get(1)); + } + + private void verifyResultEqualsLastResponse(final Optional result) { + assertThat(result).isEqualTo(RESPONSES.get(1)); + } + + private void verifyFunctionExpressionWasCalledWithIntermediateValues() { + Mockito.verify(functionExpression) + .resolve(STAGES.get(0), PIPELINE_INPUT, expressionResolver); + Mockito.verify(functionExpression) + .resolve(STAGES.get(1), RESPONSES.get(0), expressionResolver); + Mockito.verify(functionExpression) + .resolve(STAGES.get(2), RESPONSES.get(1), expressionResolver); + Mockito.verifyNoMoreInteractions(functionExpression); + } + } diff --git a/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/PlaceholderFilterTest.java b/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/PlaceholderFilterTest.java index 1f2aa05c85..2c60959a61 100644 --- a/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/PlaceholderFilterTest.java +++ b/model/placeholders/src/test/java/org/eclipse/ditto/model/placeholders/PlaceholderFilterTest.java @@ -42,14 +42,18 @@ public class PlaceholderFilterTest { private static final TopicPath KNOWN_TOPIC_PATH_SUBJECT2 = TopicPath.newBuilder(KNOWN_NAMESPACE + ":" + KNOWN_ID) .live().things().messages().subject(KNOWN_SUBJECT2).build(); - private final HeadersPlaceholder headersPlaceholder = PlaceholderFactory.newHeadersPlaceholder(); - private final ThingPlaceholder thingPlaceholder = PlaceholderFactory.newThingPlaceholder(); - private final TopicPathPlaceholder topicPlaceholder = PlaceholderFactory.newTopicPathPlaceholder(); + private static final HeadersPlaceholder headersPlaceholder = PlaceholderFactory.newHeadersPlaceholder(); + private static final ThingPlaceholder thingPlaceholder = PlaceholderFactory.newThingPlaceholder(); + private static final TopicPathPlaceholder topicPlaceholder = PlaceholderFactory.newTopicPathPlaceholder(); - private final FilterTuple[] filterChain = new FilterTuple[]{ + private static final FilterTuple[] filterChain = new FilterTuple[]{ FilterTuple.of(HEADERS, headersPlaceholder), FilterTuple.of(THING_ID, thingPlaceholder) }; + private static final Placeholder[] placeholders = new Placeholder[]{ + headersPlaceholder, + thingPlaceholder + }; static { HEADERS.put("device-id", DEVICE_ID); @@ -66,7 +70,7 @@ public void testHeadersPlaceholder() { () -> PlaceholderFilter.apply("{{ header:unknown }}", HEADERS, headersPlaceholder)); assertThatExceptionOfType(UnresolvedPlaceholderException.class).isThrownBy( () -> PlaceholderFilter.apply("{{ {{ header:device-id }} }}", HEADERS, headersPlaceholder)); - assertThat(PlaceholderFilter.apply("eclipse:ditto", HEADERS, headersPlaceholder)).isEqualTo("eclipse:ditto"); + assertThat(PlaceholderFilter.apply(THING_ID, HEADERS, headersPlaceholder)).isEqualTo(THING_ID); assertThat( PlaceholderFilter.apply("eclipse:ditto:{{ header:device-id }}", HEADERS, headersPlaceholder)).isEqualTo( "eclipse:ditto:device-12345"); @@ -81,7 +85,7 @@ public void testThingPlaceholder() { () -> PlaceholderFilter.apply("{{ header:unknown }}", THING_ID, thingPlaceholder)); assertThatExceptionOfType(UnresolvedPlaceholderException.class).isThrownBy( () -> PlaceholderFilter.apply("{{ {{ thing:name }} }}", THING_ID, thingPlaceholder)); - assertThat(PlaceholderFilter.apply("eclipse:ditto", THING_ID, thingPlaceholder)).isEqualTo("eclipse:ditto"); + assertThat(PlaceholderFilter.apply(THING_ID, THING_ID, thingPlaceholder)).isEqualTo(THING_ID); assertThat(PlaceholderFilter.apply("prefix:{{ thing:namespace }}:{{ thing:name }}:suffix", THING_ID, thingPlaceholder)).isEqualTo("prefix:eclipse:ditto:suffix"); assertThat(PlaceholderFilter.apply("testTargetAmqpCon4_{{thing:namespace}}:{{thing:name}}", THING_ID, @@ -175,6 +179,49 @@ public void testInvalidPlaceholderVariations() { filterChain)); } + @Test + public void testValidate() { + // no whitespace + PlaceholderFilter.validate("{{thing:namespace}}/{{thing:name}}:{{header:device-id}}", placeholders); + + // multi whitespace + PlaceholderFilter.validate("{{ thing:namespace }}/{{ thing:name }}:{{ header:device-id }}", placeholders); + + // mixed whitespace + PlaceholderFilter.validate("{{thing:namespace }}/{{ thing:name }}:{{header:device-id }}", placeholders); + + // no separators + PlaceholderFilter.validate("{{thing:namespace }}{{ thing:name }}{{header:device-id }}", placeholders); + + // whitespace separators + PlaceholderFilter.validate("{{thing:namespace }} {{ thing:name }} {{header:device-id }}", placeholders); + + // pre/postfix whitespace + PlaceholderFilter.validate(" {{thing:namespace }}{{ thing:name }}{{header:device-id }} ", placeholders); + + // pre/postfix + PlaceholderFilter.validate("-----{{thing:namespace }}{{ thing:name }}{{header:device-id }}-----", placeholders); + + // pre/postfix and separators + PlaceholderFilter.validate("-----{{thing:namespace }}///{{ thing:name }}///{{header:device-id }}-----", placeholders); + } + + @Test + public void testValidateFails() { + // illegal braces combinations + assertThatExceptionOfType(UnresolvedPlaceholderException.class).isThrownBy( + () -> PlaceholderFilter.validate("{{th{{ing:namespace }}{{ thing:name }}{{header:device-id }}", placeholders)); + + assertThatExceptionOfType(UnresolvedPlaceholderException.class).isThrownBy( + () -> PlaceholderFilter.validate("{{th}}ing:namespace }}{{ thing:name }}{{header:device-id }}", placeholders)); + + assertThatExceptionOfType(UnresolvedPlaceholderException.class).isThrownBy( + () -> PlaceholderFilter.validate("{{thing:nam{{espace }}{{ thing:name }}{{header:device-id }}", placeholders)); + + assertThatExceptionOfType(UnresolvedPlaceholderException.class).isThrownBy( + () -> PlaceholderFilter.validate("{{thing:nam}}espace }}{{ thing:name }}{{header:device-id }}", placeholders)); + } + private static String filterChain(final String template, final FilterTuple... tuples) { String result = template; for (final FilterTuple tuple : tuples) { diff --git a/services/connectivity/messaging/src/main/java/org/eclipse/ditto/services/connectivity/messaging/MessageMappingProcessorActor.java b/services/connectivity/messaging/src/main/java/org/eclipse/ditto/services/connectivity/messaging/MessageMappingProcessorActor.java index 81fe47f145..8412a9b1e5 100644 --- a/services/connectivity/messaging/src/main/java/org/eclipse/ditto/services/connectivity/messaging/MessageMappingProcessorActor.java +++ b/services/connectivity/messaging/src/main/java/org/eclipse/ditto/services/connectivity/messaging/MessageMappingProcessorActor.java @@ -384,7 +384,7 @@ private Set getCountersForOutboundSignal(final Outbo static final class PlaceholderInTargetAddressSubstitution implements BiFunction { - private static final HeadersPlaceholder HEADER_PLACEHOLDER = PlaceholderFactory.newHeadersPlaceholder(); + private static final HeadersPlaceholder HEADERS_PLACEHOLDER = PlaceholderFactory.newHeadersPlaceholder(); private static final ThingPlaceholder THING_PLACEHOLDER = PlaceholderFactory.newThingPlaceholder(); private static final TopicPathPlaceholder TOPIC_PLACEHOLDER = PlaceholderFactory.newTopicPathPlaceholder(); @@ -404,7 +404,7 @@ public OutboundSignal.WithExternalMessage apply(final OutboundSignal outboundSig .anyMatch(t -> Placeholders.containsAnyPlaceholder(t.getAddress()))) { final ExpressionResolver expressionResolver = PlaceholderFactory.newExpressionResolver( - PlaceholderFactory.newPlaceholderResolver(HEADER_PLACEHOLDER, externalMessage.getHeaders()), + PlaceholderFactory.newPlaceholderResolver(HEADERS_PLACEHOLDER, externalMessage.getHeaders()), PlaceholderFactory.newPlaceholderResolver(THING_PLACEHOLDER, outboundSignal.getSource().getId()), PlaceholderFactory.newPlaceholderResolver(TOPIC_PLACEHOLDER, topicPathOpt.orElse(null)) ); diff --git a/services/connectivity/messaging/src/main/java/org/eclipse/ditto/services/connectivity/messaging/rabbitmq/RabbitMQConsumerActor.java b/services/connectivity/messaging/src/main/java/org/eclipse/ditto/services/connectivity/messaging/rabbitmq/RabbitMQConsumerActor.java index 826798fb6e..0dd63b74e3 100644 --- a/services/connectivity/messaging/src/main/java/org/eclipse/ditto/services/connectivity/messaging/rabbitmq/RabbitMQConsumerActor.java +++ b/services/connectivity/messaging/src/main/java/org/eclipse/ditto/services/connectivity/messaging/rabbitmq/RabbitMQConsumerActor.java @@ -11,6 +11,7 @@ package org.eclipse.ditto.services.connectivity.messaging.rabbitmq; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -56,13 +57,8 @@ public final class RabbitMQConsumerActor extends BaseConsumerActor { private static final String MESSAGE_ID_HEADER = "messageId"; - private static final Set CONTENT_TYPES_INTERPRETED_AS_TEXT; - - static { - final Set contentTypes = new HashSet<>(5); - Collections.addAll(contentTypes, "text/plain", "text/html", "text/yaml", "application/json", "application/xml"); - CONTENT_TYPES_INTERPRETED_AS_TEXT = Collections.unmodifiableSet(contentTypes); - } + private static final Set CONTENT_TYPES_INTERPRETED_AS_TEXT = Collections.unmodifiableSet(new HashSet<>( + Arrays.asList("text/plain", "text/html", "text/yaml", "application/json", "application/xml"))); private final DiagnosticLoggingAdapter log = LogUtil.obtain(this); diff --git a/services/connectivity/messaging/src/main/java/org/eclipse/ditto/services/connectivity/messaging/validation/AbstractProtocolValidator.java b/services/connectivity/messaging/src/main/java/org/eclipse/ditto/services/connectivity/messaging/validation/AbstractProtocolValidator.java index a0e047c49c..1f028c0e1b 100644 --- a/services/connectivity/messaging/src/main/java/org/eclipse/ditto/services/connectivity/messaging/validation/AbstractProtocolValidator.java +++ b/services/connectivity/messaging/src/main/java/org/eclipse/ditto/services/connectivity/messaging/validation/AbstractProtocolValidator.java @@ -177,25 +177,8 @@ private static Supplier targetDescription(final Target target, final Con */ protected void validateTemplate(final String template, final DittoHeaders headers, final Placeholder... placeholders) { - validateTemplate(template, false, headers, placeholders); - } - - /** - * Validates that the passed {@code template} is both valid and depending on the {@code allowUnresolved} boolean - * that the placeholders in the passed {@code template} are completely replaceable by the provided - * {@code placeholders}. - * - * @param template a string potentially containing placeholders to replace - * @param allowUnresolved whether to allow if there could be placeholders in the template left unreplaced - * @param headers the DittoHeaders to use in order for e.g. building DittoRuntimeExceptions - * @param placeholders the {@link Placeholder}s to use for replacement - * @throws ConnectionConfigurationInvalidException in case the template's placeholders could not completely be - * resolved - */ - protected void validateTemplate(final String template, final boolean allowUnresolved, final DittoHeaders headers, - final Placeholder... placeholders) { try { - PlaceholderFilter.validate(template, allowUnresolved, placeholders); + PlaceholderFilter.validate(template, placeholders); } catch (final DittoRuntimeException exception) { throw ConnectionConfigurationInvalidException .newBuilder(MessageFormat.format(ENFORCEMENT_ERROR_MESSAGE, template,