AccessorRegistry.java
package org.klojang.templates;
import org.klojang.check.Check;
import org.klojang.collections.TypeMap;
import org.klojang.invoke.BeanReader;
import org.klojang.invoke.BeanReaderBuilder;
import org.klojang.path.PathWalker;
import org.klojang.templates.x.MTag;
import java.util.HashMap;
import java.util.Map;
import static java.util.Collections.emptyMap;
import static org.klojang.check.CommonChecks.keyIn;
import static org.klojang.check.CommonChecks.no;
import static org.klojang.check.Tag.TYPE;
import static org.klojang.templates.x.MTag.*;
/**
* <p>A registry of {@linkplain Accessor accessors}. Accessors are used by the
* {@link RenderSession#insert(Object) insert()} and
* {@link RenderSession#populate(String, Object) populate()} methods of the
* {@code RenderSession} class to extract values from data provided by the data access
* layer. This is how an {@code AccessorRegistry} decides which accessor to use for a
* particular type of object:
*
* <ol>
* <li>If you have {@linkplain Builder#register(Class, Accessor) registered} your
* own {@code Accessor} for that particular type of object, then that is the
* {@code Accessor} that is going to be used.
* <li>Otherwise an internally defined, non-exposed {@code Accessor} implementation
* will be used. This {@code Accessor} implementation is very versatile and can
* read almost any type of object. It is internally backed by a
* {@link PathWalker}.
* </ol>
*
* <p>Note that the internally defined {@code Accessor} mentioned above does not use
* reflection to read bean properties, but it <i>does</i> use reflection to figure
* out what those properties are in the first place. Thus, if the JavaBeans are
* inside a Java 9+ module, you must {@code open} the module to <i>Klojang
* Templates</i>. Irrespective of whether the JavaBeans are inside a Java 9+
* module, <b>both the bean class and the bean properties must be {@code public}</b>.
*
* <p>Alternatively, you could write your own {@code Accessor}:
*
* <blockquote><pre>{@code
* Accessor<Person> personAccessor =
* (person, property) -> switch(property) {
* case "id" : return person.getId();
* case "firstName" : return person.getFirstName();
* case "lastName" : return person.getLastName();
* case "birthDate" : return person.getBirthDate();
* default : return Accessor.UNDEFINED;
* };
* AccessorRegistry accessors = AccessorRegistry
* .configure()
* .register(Person.class, new PersonAccessor())
* .freeze();
* RenderSession session = template.newRenderSession(accessors);
* }</pre></blockquote>
*
* <p>A slightly less verbose, but still fully reflection-free alternative is to use
* a {@link BeanReaderBuilder}:
*
* <blockquote><pre>{@code
* // forClass returns a BeanReaderBuilder
* BeanReader beanReader = BeanReader.forClass(Person.class)
* .withInt("id")
* .withString("firstName", "lastName")
* .with(LocalDate.class, "birthDate"))
* .build();
* AccessorRegistry accessors = AccessorRegistry
* .configure()
* .register(beanReader)
* .freeze();
* RenderSession session = template.newRenderSession(accessors);
* }</pre></blockquote>
*
* <p>In practice, you would likely create just a single {@code AccessorRegistry}
* instance for your entire application, when it starts up, and pass that instance
* to all calls to
* {@link Template#newRenderSession(AccessorRegistry) Template.newRenderSession()}.
*
* @author Ayco Holleman
* @see Template#newRenderSession(AccessorRegistry)
*/
public final class AccessorRegistry {
/**
* The default {@code AccessorRegistry}. It assumes that template variables map
* <i>as-is</i> to names used in source data objects.
*/
public static final AccessorRegistry STANDARD_ACCESSORS = configure().freeze();
/**
* Returns an {@code AccessorRegistry} that should be sufficient for most use cases. It
* allows you to specify one global {@link NameMapper} for mapping the template
* variables to the names used in source data objects.
*
* @param nameMapper the {@code NameMapper} to be used to map template variables to bean
* properties and/or map keys.
* @return an {@code AccessorRegistry} the should sufficient for most use cases
*/
public static AccessorRegistry standard(NameMapper nameMapper) {
return configure().setDefaultNameMapper(nameMapper).freeze();
}
/**
* Returns an {@code AccessorRegistry} that should be sufficient for most use cases.
*
* @param nullEqualsUndefined whether {@code null} values should be treated the same way
* as {@link Accessor#UNDEFINED}
* @return an {@code AccessorRegistry} the should sufficient for most use cases
*/
public static AccessorRegistry standard(boolean nullEqualsUndefined) {
return configure().nullEqualsUndefined(nullEqualsUndefined).freeze();
}
/**
* Returns an {@code AccessorRegistry} that should be sufficient for most use cases. It
* allows you to specify one global {@link NameMapper} for mapping the template
* variables to the names used in source data objects.
*
* @param nameMapper the {@code NameMapper} to be used to map template variables to bean
* properties and/or map keys.
* @param nullEqualsUndefined whether {@code null} values should be treated the same way
* as {@link Accessor#UNDEFINED}
* @return an {@code AccessorRegistry} the should sufficient for most use cases
*/
public static AccessorRegistry standard(
NameMapper nameMapper,
boolean nullEqualsUndefined) {
return configure()
.setDefaultNameMapper(nameMapper)
.nullEqualsUndefined(nullEqualsUndefined)
.freeze();
}
/* ++++++++++++++++++++[ BEGIN BUILDER CLASS ]+++++++++++++++++ */
/**
* A builder class for {@link AccessorRegistry} instances.
*
* @author Ayco Holleman
*/
public static final class Builder {
private static final String MAPPER_ALREADY_SET =
"name mapper already set for template ${0}";
private static final String TEMPLATE_ALREADY_SET =
"template ${0} already has accessor for ${1}";
private static final String TYPE_ALREADY_SET =
"${arg} has already been associated with an accessor";
private NameMapper defMapper = NameMapper.AS_IS;
private boolean nullEqualsUndefined = false;
private final Map<Class<?>, Map<Template, Accessor<?>>> accs = new HashMap<>();
private final Map<Template, NameMapper> mappers = new HashMap<>();
private Builder() {}
/**
* Sets the default {@code NameMapper} used to map template variables to bean
* properties and/or map keys. If no default {@code NameMapper} is specified, template
* variables will be mapped as-is to bean properties and/or map keys.
*
* @param nameMapper the name mapper
* @return this {@code Builder} instance
*/
public Builder setDefaultNameMapper(NameMapper nameMapper) {
defMapper = Check.notNull(nameMapper).ok();
return this;
}
/**
* Determines whether {@code null} values should be treated just like
* {@link Accessor#UNDEFINED}. By default this is not the case.
*
* @param b whether {@code null} values should be treated just like
* {@link Accessor#UNDEFINED}
* @return this {@code Builder} instance
* @see Accessor#UNDEFINED
*/
public Builder nullEqualsUndefined(boolean b) {
nullEqualsUndefined = b;
return this;
}
/**
* Sets the {@code NameMapper} to be used for the specified template.
*
* @param template the template for which to use the specified name mapper
* @param nameMapper the name mapper
* @return this {@code Builder} instance
*/
public Builder setNameMapper(Template template, NameMapper nameMapper) {
Check.notNull(template, TEMPLATE)
.isNot(keyIn(), mappers, MAPPER_ALREADY_SET, template.getName());
Check.notNull(nameMapper, NAME_MAPPER);
mappers.put(template, nameMapper);
return this;
}
/**
* Sets the {@code Accessor} to be used for objects of the specified type.
*
* @param <T> the type of the objects for which to use the {@code Accessor}
* @param type the {@code Class} object corresponding to the type
* @param accessor the {@code Accessor}
* @return this {@code Builder} instance
*/
public <T> Builder register(Class<T> type, Accessor<T> accessor) {
Check.notNull(type, TYPE);
Check.notNull(accessor, ACCESSOR);
return register0(null, type, accessor);
}
/**
* Sets the {@code Accessor} to be used for objects of the specified type, when
* inserted into the specified template.
*
* @param <T> the type of the objects for which to use the {@code Accessor}
* @param template the template for which to use the {@code Accessor}
* @param type the {@code Class} object corresponding to the type
* @param accessor the {@code Accessor}
* @return this {@code Builder} instance
*/
public <T> Builder register(Template template, Class<T> type, Accessor<T> accessor) {
Check.notNull(template, TEMPLATE);
Check.notNull(type, TYPE);
Check.notNull(accessor, ACCESSOR);
return register0(template, type, accessor);
}
/**
* Use the specified {@link BeanReader} to access objects of the type the
* {@code BeanReader} can read. Use a {@link BeanReaderBuilder} to obtain the
* {@code BeanReader} if you prefer 100% reflection-free bean reading. See
* {@link BeanReader#forClass(Class)}.
*
* @param br the {@code BeanReader}
* @param <T> the type of the beans
* @return this {@code Builder} instance
*/
public <T> Builder register(BeanReader<T> br) {
return register(br, defMapper);
}
/**
* Use the specified {@link BeanReader} to access objects of the type the
* {@code BeanReader} can read. Use a {@link BeanReaderBuilder} to obtain the
* {@code BeanReader} if you prefer 100% reflection-free bean reading. See
* {@link BeanReader#forClass(Class)}.
*
* @param beanReader the {@code BeanReader}
* @param template the template for which to use the accessor (may be a root template
* or a nested template)
* @param <T> the type of the beans
* @return this {@code Builder} instance
*/
public <T> Builder register(BeanReader<T> beanReader, Template template) {
return register(beanReader, template, defMapper);
}
/**
* Use the specified {@link BeanReader} to access objects of the type the
* {@code BeanReader} can read. Use a {@link BeanReaderBuilder} to obtain the
* {@code BeanReader} if you prefer 100% reflection-free bean reading. See
* {@link BeanReader#forClass(Class)}.
*
* @param br the {@code BeanReader}
* @param nameMapper the {@code NameMapper} to be used to map template variables to
* bean properties
* @param <T> the type of the beans
* @return this {@code Builder} instance
*/
public <T> Builder register(BeanReader<T> br, NameMapper nameMapper) {
Check.notNull(br, "BeanReader");
Check.notNull(nameMapper, NAME_MAPPER);
return register0(null, br.getBeanClass(), new BeanAccessor<>(br, nameMapper));
}
/**
* Use the specified {@link BeanReader} to access objects of the type the
* {@code BeanReader} can read. Use a {@link BeanReaderBuilder} to obtain the
* {@code BeanReader} if you prefer 100% reflection-free bean reading. See
* {@link BeanReader#forClass(Class)}.
*
* @param beanReader the {@code BeanReader}
* @param template the template for which to use the accessor (may be a root template
* or a nested template)
* @param nameMapper the {@code NameMapper} to be used to map template variables to
* bean properties
* @param <T> the type of the beans
* @return this {@code Builder} instance
*/
public <T> Builder register(
BeanReader<T> beanReader,
Template template,
NameMapper nameMapper) {
Check.notNull(beanReader, "BeanReader");
Check.notNull(template, TEMPLATE);
Check.notNull(nameMapper, NAME_MAPPER);
return register0(
template,
beanReader.getBeanClass(),
new BeanAccessor<>(beanReader, nameMapper)
);
}
/**
* Returns an {@code AccessorRegistry} with the configured accessors.
*
* @return an {@code AccessorRegistry} with the configured accessors
*/
public AccessorRegistry freeze() {
return new AccessorRegistry(accs, defMapper, nullEqualsUndefined, mappers);
}
private <T> Builder register0(Template tmpl, Class<T> clazz, Accessor<T> acc) {
Map<Template, Accessor<?>> map = accs.get(clazz);
if (map == null) {
accs.put(clazz, map = new HashMap<>());
} else if (tmpl == null) {
// allowed - template-agnostic accessor
Check.that(map.containsKey(null)).is(no(), TYPE_ALREADY_SET, clazz);
} else {
Check.that(map.containsKey(tmpl)).is(no(),
TEMPLATE_ALREADY_SET, tmpl.getName(), clazz);
}
map.put(tmpl, acc);
return this;
}
}
/* +++++++++++++++++++++[ END BUILDER CLASS ]++++++++++++++++++ */
/**
* Returns a {@code Builder} object that lets you configure an
* {@code AccessorRegistry}.
*
* @return a {@code Builder} object that lets you configure an {@code AccessorRegistry}
*/
public static Builder configure() {
return new Builder();
}
private final Map<Class<?>, Map<Template, Accessor<?>>> accs;
private final NameMapper defMapper;
private final boolean nullEqualsUndefined;
private final Map<Template, NameMapper> mappers;
private AccessorRegistry(
Map<Class<?>, Map<Template, Accessor<?>>> accs,
NameMapper defMapper,
boolean nullEqualsUndefined,
Map<Template, NameMapper> mappers) {
this.accs = accs.isEmpty() ? emptyMap() : TypeMap.fixedTypeMap(accs);
this.defMapper = defMapper;
this.nullEqualsUndefined = nullEqualsUndefined;
this.mappers = Map.copyOf(mappers);
}
boolean nullEqualsUndefined() {
return nullEqualsUndefined;
}
Accessor<?> getAccessor(Object obj, Template template) {
Class<?> type = obj.getClass();
Map<Template, Accessor<?>> m = accs.get(type);
Accessor<?> acc = null;
if (m != null) {
acc = m.get(template);
if (acc == null) {
acc = m.get(null);
}
}
if (acc == null) {
NameMapper nm = mappers.getOrDefault(template, defMapper);
acc = new PathAccessor(nm);
}
return acc;
}
}