TemplateBuilderHelper.java
package fr.sii.ogham.core.builder.template;
import static fr.sii.ogham.core.util.BuilderUtils.instantiateBuilder;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import fr.sii.ogham.core.builder.Builder;
import fr.sii.ogham.core.builder.configuration.ConfigurationValueBuilder;
import fr.sii.ogham.core.builder.configuration.ConfigurationValueBuilderHelper;
import fr.sii.ogham.core.builder.configurer.Configurer;
import fr.sii.ogham.core.builder.context.BuildContext;
import fr.sii.ogham.core.builder.env.EnvironmentBuilder;
import fr.sii.ogham.core.builder.priority.ImplementationPriorityProvider;
import fr.sii.ogham.core.builder.priority.PriorityProvider;
import fr.sii.ogham.core.exception.builder.BuildException;
import fr.sii.ogham.core.message.content.MultiTemplateContent;
import fr.sii.ogham.core.message.content.Variant;
import fr.sii.ogham.core.template.detector.FixedEngineDetector;
import fr.sii.ogham.core.template.detector.TemplateEngineDetector;
import fr.sii.ogham.core.template.parser.AutoDetectTemplateParser;
import fr.sii.ogham.core.template.parser.AutoDetectTemplateParser.TemplateImplementation;
import fr.sii.ogham.core.template.parser.TemplateParser;
import fr.sii.ogham.core.util.PriorizedList;
import fr.sii.ogham.template.common.adapter.FailIfNotFoundVariantResolver;
import fr.sii.ogham.template.common.adapter.FailIfNotFoundWithTestedPathsVariantResolver;
import fr.sii.ogham.template.common.adapter.FirstExistingResourceVariantResolver;
import fr.sii.ogham.template.common.adapter.NullVariantResolver;
import fr.sii.ogham.template.common.adapter.VariantResolver;
/**
* Helps to configure a {@link TemplateParser} builder.
*
* <p>
* It registers and uses {@link Builder}s to instantiate and configure
* {@link TemplateParser} specialized implementations.
* </p>
*
* <p>
* It also configures how to handle missing variant (either fail or do nothing).
* </p>
*
* @author Aurélien Baudet
*
* @param <P>
* the type of the parent builder used by custom
* {@link TemplateParser} {@link Builder}
*/
public class TemplateBuilderHelper<P> {
private static final Logger LOG = LoggerFactory.getLogger(TemplateBuilderHelper.class);
private final P parent;
private final List<Builder<? extends TemplateParser>> templateBuilders;
private final BuildContext buildContext;
private final ConfigurationValueBuilderHelper<TemplateBuilderHelper<P>, Boolean> missingVariantFailValueBuilder;
private final ConfigurationValueBuilderHelper<TemplateBuilderHelper<P>, Boolean> listPossiblePathsValueBuilder;
private final PriorityProvider<TemplateParser> priorityProvider;
private VariantResolver missingResolver;
/**
* Initializes the builder with a parent builder. The parent builder is used
* when calling and() method of any registered {@link TemplateParser}
* {@link Builder}. The {@link EnvironmentBuilder} is used to evaluate
* properties at build time (used by {@link TemplateParser}
* {@link Builder}s).
*
* @param parent
* the parent builder
* @param buildContext
* for registering instances and property evaluation
*/
public TemplateBuilderHelper(P parent, BuildContext buildContext) {
super();
this.parent = parent;
this.buildContext = buildContext;
templateBuilders = new ArrayList<>();
missingVariantFailValueBuilder = buildContext.newConfigurationValueBuilder(this, Boolean.class);
listPossiblePathsValueBuilder = buildContext.newConfigurationValueBuilder(this, Boolean.class);
priorityProvider = new ImplementationPriorityProvider<>(buildContext);
}
/**
* Indicates if some {@link TemplateParser} {@link Builder}s have been
* registered
*
* @return true if at least one builder has been registered
*/
public boolean hasRegisteredTemplates() {
return !templateBuilders.isEmpty();
}
/**
* If a variant is missing, then force to fail.
*
* <p>
* This may be useful if you want for example to always provide a text
* fallback when using an html template. So if a client can't read the html
* version, the fallback version will still always be readable. So to avoid
* forgetting to write text template, set this to true.
* </p>
*
* <p>
* The value set using this method takes precedence over any property and
* default value configured using {@link #failIfMissingVariant()}.
*
* <pre>
* .failIfMissingVariant(false)
* .failIfMissingVariant()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(true)
* </pre>
*
* <pre>
* .failIfMissingVariant(false)
* .failIfMissingVariant()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(true)
* </pre>
*
* In both cases, {@code failIfMissingVariant(false)} is used.
*
* <p>
* If this method is called several times, only the last value is used.
*
* <p>
* If {@code null} value is set, it is like not setting a value at all. The
* property/default value configuration is applied.
*
* @param fail
* fail if a variant is missing
* @return this instance for fluent chaining
*/
public TemplateBuilderHelper<P> failIfMissingVariant(Boolean fail) {
missingVariantFailValueBuilder.setValue(fail);
return this;
}
/**
* If a variant is missing, then force to fail.
*
* <p>
* This may be useful if you want for example to always provide a text
* fallback when using an html template. So if a client can't read the html
* version, the fallback version will still always be readable. So to avoid
* forgetting to write text template, set this to true.
* </p>
*
* <p>
* This method is mainly used by {@link Configurer}s to register some
* property keys and/or a default value. The aim is to let developer be able
* to externalize its configuration (using system properties, configuration
* file or anything else). If the developer doesn't configure any value for
* the registered properties, the default value is used (if set).
*
* <pre>
* .failIfMissingVariant()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(true)
* </pre>
*
* <p>
* Non-null value set using {@link #failIfMissingVariant(Boolean)} takes
* precedence over property values and default value.
*
* <pre>
* .failIfMissingVariant(false)
* .failIfMissingVariant()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(true)
* </pre>
*
* The value {@code false} is used regardless of the value of the properties
* and default value.
*
* <p>
* See {@link ConfigurationValueBuilder} for more information.
*
*
* @return the builder to configure property keys/default value
*/
public ConfigurationValueBuilder<TemplateBuilderHelper<P>, Boolean> failIfMissingVariant() {
return missingVariantFailValueBuilder;
}
/**
* When {@link #failIfMissingVariant()} is enabled, also indicate which
* paths were tried in order to help debugging why a variant was not found.
*
* <p>
* The value set using this method takes precedence over any property and
* default value configured using {@link #listPossiblePaths()}.
*
* <pre>
* .listPossiblePaths(true)
* .listPossiblePaths()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(false)
* </pre>
*
* <pre>
* .listPossiblePaths(true)
* .listPossiblePaths()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(false)
* </pre>
*
* In both cases, {@code listPossiblePaths(true)} is used.
*
* <p>
* If this method is called several times, only the last value is used.
*
* <p>
* If {@code null} value is set, it is like not setting a value at all. The
* property/default value configuration is applied.
*
* @param enable
* enable/disable tracking of possible paths for template
* variants
* @return this instance for fluent chaining
*/
public TemplateBuilderHelper<P> listPossiblePaths(Boolean enable) {
listPossiblePathsValueBuilder.setValue(enable);
return this;
}
/**
* When {@link #failIfMissingVariant()} is enabled, also indicate which
* paths were tried in order to help debugging why a variant was not found.
*
* <p>
* This method is mainly used by {@link Configurer}s to register some
* property keys and/or a default value. The aim is to let developer be able
* to externalize its configuration (using system properties, configuration
* file or anything else). If the developer doesn't configure any value for
* the registered properties, the default value is used (if set).
*
* <pre>
* .listPossiblePaths()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(false)
* </pre>
*
* <p>
* Non-null value set using {@link #listPossiblePaths(Boolean)} takes
* precedence over property values and default value.
*
* <pre>
* .listPossiblePaths(true)
* .listPossiblePaths()
* .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
* .defaultValue(false)
* </pre>
*
* The value {@code true} is used regardless of the value of the properties
* and default value.
*
* <p>
* See {@link ConfigurationValueBuilder} for more information.
*
*
* @return the builder to configure property keys/default value
*/
public ConfigurationValueBuilder<TemplateBuilderHelper<P>, Boolean> listPossiblePaths() {
return listPossiblePathsValueBuilder;
}
/**
* Provide custom resolver that will handle a missing variant.
*
* @param resolver
* the custom resolver
*/
public void missingVariant(VariantResolver resolver) {
this.missingResolver = resolver;
}
/**
* Registers and configures a {@link TemplateParser} through a dedicated
* builder.
*
* For example:
*
* <pre>
* .register(ThymeleafEmailBuilder.class)
* .detector(new ThymeleafEngineDetector());
* </pre>
*
* <p>
* Your {@link Builder} may implement {@link VariantBuilder} to handle
* template {@link Variant}s (used for {@link MultiTemplateContent} that
* provide a single path to templates with different extensions for
* example).
* </p>
*
* <p>
* Your {@link Builder} may also implement {@link DetectorBuilder} in order
* to indicate which kind of templates your {@link TemplateParser} is able
* to parse. If your template parse is able to parse any template file you
* are using, you may not need to implement {@link DetectorBuilder}.
* </p>
*
* <p>
* In order to be able to keep chaining, you builder instance may provide a
* constructor with two arguments:
* <ul>
* <li>The type of the parent builder ({@code <P>})</li>
* <li>The {@link EnvironmentBuilder} instance</li>
* </ul>
* If you don't care about chaining, just provide a default constructor.
*
* @param builderClass
* the builder class to instantiate
* @param <B>
* the type of the builder
* @return the builder to configure the implementation
*/
@SuppressWarnings("unchecked")
public <B extends Builder<? extends TemplateParser>> B register(Class<B> builderClass) {
// if already registered => provide same instance
for (Builder<? extends TemplateParser> builder : templateBuilders) {
if (builderClass.isAssignableFrom(builder.getClass())) {
return (B) builder;
}
}
// create the builder instance
B builder = instantiateBuilder(builderClass, parent, buildContext);
templateBuilders.add(builder);
return builder;
}
/**
* Build the template parser according to options previously enabled. If
* only one template engine has been activated then the parser will be this
* template engine parser. If there are several activated engines, then the
* builder will generate an {@link AutoDetectTemplateParser}. This kind of
* parser is able to detect which parser to use according to the provided
* template at runtime. The auto-detection is delegated to each defined
* {@link TemplateEngineDetector} associated with each engine.
*
* @return the template parser instance
* @throws BuildException
* when template parser couldn't be initialized
*/
public TemplateParser buildTemplateParser() {
// TODO: handle enable?
List<TemplateImplementation> impls = buildTemplateParserImpls();
if (impls.isEmpty()) {
// if no template parser available => exception
throw new BuildException("No parser available. Either disable template features or register a template engine");
}
if (impls.size() == 1) {
// if no detector defined or only one available parser => do not use
// auto detection
TemplateParser parser = impls.get(0).getParser();
LOG.info("Using single template engine: {}", parser);
return parser;
}
LOG.info("Using auto detection mechanism");
LOG.debug("Auto detection mechanisms: {}", impls);
return buildContext.register(new AutoDetectTemplateParser(impls));
}
/**
* Instantiates and configures the variant resolution. Variant resolution is
* a chain of resolvers. The first resolver that is able to resolve a
* variant is used. If no resolver is able to resolve a variant, it uses the
* default variant resolver (see {@link #failIfMissingVariant(Boolean)} and
* {@link #missingVariant(VariantResolver)}).
*
* @return the variant resolver
*/
public VariantResolver buildVariant() {
FirstExistingResourceVariantResolver variantResolver = buildContext.register(new FirstExistingResourceVariantResolver(buildDefaultVariantResolver()));
for (Builder<? extends TemplateParser> builder : templateBuilders) {
if (builder instanceof VariantBuilder) {
variantResolver.addVariantResolver(((VariantBuilder<?>) builder).buildVariant());
}
}
return variantResolver;
}
@SuppressWarnings("squid:S5411")
private VariantResolver buildDefaultVariantResolver() {
if (missingResolver != null) {
return missingResolver;
}
if (missingVariantFailValueBuilder.getValue(false)) {
return buildFailingVariantResolver();
}
return buildContext.register(new NullVariantResolver());
}
@SuppressWarnings("squid:S5411")
private VariantResolver buildFailingVariantResolver() {
if (!listPossiblePathsValueBuilder.getValue(false)) {
return buildContext.register(new FailIfNotFoundVariantResolver());
}
FailIfNotFoundWithTestedPathsVariantResolver failResolver = new FailIfNotFoundWithTestedPathsVariantResolver();
for (Builder<? extends TemplateParser> builder : templateBuilders) {
if (builder instanceof VariantBuilder) {
failResolver.addVariantResolver(((VariantBuilder<?>) builder).buildVariant());
}
}
return buildContext.register(failResolver);
}
private List<TemplateImplementation> buildTemplateParserImpls() {
PriorizedList<TemplateImplementation> impls = new PriorizedList<>();
for (Builder<? extends TemplateParser> builder : templateBuilders) {
TemplateEngineDetector detector;
if (builder instanceof DetectorBuilder) {
detector = ((DetectorBuilder<?>) builder).buildDetector();
} else {
detector = buildContext.register(new FixedEngineDetector(true));
}
TemplateParser templateParser = builder.build();
impls.register(new TemplateImplementation(detector, templateParser), priorityProvider.provide(templateParser));
}
return impls.getOrdered();
}
}