Skip to content

Commit

Permalink
introduce Placeholders class which supports replacement of placeholde…
Browse files Browse the repository at this point in the history
…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
danielFesenmeyer committed Sep 13, 2018
1 parent e9a1eeb commit cc52c04
Show file tree
Hide file tree
Showing 2 changed files with 319 additions and 0 deletions.
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();
}
}
}
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();
}
}

0 comments on commit cc52c04

Please sign in to comment.