Last active
February 25, 2016 16:41
-
-
Save SnuktheGreat/c8ccb5c18a1a00650a21 to your computer and use it in GitHub Desktop.
ParameterizedString - Utility for replacing named markers in a String.
This file contains hidden or 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
| 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); | |
| } | |
| } | |
| } |
This file contains hidden or 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
| 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