-
Notifications
You must be signed in to change notification settings - Fork 219
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
introduce Placeholders class which supports replacement of placeholde…
…rs in the new "{{ prefix:name }}" and the legacy format "${prefix.name}" Signed-off-by: Daniel Fesenmeyer <daniel.fesenmeyer@bosch-si.com>
- Loading branch information
1 parent
e9a1eeb
commit cc52c04
Showing
2 changed files
with
319 additions
and
0 deletions.
There are no files selected for viewing
126 changes: 126 additions & 0 deletions
126
model/base/src/main/java/org/eclipse/ditto/model/base/common/Placeholders.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
/* | ||
* Copyright (c) 2017 Bosch Software Innovations GmbH. | ||
* | ||
* All rights reserved. This program and the accompanying materials | ||
* are made available under the terms of the Eclipse Public License v2.0 | ||
* which accompanies this distribution, and is available at | ||
* https://www.eclipse.org/org/documents/epl-2.0/index.php | ||
* | ||
* Contributors: | ||
* Bosch Software Innovations GmbH - initial contribution | ||
*/ | ||
package org.eclipse.ditto.model.base.common; | ||
|
||
import static java.util.Objects.requireNonNull; | ||
|
||
import java.util.function.Function; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
|
||
import javax.annotation.concurrent.Immutable; | ||
|
||
/** | ||
* Supports substitution of placeholders in the format {@code {{ prefix:key }}} | ||
* or the legacy-format {@code ${prefix.key}}. | ||
* | ||
*/ | ||
@Immutable | ||
public final class Placeholders { | ||
|
||
private static final String PLACEHOLDER_GROUP_NAME = "ph"; | ||
|
||
private static final String PLACEHOLDER_START = "{{"; | ||
private static final String PLACEHOLDER_END = "}}"; | ||
|
||
private static final String PLACEHOLDER_REGEX = | ||
Pattern.quote(PLACEHOLDER_START) + " (?<" + PLACEHOLDER_GROUP_NAME + ">\\S+) " + | ||
Pattern.quote(PLACEHOLDER_END); | ||
|
||
|
||
private static final String LEGACY_PLACEHOLDER_START = "${"; | ||
private static final String LEGACY_PLACEHOLDER_END = "}"; | ||
private static final String LEGACY_PLACEHOLDER_REGEX = | ||
Pattern.quote(LEGACY_PLACEHOLDER_START) + "(?<" + PLACEHOLDER_GROUP_NAME + ">\\S+)" + | ||
Pattern.quote(LEGACY_PLACEHOLDER_END); | ||
|
||
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile(PLACEHOLDER_REGEX); | ||
private static final Pattern LEGACY_PLACEHOLDER_PATTERN = Pattern.compile(LEGACY_PLACEHOLDER_REGEX); | ||
|
||
private static final Function<String, String> TO_LEGACY_PLACEHOLDER_CONVERTER = | ||
inputPlaceholder -> inputPlaceholder.replaceAll(Pattern.quote("."), ":"); | ||
|
||
private Placeholders() { | ||
throw new AssertionError(); | ||
} | ||
|
||
/** | ||
* Checks whether the given {@code input} contains any placeholder. | ||
* @param input the input. | ||
* @return {@code} true, if the input contains a placeholder. | ||
*/ | ||
public static boolean containsAnyPlaceholder(final CharSequence input) { | ||
requireNonNull(input); | ||
return PLACEHOLDER_PATTERN.matcher(input).find() || | ||
LEGACY_PLACEHOLDER_PATTERN.matcher(input).find(); | ||
} | ||
|
||
/** | ||
* Substitutes any placeholder contained in the input. | ||
* | ||
* @param input the input. | ||
* @param placeholderReplacerFunction a function defining how a placeholder will be replaced. It must not return | ||
* null, instead it should throw a specific exception if a placeholder cannot be replaced. | ||
* @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 | ||
*/ | ||
public static String substitute(final String input, final Function<String, String> placeholderReplacerFunction) { | ||
final Function<String, String> legacyPlaceholderReplacerFunction = | ||
TO_LEGACY_PLACEHOLDER_CONVERTER.andThen(placeholderReplacerFunction); | ||
|
||
String maybeSubstituted = | ||
substitute(input, PLACEHOLDER_PATTERN, placeholderReplacerFunction); | ||
maybeSubstituted = | ||
substitute(maybeSubstituted, LEGACY_PLACEHOLDER_PATTERN, legacyPlaceholderReplacerFunction); | ||
|
||
return maybeSubstituted; | ||
} | ||
|
||
private static String substitute(final String input, final Pattern pattern, | ||
final Function<String, String> replacerFunction) { | ||
|
||
final Matcher matcher = pattern.matcher(input); | ||
StringBuilder substituted = null; | ||
int previousMatchEnd = 0; | ||
while (matcher.find()) { | ||
if (substituted == null) { | ||
substituted = new StringBuilder(input.length()); | ||
} | ||
final int start = matcher.start(); | ||
final int end = matcher.end(); | ||
|
||
final String previousText = input.substring(previousMatchEnd, start); | ||
substituted.append(previousText); | ||
|
||
final String placeholder = matcher.group(PLACEHOLDER_GROUP_NAME); | ||
final String replacement = replacerFunction.apply(placeholder); | ||
if (replacement == null) { | ||
throw new IllegalStateException("Null values must not be returned by replacerFunction"); | ||
} | ||
substituted.append(replacement); | ||
|
||
previousMatchEnd = end; | ||
} | ||
|
||
if (substituted != null && previousMatchEnd < input.length()) { | ||
final String remainingText = input.substring(previousMatchEnd); | ||
substituted.append(remainingText); | ||
} | ||
|
||
if (substituted == null) { | ||
return input; | ||
} else { | ||
return substituted.toString(); | ||
} | ||
} | ||
} |
193 changes: 193 additions & 0 deletions
193
model/base/src/test/java/org/eclipse/ditto/model/base/common/PlaceholdersTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
/* | ||
* Copyright (c) 2017 Bosch Software Innovations GmbH. | ||
* | ||
* All rights reserved. This program and the accompanying materials | ||
* are made available under the terms of the Eclipse Public License v2.0 | ||
* which accompanies this distribution, and is available at | ||
* https://www.eclipse.org/org/documents/epl-2.0/index.php | ||
* | ||
* Contributors: | ||
* Bosch Software Innovations GmbH - initial contribution | ||
*/ | ||
package org.eclipse.ditto.model.base.common; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; | ||
import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf; | ||
import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; | ||
|
||
import java.util.LinkedHashMap; | ||
import java.util.Map; | ||
import java.util.function.Function; | ||
|
||
import org.junit.Before; | ||
import org.junit.Test; | ||
|
||
/** | ||
* Tests {@link Placeholders}. | ||
*/ | ||
public class PlaceholdersTest { | ||
|
||
private static final String REPLACER_KEY_1 = "my:arbitrary:replacer1"; | ||
private static final String REPLACER_1 = "{{ " + REPLACER_KEY_1 + " }}"; | ||
private static final String LEGACY_REPLACER_1 = "${my.arbitrary.replacer1}"; | ||
private static final String REPLACED_1 = "firstReplaced"; | ||
private static final String REPLACER_KEY_2 = "my:arbitrary:replacer2"; | ||
private static final String REPLACER_2 = "{{ " + REPLACER_KEY_2 + " }}"; | ||
private static final String LEGACY_REPLACER_2 = "${my.arbitrary.replacer2}"; | ||
private static final String REPLACED_2 = "secondReplaced"; | ||
|
||
private static final String UNKNOWN_REPLACER_KEY = "unknown:unknown"; | ||
private static final String UNKNOWN_REPLACER = "{{ " + UNKNOWN_REPLACER_KEY + " }}"; | ||
private static final String UNKNOWN_LEGACY_REPLACER_KEY = "unknown.unknown"; | ||
private static final String UNKNOWN_LEGACY_REPLACER = "${" + UNKNOWN_LEGACY_REPLACER_KEY + "}"; | ||
|
||
private Function<String, String> replacerFunction; | ||
|
||
@Before | ||
public void init() { | ||
final Map<String, String> replacementDefinitions = new LinkedHashMap<>(); | ||
replacementDefinitions.put(REPLACER_KEY_1, REPLACED_1); | ||
replacementDefinitions.put(REPLACER_KEY_2, REPLACED_2); | ||
replacerFunction = replacementDefinitions::get; | ||
} | ||
|
||
@Test | ||
public void assertImmutability() { | ||
assertInstancesOf(Placeholders.class, areImmutable()); | ||
} | ||
|
||
@Test | ||
public void substituteFailsWithNullInput() { | ||
assertThatExceptionOfType(NullPointerException.class) | ||
.isThrownBy(() -> Placeholders.substitute(null, replacerFunction)); | ||
} | ||
|
||
@Test | ||
public void substituteFailsWithNullReplacerFunction() { | ||
assertThatExceptionOfType(NullPointerException.class) | ||
.isThrownBy(() -> Placeholders.substitute("doesNotMatter", null)); | ||
} | ||
|
||
@Test | ||
public void substituteReturnsInputWhenInputDoesNotContainReplacers() { | ||
final String input = "withoutReplacers"; | ||
|
||
final String substituted = Placeholders.substitute(input, replacerFunction); | ||
|
||
assertThat(substituted).isSameAs(input); | ||
} | ||
|
||
@Test | ||
public void substituteReturnsReplacedWhenInputContainsOnlyPlaceholder() { | ||
final String substituted = Placeholders.substitute(REPLACER_1, replacerFunction); | ||
|
||
assertThat(substituted).isEqualTo(REPLACED_1); | ||
} | ||
|
||
@Test | ||
public void substituteReturnsReplacedInputWhenInputContainsSurroundedPlaceholder() { | ||
final String substituted = | ||
Placeholders.substitute("a" + REPLACER_1 + "z", replacerFunction); | ||
|
||
assertThat(substituted).isEqualTo("a" + REPLACED_1 + "z"); | ||
} | ||
|
||
@Test | ||
public void substituteReturnsReplacedInputWhenInputContainsMultiplePlaceholders() { | ||
final String input = "a" + REPLACER_1 + "b" + REPLACER_2 + "c"; | ||
|
||
final String substituted = Placeholders.substitute(input, replacerFunction); | ||
|
||
final String expectedOutput = "a" + REPLACED_1 + "b" + REPLACED_2 + "c"; | ||
assertThat(substituted).isEqualTo(expectedOutput); | ||
} | ||
|
||
@Test | ||
public void substituteReturnsReplacedWhenInputContainsOnlyLegacyPlaceholder() { | ||
final String substituted = | ||
Placeholders.substitute(LEGACY_REPLACER_1, replacerFunction); | ||
|
||
assertThat(substituted).isEqualTo(REPLACED_1); | ||
} | ||
|
||
@Test | ||
public void substituteReturnsReplacedInputWhenInputContainsBothNewAndLegacyPlaceholders() { | ||
final String input = "a" + REPLACER_1 + "b" + LEGACY_REPLACER_2 + "c"; | ||
|
||
final String substituted = Placeholders.substitute(input, replacerFunction); | ||
|
||
final String expectedOutput = "a" + REPLACED_1 + "b" + REPLACED_2 + "c"; | ||
assertThat(substituted).isEqualTo(expectedOutput); | ||
} | ||
|
||
@Test | ||
public void substituteThrowsWhenReplacerFunctionReturnsNullForPlaceholder() { | ||
assertThatExceptionOfType(IllegalStateException.class) | ||
.isThrownBy(() -> Placeholders.substitute(UNKNOWN_REPLACER, (placeholder) -> null)); | ||
} | ||
|
||
@Test | ||
public void substituteThrowsWhenReplacerFunctionReturnsNullForLegacyPlaceholder() { | ||
assertThatExceptionOfType(IllegalStateException.class) | ||
.isThrownBy(() -> Placeholders.substitute(UNKNOWN_LEGACY_REPLACER, (placeholder) -> null)); | ||
} | ||
|
||
@Test | ||
public void substituteReturnsReplacedInputWhenInputContainsNestedPlaceholder() { | ||
final String nestedPlaceholder = "{{ " + REPLACER_1 + " }}"; | ||
|
||
final String substituted = Placeholders.substitute(nestedPlaceholder, replacerFunction); | ||
|
||
final String expectedOutput = "{{ " + REPLACED_1 + " }}"; | ||
assertThat(substituted).isEqualTo(expectedOutput); | ||
} | ||
|
||
/** | ||
* Nesting legacy-placeholders does not work. In our case, null will be returned by the replacerFunction. | ||
* Normally, replacerFunctions should throw a DittoRuntimeException in this case. | ||
*/ | ||
@Test | ||
public void substituteThrowsWhenInputContainsNestedLegacyPlaceholder() { | ||
final String nestedPlaceholder = "${" + LEGACY_REPLACER_1 + "}"; | ||
|
||
assertThatExceptionOfType(IllegalStateException.class) | ||
.isThrownBy(() -> Placeholders.substitute(nestedPlaceholder, (placeholder) -> null)); | ||
} | ||
|
||
@Test | ||
public void containsReturnsTrueWhenInputContainsPlaceholder() { | ||
final String input = "a" + REPLACER_1 + "z"; | ||
|
||
final boolean contains = Placeholders.containsAnyPlaceholder(input); | ||
|
||
assertThat(contains).isTrue(); | ||
} | ||
|
||
@Test | ||
public void containsReturnsTrueWhenInputContainsLegacyPlaceholder() { | ||
final String input = "a" + LEGACY_REPLACER_1 + "z"; | ||
|
||
final boolean contains = Placeholders.containsAnyPlaceholder(input); | ||
|
||
assertThat(contains).isTrue(); | ||
} | ||
|
||
@Test | ||
public void containsReturnsTrueWhenInputContainsBothNewAndLegacyPlaceholders() { | ||
final String input = "a" + REPLACER_1 + "b" + LEGACY_REPLACER_2 + "c"; | ||
|
||
final boolean contains = Placeholders.containsAnyPlaceholder(input); | ||
|
||
assertThat(contains).isTrue(); | ||
} | ||
|
||
@Test | ||
public void containsReturnsFailsWhenInputDoesNotContainAnyPlaceholder() { | ||
final String input = "abc"; | ||
|
||
final boolean contains = Placeholders.containsAnyPlaceholder(input); | ||
|
||
assertThat(contains).isFalse(); | ||
} | ||
} |