Skip to content

Instantly share code, notes, and snippets.

@SnuktheGreat
Last active February 25, 2016 16:41
Show Gist options
  • Select an option

  • Save SnuktheGreat/c8ccb5c18a1a00650a21 to your computer and use it in GitHub Desktop.

Select an option

Save SnuktheGreat/c8ccb5c18a1a00650a21 to your computer and use it in GitHub Desktop.
ParameterizedString - Utility for replacing named markers in a String.
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps;
import static com.google.common.base.Preconditions.checkArgument;
/**
* This class can be used to replace named parameters in a string. Other options include: Set a {@link Function} to avoid using toString
* for some specified classes; Set default values; Get named parameter values from a {@link Matcher}; And Vararg methods to keep your calls
* nice and tiny. See the JUnit test for examples.
*
* Copied from <a href="https://gist.github.com/SnuktheGreat/c8ccb5c18a1a00650a21">GitHub Gist</a>.
*/
public class ParametrizedString {
private static final Function<Object, String> DEFAULT_MAPPER = Object::toString;
private static final int OPEN = '{';
private static final int CLOSED = '}';
private static final int ESCAPE = '\\';
private final Map<Class<?>, Function<Object, String>> classMappers = new HashMap<>();
private final ImmutableList<Placeholder> placeholders;
private final ImmutableMultimap<String, NamedPlaceholder> nameToPlaceHolder;
/**
* Create a new {@link ParametrizedString} from the given format.
*
* @param string The parametrized message format
* @return A new {@link ParametrizedString} from the given format.
*/
public static ParametrizedString format(String string) {
ImmutableList.Builder<Placeholder> placeholders = ImmutableList.builder();
StringBuilder builder = new StringBuilder();
boolean buildingTag = false;
boolean escaped = false;
for (int offset = 0; offset < string.length(); ) {
int codePoint = string.codePointAt(offset);
int codePointLength = Character.charCount(codePoint);
if (escaped) {
builder.appendCodePoint(codePoint);
escaped = false;
} else if (codePoint == ESCAPE) {
escaped = true;
} else if (codePoint == CLOSED && buildingTag) {
placeholders.add(new NamedPlaceholder(builder.toString()));
buildingTag = false;
builder = new StringBuilder();
} else if (codePoint == OPEN && !buildingTag) {
if (builder.length() > 0) {
placeholders.add(new StaticPlaceholder(builder.toString()));
}
buildingTag = true;
builder = new StringBuilder();
} else {
builder.appendCodePoint(codePoint);
}
offset += codePointLength;
}
if (builder.length() > 0) {
placeholders.add(new StaticPlaceholder(builder.toString()));
}
return new ParametrizedString(placeholders.build());
}
private ParametrizedString(ImmutableList<Placeholder> placeholders) {
this.placeholders = placeholders;
this.nameToPlaceHolder = Multimaps.index(
placeholders.stream()
.filter(placeholder -> placeholder instanceof NamedPlaceholder)
.map(placeholder -> (NamedPlaceholder) placeholder)
.collect(Collectors.toList()),
NamedPlaceholder::getName);
}
/**
* @return An immutable set of all parameters used in this {@link ParametrizedString}.
*/
public Set<String> getParameterNames() {
return nameToPlaceHolder.keySet();
}
/**
* Override the {@link Function} used to determine the string value for a class type. Normally {@link Object#toString()} is used to
* determine any argument's string value.
*
* @param type The {@link Class} type to set the function for.
* @param message The function to convert instances of the given type to a string.
* @param <T> The type of the type.
* @return This {@link ParametrizedString} for further chaining.
*/
@SuppressWarnings("unchecked")
public <T> ParametrizedString map(Class<T> type, Function<T, String> message) {
classMappers.put(type, (Function) message);
return this;
}
/**
* Set the default value for the parameter with the given name to the given value.
*
* @param name The name of the parameter.
* @param defaultValue The new default value of this parameter.
* @return This {@link ParametrizedString} for further chaining.
*/
public ParametrizedString setDefaultValue(String name, String defaultValue) {
nameToPlaceHolder.get(name).stream()
.forEach(placeholder -> placeholder.defaultValue = defaultValue);
return this;
}
/**
* Set the default values for the parameters with the names in the given map to the corresponding values in that same map.
*
* @param defaultValues The map containing default parameter name -> value combinations.
* @return This {@link ParametrizedString} for further chaining.
*/
public ParametrizedString setDefaultValues(Map<String, String> defaultValues) {
defaultValues.entrySet().forEach(entry -> setDefaultValue(entry.getKey(), entry.getValue()));
return this;
}
/**
* Set the default values for the parameters with the names in the given vararg to the corresponding values in that same vararg. The
* given vararg is parsed as a map with each even numbered element used as the key for each following uneven numbered element. Note
* that the given vararg must be of even length.
*
* @param defaultValues The vararg containing default parameter name -> value combinations.
* @return This {@link ParametrizedString} for further chaining.
*/
public ParametrizedString setDefaultValues(String... defaultValues) {
return setDefaultValues(toMap(defaultValues));
}
/**
* Return the formatted string where each {@link #getParameterNames() known parameter name} is replaced with the corresponding value
* from the map. If the value has a configured mapper (set with {@link #map(Class, Function)}), that will be used to convert the
* instance object type to a string value. Otherwise {@link Object#toString()} is used by default. Any expected named parameters that
* are missing from the map will be set to their default value (set with {@link #setDefaultValue(String, String)} or a blank string by
* default).
*
* @param args The map containing parameter name -> value combinations.
* @return The formatted string where all named parameters have either been replaced from the given map, or set from a default value.
*/
public String apply(Map<String, Object> args) {
Map<String, String> stringValues = Maps.transformEntries(args,
(key, value) -> findMapper(value.getClass()).apply(value));
return placeholders.stream()
.map(placeholder -> placeholder.fill(stringValues))
.collect(Collectors.joining());
}
/**
* Like {@link #apply(Map)}, but with the map created from the given vararg. The given vararg is parsed as a map with each even
* numbered element used as the key for each following uneven numbered element. Note that the given vararg must be of even length and
* all key elements should be strings.
*
* @param args The vararg containing parameter name -> value combinations.
* @return The formatted string where all named parameters have either been replaced from the given vararg, or set from a default value.
*/
public String apply(Object... args) {
return apply(toMap(args));
}
/**
* Return the formatted string where each {@link #getParameterNames() known parameter name} gets a corresponding value from the equally
* {@link Matcher#group(String) named group} in the given {@link Matcher}. Note that this method will throw an exception if any of the
* parameter names is not found as a group on the {@link Matcher}.
*
* @param matcher The matched to get the named group values from.
* @return The formatted string where all named parameters have been replaced from groups within the given {@link Matcher}.
*/
public String apply(Matcher matcher) {
return apply(nameToPlaceHolder.keySet().stream()
.collect(Collectors.toMap(name -> name, matcher::group)));
}
private Map<String, Object> toMap(Object[] args) {
checkArgument(args.length % 2 == 0, "Expecting an even number of arguments, was %d.", args.length);
ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
for (int index = 0; index < args.length; index += 2) {
Object key = args[index];
Object value = args[index + 1];
if (key instanceof String) {
builder.put((String) key, value);
} else {
throw new IllegalArgumentException(String.format("Key argument %d (%s) is not of type String.", index, key));
}
}
return builder.build();
}
private Map<String, String> toMap(String[] args) {
checkArgument(args.length % 2 == 0, "Expecting an even number of arguments, was %d.", args.length);
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
for (int index = 0; index < args.length; index += 2) {
builder.put(args[index], args[index + 1]);
}
return builder.build();
}
private Function<Object, String> findMapper(Class<?> valueCls) {
Function<Object, String> mapper = null;
Class<?> cls = valueCls;
while (cls != Object.class && mapper == null) {
mapper = classMappers.get(cls);
cls = cls.getSuperclass();
}
return Optional.ofNullable(mapper)
.orElse(DEFAULT_MAPPER);
}
private interface Placeholder {
String fill(Map<String, String> namedParameters);
}
private static class StaticPlaceholder implements Placeholder {
private final String value;
private StaticPlaceholder(String value) {
this.value = value;
}
@Override
public String fill(Map<String, String> namedParameters) {
return value;
}
}
private static class NamedPlaceholder implements Placeholder {
private final String name;
private String defaultValue = "";
private NamedPlaceholder(String name) {
this.name = name;
}
private String getName() {
return name;
}
@Override
public String fill(Map<String, String> namedParameters) {
return namedParameters.getOrDefault(name, defaultValue);
}
}
}
import org.junit.Test;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
public class ParametrizedStringTest {
@Test
public void testFormat() throws Exception {
ParametrizedString format = ParametrizedString.format("There were {nrOfErrors} errors on {service}.");
assertThat(format.getParameterNames().size(), is(2));
assertThat(format.getParameterNames(), hasItems("nrOfErrors", "service"));
assertThat(
format.apply(
"nrOfErrors", 12,
"service", "mail-server"),
equalTo("There were 12 errors on mail-server."));
assertThat(format.apply(), equalTo("There were errors on ."));
assertThat(format.apply("not_used", 1337), equalTo("There were errors on ."));
}
@Test
public void testFormat_escape() throws Exception {
ParametrizedString format = ParametrizedString.format("There were \\{nrOfErrors} errors on {service}.");
assertThat(format.getParameterNames().size(), is(1));
assertThat(format.getParameterNames(), hasItem("service"));
assertThat(
format.apply(
"nrOfErrors", 12,
"service", "mail-server"),
equalTo("There were {nrOfErrors} errors on mail-server."));
}
@Test
public void testFormat_doubleEscape() throws Exception {
ParametrizedString format = ParametrizedString.format("There were \\\\{nrOfErrors} errors on {service}.");
assertThat(format.getParameterNames().size(), is(2));
assertThat(format.getParameterNames(), hasItems("nrOfErrors", "service"));
assertThat(
format.apply(
"nrOfErrors", 12,
"service", "mail-server"),
equalTo("There were \\12 errors on mail-server."));
}
@Test
public void testFormat_twoTagsCloseToOneAnother() throws Exception {
ParametrizedString format = ParametrizedString.format("There were {nrOfErrors} errors on {prefix}{service}");
assertThat(format.getParameterNames().size(), is(3));
assertThat(format.getParameterNames(), hasItems("nrOfErrors", "prefix", "service"));
assertThat(
format.apply(
"nrOfErrors", 12,
"prefix", "nl-",
"service", "mail-server"),
equalTo("There were 12 errors on nl-mail-server"));
}
@Test
public void testFormat_doubleOpen() throws Exception {
ParametrizedString format = ParametrizedString.format("There were {{nrOfErrors} errors on {service}.");
assertThat(format.getParameterNames().size(), is(2));
assertThat(format.getParameterNames(), hasItems("{nrOfErrors", "service"));
assertThat(
format.apply(
"{nrOfErrors", 12,
"service", "mail-server"),
equalTo("There were 12 errors on mail-server."));
}
@Test
public void testClassMapper() throws Exception {
ParametrizedString format = ParametrizedString
.format("There were {nrOfErrors} errors on {service}.")
.map(Service.class, Service::createFullName);
assertThat(
format.apply(
"nrOfErrors", 12,
"service", new Service("mail-server", "nl")),
equalTo("There were 12 errors on nl-mail-server."));
}
@Test
public void testDefaultValues() throws Exception {
ParametrizedString format = ParametrizedString
.format("There were {nrOfErrors} errors on {service}.")
.setDefaultValues(
"nrOfErrors", "0",
"service", "<unknown>");
assertThat(
format.apply(),
equalTo("There were 0 errors on <unknown>."));
assertThat(
format.apply(
"nrOfErrors", 12,
"service", "mail-server"),
equalTo("There were 12 errors on mail-server."));
}
private static class Service {
private final String name;
private final String location;
private Service(String name, String location) {
this.name = name;
this.location = location;
}
private String getName() {
return name;
}
private String createFullName() {
return location + '-' + name;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment