org.klojang.templates.StringifierRegistry Maven / Gradle / Ivy
Show all versions of klojang-templates Show documentation
package org.klojang.templates;
import org.klojang.check.Check;
import org.klojang.collections.TypeMap;
import org.klojang.templates.x.StandardStringifiers;
import org.klojang.util.Tuple2;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.klojang.check.CommonChecks.*;
import static org.klojang.check.Tag.*;
import static org.klojang.templates.TemplateUtils.getNestedTemplate;
import static org.klojang.templates.x.MTag.STRINGIFIER;
import static org.klojang.templates.x.MTag.TEMPLATE;
import static org.klojang.templates.x.Messages.ERR_NO_SUCH_VARIABLE;
import static org.klojang.util.StringMethods.*;
/**
* A registry of {@linkplain Stringifier stringifiers} used by the
* {@link RenderSession} to stringify the values coming back from the data access
* layer. Stringifiers can be used, for example, to apply non-standard formatting to
* dates and numbers, or to apply some sort of escaping (e.g.
* {@linkplain VarGroup#HTML HTML escaping}), or to stringify objects whose
* {@code toString()} method does not satisfy your needs. If you need to configure
* stringifiers, your code might look something like this:
*
* {@code
* StringifierRegistry stringifiers = StringifierRegistry.configure()
* .forType(int.class, obj -> String.valueOf((int) obj + 10))
* .freeze();
* Template template = Template.fromString("~%foo%");
* RenderSession session = template.newRenderSession(stringifiers);
* String out = session.set("foo", 32).render(); // 42
* }
*
* In practice, you are more likely to create just a single
* {@code StringifierRegistry} instance for your entire application, when it starts
* up, and pass that instance to all calls to
* {@link Template#newRenderSession(StringifierRegistry)
* Template.newRenderSession()}.
*
*
*
This is how a {@link StringifierRegistry} decides which stringifier to use for
* a template variable:
*
*
* - If a stringifier has been registered for a
* {@linkplain VarGroup variable group} and the variable belongs to that group,
* then that is the stringifier that is going to be used.
*
- If a stringifier has been registered for that particular variable in that
* particular template, then that is the stringifier that is going to be used.
*
- If a stringifier has been registered for all variables with that particular
* name (irrespective of the template they belong to), then that is the
* stringifier that is going to be used. See
* {@link Builder#forName(String, Stringifier)}
*
- If a stringifier has been registered for the data type of that particular
* variable, then that is the stringifier that is going to be used.
*
- If you have
* {@linkplain Builder#setDefaultStringifier(Stringifier) registered} an
* alternative default stringifier, then that is the stringifier that is going
* to be used.
*
- Otherwise {@link Stringifier#DEFAULT Stringifier.DEFAULT} is going to be
* used.
*
*
* @author Ayco Holleman
*/
public final class StringifierRegistry {
/**
* A minimal {@code StringifierRegistry} instance. It contains stringifiers for the
* predefined {@link VarGroup variable groups}. Variables not within these groups
* are stringified using the {@linkplain Stringifier#DEFAULT default stringifier}.
* This is the {@code StringifierRegistry} a {@link RenderSession} will use if you
* called {@link Template#newRenderSession() Template.newRenderSession()} without
* the {@code StringifierRegistry} argument.
*/
public static final StringifierRegistry STANDARD_STRINGIFIERS = configure().freeze();
/* ++++++++++++++++++++[ BEGIN BUILDER CLASS ]+++++++++++++++++ */
/**
* A builder class for {@link StringifierRegistry} instances.
*
* @author Ayco Holleman
*/
public static class Builder {
private static final String ERR_VAR_ASSIGNED = "Stringifier already set for variable \"${arg}\"";
private static final String ERR_GROUP_ASSIGNED = "Stringifier already set for group \"${arg}\"";
private static final String ERR_TYPE_ASSIGNED = "Stringifier already set for \"${arg}\"";
private static final String ERR_TYPE_SET = "Data type already set for variable \"${arg}\"";
private Stringifier defStringifier = Stringifier.DEFAULT;
private final Map stringifiers = new HashMap<>();
private final Map, Stringifier> typeStringifiers = new HashMap<>();
private final Map, Class>> typeLookup = new HashMap<>();
private final List> partialNames = new ArrayList<>();
private Builder(boolean std) {
if (std) {
StandardStringifiers.get().forEach(
(k, v) -> stringifiers.put(new StringifierId(k), v));
}
}
/**
* Lets you specify an alternative default stringifier, replacing
* {@link Stringifier#DEFAULT}. For example, you might want the default
* stringifier to be {@link #ESCAPE_HTML}.
*
* @param stringifier the stringifier to use as the default stringifier
* @return this {@code Builder}
*/
public Builder setDefaultStringifier(Stringifier stringifier) {
this.defStringifier = Check.notNull(stringifier).ok();
return this;
}
/**
* Assigns the specified stringifier to one or more variables in the specified
* template. The variable names are taken to be fully-qualified names, relative
* to the specified template. For example:
*
* {@code
* Template template = Template.fromResource(getClass(), "/html/company.html");
* StringifierRegistry stringifiers = StringifierRegistry.configure()
* .register(template,
* new ZipCodeFormatter(),
* "zipCode"
* "departments.employees.address.zipCode",
* "departments.manager.address.zipCode")
* .freeze();
* }
*
* To assign the stringifier to all variables in the specified
* template (non-recursively), specify an empty string array.
*
* @param template the template containing the variables
* @param stringifier the stringifier
* @param varNames any array of fully-qualified variable names
* @return this {@code Builder}
* @see TemplateUtils#getFQN(Template, String)
* @see TemplateUtils#getContainingTemplate(Template, String)
*/
public Builder register(
Template template, Stringifier stringifier,
String... varNames) {
Check.notNull(stringifier, STRINGIFIER);
Check.notNull(template, TEMPLATE);
Check.notNull(varNames, VARARGS);
boolean all = varNames.length == 0;
if (all) {
for (String name : template.getVariables()) {
Check.that(new StringifierId(template, name))
.isNot(keyIn(), stringifiers, ERR_VAR_ASSIGNED, name)
.then(id -> stringifiers.put(id, stringifier));
}
} else {
for (String name : varNames) {
Template tmpl = TemplateUtils.getContainingTemplate(template, name);
Check.that(name).is(in(), tmpl.getVariables(), ERR_NO_SUCH_VARIABLE);
Check.that(new StringifierId(template, name))
.isNot(keyIn(), stringifiers, ERR_VAR_ASSIGNED, name)
.then(id -> stringifiers.put(id, stringifier));
}
}
return this;
}
/**
* Assigns the specified stringifier to one or more variables in a nested
* template. {@code nestedTemplateName} must be the fully-qualified name of the
* nested template, relative to the root template. The variable names must be
* simple names. For example:
*
*
{@code
* Template template = Template.fromResource(getClass(), "/html/company.html");
* NameFormatter nameFormatter = new NameFormatter();
* StringifierRegistry stringifiers = StringifierRegistry.configure()
* .forTemplate(template,
* "departments.employees",
* nameFormatter,
* "firstName",
* "lastName")
* .forTemplate(
* template,
* "departments.manager",
* nameFormatter,
* "firstName",
* "lastName")
* .freeze();
* }
*
* To assign the stringifier to all variables in the nested template,
* specify an empty string array.
*
* @param root the root template
* @param nestedTemplateName the name of a template descending from the root
* template, or {@code null} if you want to target the variables in the root
* template itself
* @param stringifier the stringifier
* @param varNames the names of the variables to which to assign the
* stringifier, or an empty string array if you want to assign the
* stringifier to all variables within the target template
* @return this {@code Builder}
* @see TemplateUtils#getNestedTemplate(Template, String)
*/
public Builder forTemplate(
Template root,
String nestedTemplateName,
Stringifier stringifier,
String... varNames) {
Check.notNull(root, TEMPLATE);
Check.notNull(stringifier, STRINGIFIER);
Check.that(varNames, VARARGS).is(deepNotNull());
Template tmpl = getNestedTemplate(root, nestedTemplateName);
boolean all = varNames.length == 0;
if (all) {
for (String name : tmpl.getVariables()) {
Check.that(new StringifierId(tmpl, name))
.isNot(keyIn(), stringifiers, ERR_VAR_ASSIGNED, name)
.then(id -> stringifiers.put(id, stringifier));
}
} else {
for (String name : varNames) {
Check.that(name).is(in(), root.getVariables(), ERR_NO_SUCH_VARIABLE);
Check.that(new StringifierId(tmpl, name))
.isNot(keyIn(), stringifiers, ERR_VAR_ASSIGNED)
.then(id -> stringifiers.put(id, stringifier));
}
}
return this;
}
/**
* Assigns the specified stringifier to the specified
* {@linkplain VarGroup variable group}. Note that different instances of the
* same variable within the same template can be assigned to different variable
* groups (for example: {@code ~%html:fullName%} and {@code ~%js:fullName%}).
*
* @param groupName the name of the variable group to which to assign the
* stringifier
* @param stringifier the stringifier
* @return this {@code Builder}
*/
public Builder forVarGroup(String groupName, Stringifier stringifier) {
Check.notNull(groupName, "group name");
Check.notNull(stringifier, STRINGIFIER);
VarGroup varGroup = VarGroup.withName(groupName);
Check.that(new StringifierId(varGroup))
.isNot(keyIn(), stringifiers, ERR_GROUP_ASSIGNED)
.then(id -> stringifiers.put(id, stringifier));
return this;
}
/**
* Assigns the specified stringifier to all variables with the specified name.
* This works across all templates within the application, so be careful when
* registering a stringifier this way. You may specify a wildcard '*' character
* at the beginning or end of the variable name. For example to assign a number
* formatter to all variables whose name ends with "Price", specify
* {@code *Price} as the variable name.
*
* @param name the variable name to associate the stringifier with.
* @param stringifier the stringifier
* @return this {@code Builder}
*/
public Builder forName(String name, Stringifier stringifier) {
Check.notNull(stringifier, STRINGIFIER);
Check.that(name, NAME).isNot(empty()).isNot(EQ(), "*");
if (name.startsWith("*") || name.endsWith("*")) {
partialNames.add(Tuple2.of(name, stringifier));
} else {
Check.that(new StringifierId(name))
.isNot(keyIn(), stringifiers, ERR_VAR_ASSIGNED)
.then(x -> stringifiers.put(x, stringifier));
}
return this;
}
/**
* Assigns the specified stringifier to the specified type. Internally,
* type-based stringifiers are stored into, and looked up in a {@link TypeMap}.
* This means that if there is no stringifier defined for, say,
* {@code Short.class}, but there is a stringifier for {@code Number.class}, then
* that is the stringifier that is going to be used for {@code Short} values.
* This saves you from having to specify a stringifier for each and every
* subclass of {@code Number} if they can all be stringified in the same way.
*
* @param type the type to associate the stringifier with.
* @param stringifier the stringifier
* @return this {@code Builder}
*/
public Builder forType(Class> type, Stringifier stringifier) {
Check.notNull(stringifier, STRINGIFIER);
Check.notNull(type, TYPE)
.isNot(keyIn(), typeStringifiers, ERR_TYPE_ASSIGNED)
.then(x -> typeStringifiers.put(x, stringifier));
return this;
}
/**
* Explicitly sets the data type of the specified variables. This enables the
* {@code StringifierRegistry} to find a type-based stringifier for a value even
* if the value is {@code null} (in which case {@code Object.getClass()} is not
* available to determine the variable's type). The variable names are taken to
* be fully-qualified names, relative to the specified template.
*
* @param type the data type to set for the specified variables
* @param template the template containing the variables
* @param varNames the fully-qualified names of the variables
* @return this {@code Builder}
*/
public Builder setType(Class> type, Template template, String... varNames) {
Check.notNull(type, TYPE);
Check.notNull(template, TEMPLATE);
Check.that(varNames, VARARGS).is(deepNotNull());
for (String var : varNames) {
Template tmpl = TemplateUtils.getContainingTemplate(template, var);
// Make sure var is a variable name, not a nested template name
Check.that(var).is(in(), tmpl.getVariables());
Tuple2 tuple = Tuple2.of(tmpl, var);
Check.that(tuple)
.isNot(keyIn(), typeLookup, ERR_TYPE_SET)
.then(t -> typeLookup.put(t, type));
}
return this;
}
/**
* Returns a new, immutable {@code StringifierRegistry} instance.
*
* @return A new, immutable {@code StringifierRegistry} instance
*/
public StringifierRegistry freeze() {
return new StringifierRegistry(
stringifiers,
typeStringifiers,
typeLookup,
partialNames,
defStringifier);
}
}
/* +++++++++++++++++++++[ END BUILDER CLASS ]++++++++++++++++++ */
/**
* Applies HTML escaping. This is one of the standard stringifiers. It is the
* stringifier used by the {@link VarGroup#HTML HTML} variable group.
*/
public static final Stringifier ESCAPE_HTML = StandardStringifiers.ESCAPE_HTML;
/**
* Applies Javascript escaping. This is one of the standard stringifiers. It is the
* stringifier used by the {@link VarGroup#JS JS} variable group.
*/
public static final Stringifier ESCAPE_JS = StandardStringifiers.ESCAPE_JS;
/**
* To be used for escaping HTML attributes. Same as {@link #ESCAPE_HTML} except
* that single quotes and double quotes are also escaped. This is one of the
* standard stringifiers. It is the stringifier used by the
* {@link VarGroup#ATTR ATTR} variable group.
*/
public static final Stringifier ESCAPE_ATTR = StandardStringifiers.ESCAPE_ATTR;
/**
* To be used for escaping HTML attributes containing Javascript, like
* {@code onclick}. This is one of the standard stringifiers. It is the stringifier
* used by the {@link VarGroup#JS_ATTR JS_ATTR} variable group.
*/
public static final Stringifier ESCAPE_JS_ATTR = StandardStringifiers.ESCAPE_JS_ATTR;
/**
* To be used for escaping URL query parameter. Both parameter names and parameter
* values can be escaped using this stringifier since they are escaped identically.
* This is one of the standard stringifiers. It is the stringifier used by the
* {@link VarGroup#PARAM PARAM} variable group.
*/
public static final Stringifier ESCAPE_QUERY_PARAM = StandardStringifiers.ESCAPE_QUERY_PARAM;
/**
* To be used for escaping URL path segments. This is one of the standard
* stringifiers. It is the stringifier used by the {@link VarGroup#PATH PATH}
* variable group.
*/
public static final Stringifier ESCAPE_PATH = StandardStringifiers.ESCAPE_PATH;
/**
* Returns a {@code Builder} instance that lets you configure a
* {@code StringifierRegistry}. The {@code StringifierRegistry} will already
* contain the {@linkplain Stringifier#DEFAULT default stringifier} and the
* stringifiers for the standard {@linkplain VarGroup variable groups}.
*
* @return A {@code Builder} instance that lets you configure a
* {@code StringifierRegistry}
*/
public static Builder configure() {
return new Builder(true);
}
/**
* Returns a {@code Builder} instance that lets you configure a
* {@code StringifierRegistry}. The registry will not contain any stringifier
* except the {@linkplain Stringifier#DEFAULT default stringifier}. Useful for
* non-HTML templates.
*
* @return A {@code Builder} instance that lets you configure a
* {@code StringifierRegistry}
*/
public static Builder cleanSlate() {
return new Builder(false);
}
private final Map stringifiers;
private final Map, Stringifier> typeStringifiers;
private final Map, Class>> typeLookup;
private final List> partialNames;
private final Stringifier defStringifier;
private StringifierRegistry(
Map stringifiers,
Map, Stringifier> typeStringifiers,
Map, Class>> typeLookup,
List> partials,
Stringifier defStringifier) {
this.stringifiers = Map.copyOf(stringifiers);
this.typeStringifiers = TypeMap.fixedTypeMap(typeStringifiers);
this.partialNames = List.copyOf(partials);
this.typeLookup = Map.copyOf(typeLookup);
this.defStringifier = defStringifier;
}
Stringifier getStringifier(VariablePart part, VarGroup varGroup, Object value)
throws RenderException {
Stringifier sf;
if (part.varGroup().isPresent()) {
VarGroup vg = part.varGroup().get();
if (null != (sf = stringifiers.get(new StringifierId(vg)))) {
return sf;
}
// else the inline group name prefix was not associated with
// a stringifier, which is pointless but allowed (in the future
// we might want to use variable groups for other purposes).
}
if (varGroup != null) {
if (null != (sf = stringifiers.get(new StringifierId(varGroup)))) {
return sf;
}
}
Template tmpl = part.getParentTemplate();
String var = part.name();
if (null != (sf = stringifiers.get(new StringifierId(tmpl, var)))) {
return sf;
}
if (null != (sf = stringifiers.get(new StringifierId(null, var)))) {
return sf;
}
for (Tuple2 partial : partialNames) {
String name = partial.first();
if (name.startsWith("*")) {
if (name.endsWith("*") && var.contains(trim(name, "*"))) {
return partial.second();
} else if (var.endsWith(ltrim(name, "*"))) {
return partial.second();
}
} else if (var.startsWith(rtrim(name, "*"))) {
return partial.second();
}
}
Class> type = typeLookup.get(Tuple2.of(tmpl, var));
if (type == null && value != null) {
type = value.getClass();
}
if (type != null) {
if (null != (sf = typeStringifiers.get(type))) {
return sf;
}
}
return defStringifier;
}
}