sirius.kernel.nls.Formatter Maven / Gradle / Ivy
/*
* Made with all the love in the world
* by scireum in Remshalden, Germany
*
* Copyright by scireum GmbH
* http://www.scireum.de - [email protected]
*/
package sirius.kernel.nls;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import sirius.kernel.commons.Strings;
import java.util.List;
import java.util.Map;
/**
* An alternative for MessageFormat which generates strings by replacing named parameters in a given template.
*
* A formatter is created for a given template string which contains named parameters like {@code ${param1}}.
* Using one of the set methods, values for the parameters can be supplied. Calling {@code #format}
* creates the output string.
*
* Non string objects which are passed in as parameters, will be converted using {@link NLS#toUserString(Object)}
*
* A formatter is neither thread safe nor intended for reuse. Instead a formatter is created, supplied with the
* relevant parameters by chaining calls to set and then discarded after getting the result string via
* format.
*
* An example call might look like this:
*
* {@code
* System.out.println(
* Formatter.create("Hello ${programmer}")
* .set("programmer, "Obi Wan")
* .format());
* }
*
*
* {@link NLS} uses this class by supplied translated patterns when calling {@link NLS#fmtr(String)}.
*
* @see NLS#fmtr(String)
*/
public class Formatter {
private boolean urlEncode = false;
private boolean smartFormat = false;
private Map replacement = Maps.newTreeMap();
private String pattern;
private String lang;
/**
* Use the static factory methods create to obtain a new instance.
*/
protected Formatter() {
super();
}
/**
* Creates a new formatter with the given pattern and language.
*
* The given language will be used when converting non-string parameters.
*
* @param pattern specifies the pattern to be used for creating the output
* @param lang specifies the language used when converting non-string parameters.
* @return this for fluently calling set methods.
*/
public static Formatter create(String pattern, String lang) {
Formatter result = new Formatter();
result.pattern = pattern;
result.lang = lang;
return result;
}
/**
* Creates a new formatter with the given pattern.
*
* Uses the currently active language when converting non-string parameters.
*
* @param pattern specifies the pattern to be used for creating the output
* @return this for fluently calling set methods.
*/
public static Formatter create(String pattern) {
Formatter result = new Formatter();
result.pattern = pattern;
result.lang = NLS.getCurrentLang();
return result;
}
/**
* Creates a new formatter with auto url encoding turned on.
*
* Any parameters passed to this formatter will be automatically url encoded.
*
* @param pattern specifies the pattern to be used for creating the output
* @return this for fluently calling set methods.
*/
public static Formatter createURLFormatter(String pattern) {
Formatter result = new Formatter();
result.pattern = pattern;
result.urlEncode = true;
return result;
}
/**
* Adds the replacement value to use for the given property.
*
* @param property the parameter in the template string which should be replaced
* @param value the value which should be used as replacement
* @return this to permit fluent method chains
*/
public Formatter set(String property, Object value) {
setDirect(property, NLS.toUserString(value, lang), urlEncode);
return this;
}
/**
* Adds the replacement value to use for the given property, without url encoding the value.
*
* Formatters created by #createURLFormatter perform automatic url conversion for all parameters.
* Using this method however, disables url encoding for the given parameter and value.
*
* @param property the parameter in the template string which should be replaced
* @param value the value which should be used as replacement
* @return this to permit fluent method chains
*/
public Formatter setUnencoded(String property, Object value) {
return setDirect(property, NLS.toUserString(value, lang), false);
}
/**
* Sets the whole context as parameters in this formatter.
*
* Calls #set for each entry in the given map.
*
* @param ctx a Map which provides a set of entries to replace.
* @return this to permit fluent method chains
*/
public Formatter set(Map ctx) {
if (ctx != null) {
for (Map.Entry e : ctx.entrySet()) {
set(e.getKey(), e.getValue());
}
}
return this;
}
/**
* Directly sets the given string value for the given property.
*
* Sets the given string as replacement value for the named parameter. The value will not be sent through
* {@link NLS#toUserString(Object)} and therefore not trimmed etc.
*
* @param property the parameter in the template string which should be replaced
* @param value the value which should be used as replacement
* @param urlEncode determines if url encoding should be applied. If the parameter is set to false,
* this method won't perform any url encoding, even if the formatter was created
* using #createURLFormatter
* @return this to permit fluent method chains
*/
public Formatter setDirect(String property, String value, boolean urlEncode) {
replacement.put(property, urlEncode ? Strings.urlEncode(value) : value);
return this;
}
/**
* Generates the formatted string.
*
* Applies all supplied replacement values on detected parameters formatted like {@code ${param}}.
*
* @return the template string with all parameters replaced for which a value was supplied.
* @throws java.lang.IllegalArgumentException if the pattern is malformed
*/
public String format() {
return format(false);
}
/**
* Generates the formatted string using smart output formatting.
*
* Applies all supplied replacement values on detected parameters formatted like {@code ${param}}.
* Block can be formed using '[' and ']' a whole block is only output, if at least one replacement
* was not empty.
*
* Consider the pattern {@code [${salutation} ][${firstname}] ${lastname}}. This will create
* Mr. Foo Bar if all three parameters are filled, but Mr. Bar if the first name is missing
* or Foo Bar if the salutation is missing.
*
* @return the template string with all parameters replaced for which a value was supplied.
* @throws java.lang.IllegalArgumentException if the pattern is malformed
*/
public String smartFormat() {
return format(true);
}
/*
* Keeps track of the current smart formatting block being parsed.
*/
private static class Block {
StringBuilder output = new StringBuilder();
boolean replacementFound = false;
int startIndex;
}
/*
* Stack based implementation parsing parameterized strings with smart blocks. Each nested block will
* result in one stack level.
*/
private String format(boolean smart) {
List blocks = Lists.newArrayList();
Block currentBlock = new Block();
blocks.add(currentBlock);
int index = 0;
while (index < pattern.length()) {
char current = pattern.charAt(index);
if (current == '$' && pattern.charAt(index + 1) == '{') {
index = performParameterReplacement(currentBlock, index);
} else if (current == '[' && smart) {
currentBlock = startBlock(blocks, index);
} else if (current == ']' && smart) {
currentBlock = endBlock(blocks, currentBlock, index);
} else {
currentBlock.output.append(current);
}
index++;
}
if (blocks.size() > 1) {
throw new IllegalArgumentException(Strings.apply(
"Unexpected end of pattern. Expected ']' for '[' at index %d in '%s'",
currentBlock.startIndex + 1,
pattern));
} else {
return currentBlock.output.toString();
}
}
private int performParameterReplacement(Block currentBlock, int index) {
index += 2;
int keyStart = index;
while (index < pattern.length() && pattern.charAt(index) != '}') {
index++;
}
if (index >= pattern.length()) {
throw new IllegalArgumentException(Strings.apply("Missing } for ${ started at index %d in '%s'",
keyStart - 1,
pattern));
}
String key = pattern.substring(keyStart, index);
String value = replacement.computeIfAbsent(key, s -> {
throw new IllegalArgumentException(Strings.apply("Unknown value '%s' used at index %d in '%s'",
key,
keyStart - 1,
pattern));
});
if (Strings.isFilled(value)) {
currentBlock.output.append(value);
currentBlock.replacementFound = true;
}
return index;
}
private Block startBlock(List blocks, int index) {
Block currentBlock;
currentBlock = new Block();
currentBlock.startIndex = index;
blocks.add(currentBlock);
return currentBlock;
}
private Block endBlock(List blocks, Block currentBlock, int index) {
if (blocks.size() == 1) {
throw new IllegalArgumentException(Strings.apply("Unexpected ']' at index %d in '%s'", index + 1, pattern));
}
if (currentBlock.replacementFound) {
Block next = blocks.get(blocks.size() - 2);
next.output.append(currentBlock.output);
next.replacementFound = true;
}
currentBlock = blocks.get(blocks.size() - 2);
blocks.remove(blocks.size() - 1);
return currentBlock;
}
@Override
public String toString() {
return format();
}
}