BuilderUtils.java
package fr.sii.ogham.core.util;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Properties;
import java.util.StringJoiner;
import fr.sii.ogham.core.builder.Builder;
import fr.sii.ogham.core.builder.context.BuildContext;
import fr.sii.ogham.core.convert.Converter;
import fr.sii.ogham.core.convert.DefaultConverter;
import fr.sii.ogham.core.env.PropertyResolver;
import fr.sii.ogham.core.exception.builder.BuildException;
import fr.sii.ogham.core.fluent.AbstractParent;
import fr.sii.ogham.core.fluent.Parent;
import fr.sii.ogham.email.builder.EmailBuilder;
/**
* Helper class for {@link Builder} implementations. It separates the builder
* implementations from the environment.
*
* @author Aurélien Baudet
* @see Builder
*/
public final class BuilderUtils {
private static Converter converter;
/**
* Provide the default properties. For now, it provides only
* {@link System#getProperties()}. But according to the environment or the
* future of the module, properties may come from other source.
*
* @return the default properties
*/
public static Properties getDefaultProperties() {
return System.getProperties();
}
/**
* If the property value is an expression ({@code "${property.key}"}), then
* it is evaluated to get the value of "property.key". If the value is not
* an expression, the value is returned and converted to the result class.
*
* @param <T>
* the type of the resulting property
* @param property
* the property that may be an expression
* @param propertyResolver
* the property resolver used to find property value (if it is an
* expression)
* @param resultClass
* the result class
* @return the resulting value of the expression, the value or null
*/
public static <T> T evaluate(String property, PropertyResolver propertyResolver, Class<T> resultClass) {
if (isExpression(property)) {
return propertyResolver.getProperty(getPropertyKey(property), resultClass);
}
return getConverter().convert(property, resultClass);
}
/**
* Evaluate a list of properties that may contain expressions. It internally
* calls {@link #evaluate(String, PropertyResolver, Class)}. It tries on
* first property in the list. If {@code null} value is returned then the
* next property is tried and so on until one property returns a non-null
* value.
*
* <p>
* If all properties return null, it returns null.
*
* @param <T>
* the type of resulting value
* @param properties
* the list of properties to try in sequence
* @param propertyResolver
* the property resolver used to find property value (if it is an
* expression)
* @param resultClass
* the result class
* @return the resulting value or null
*/
public static <T> T evaluate(List<String> properties, PropertyResolver propertyResolver, Class<T> resultClass) {
if (properties == null) {
return null;
}
for (String prop : properties) {
T value = evaluate(prop, propertyResolver, resultClass);
if (value != null) {
return value;
}
}
return null;
}
/**
* Get the property of inside the expression
*
* @param expression
* the property expression
* @return the property key
*/
public static String getPropertyKey(String expression) {
return expression.substring(2, expression.length() - 1);
}
/**
* Indicates if the property is the form of an expression
* ("${property.key}") or not.
*
* @param property
* the property that may be an expression
* @return true if it is an expression, false otherwise
*/
public static boolean isExpression(String property) {
return property != null && property.startsWith("${") && property.endsWith("}");
}
/**
* Change the converter used by ByulderUtils
*
* @param converter
* the new converter
*/
public static void setConverter(Converter converter) {
BuilderUtils.converter = converter;
}
// @formatter:off
/**
* Utility method used to dynamically instantiate a builder instance.
*
* <p>
* If you want fluent chaining, your builder class <strong>MUST</strong>
* declare parent of type {@code P} as first parameter. The builder can
* implement {@link Parent} or even extend {@link AbstractParent}. For
* example, if builder is a child of {@link EmailBuilder}:
*
* <pre>
* {@code
* class MyBuilder extends AbstractParent<EmailBuilder> implements Builder<Foo> {
* public MyBuilder(EmailBuilder parent) {
* super(parent);
* }
* }
* }</pre>
*
*
* <p>
* You may need {@link BuildContext} in order to be able to evaluate
* properties in your {@link Builder#build()} method. Just declare a
* parameter of type {@link BuildContext} either as first parameter if
* you don't want fluent chaining:
*
* <pre>
* {@code
* class MyBuilder implements Builder<Foo> {
* public MyBuilder(BuildContext buildContext) {
* this.buildContext = buildContext;
* }
* }
* }</pre>
*
* or as second parameter if you want fluent chaining:
*
* <pre>
* {@code
* class MyBuilder extends AbstractParent<EmailBuilder> implements Builder<Foo> {
* public MyBuilder(EmailBuilder parent, BuildContext buildContext) {
* super(parent);
* this.buildContext = buildContext;
* }
* }
* }</pre>
*
*
* <p>
* If you need none of these features, you still have to provide a public
* default constructor.
*
* <p>
* If several constructors exist, the following order is used (first
* matching constructor is used):
* <ul>
* <li>{@code contructor(P parent, BuildContext buildContext)}</li>
* <li>{@code contructor(P parent}</li>
* <li>{@code contructor(BuildContext buildContext)}</li>
* <li>{@code contructor(}</li>
* </ul>
*
* @param <T>
* The type of the built object
* @param <B>
* The type of the builder that builds T
* @param <P>
* The type of the parent builder (used for fluent chaining)
* @param builderClass
* The builder class to instantiate
* @param parent
* The parent builder for fluent chaining
* @param buildContext
* The current build context
* @return the builder instance
* @throws BuildException
* when builder can't be instantiated
*/
// @formatter:on
@SuppressWarnings("squid:RedundantThrowsDeclarationCheck")
public static <T, B extends Builder<? extends T>, P> B instantiateBuilder(Class<B> builderClass, P parent, BuildContext buildContext) throws BuildException {
try {
return instantiate(builderClass, parent, buildContext);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | SecurityException | IllegalArgumentException e) {
throw new BuildException("Can't instantiate builder from class " + builderClass.getSimpleName(), e);
}
}
/**
* Build the instance using the provided builder.
*
* <p>
* If builder is {@code null}, it returns {@code null}.
*
* <p>
* If builder is not {@code null}, the value of {@link Builder#build()} is
* used. The returned value may be {@code null}.
*
* @param <T>
* the type of the built instance
* @param builder
* the builder
* @return the built instance or null if builder is null or if it returns
* null
*/
public static <T> T build(Builder<T> builder) {
if (builder == null) {
return null;
}
return builder.build();
}
private static <T, B extends Builder<? extends T>, P> B instantiate(Class<B> builderClass, P parent, BuildContext buildContext)
throws InstantiationException, IllegalAccessException, InvocationTargetException {
try {
return builderClass.getConstructor(parent.getClass(), BuildContext.class).newInstance(parent, buildContext);
} catch (NoSuchMethodException e) {
// skip
}
try {
return builderClass.getConstructor(parent.getClass()).newInstance(parent);
} catch (NoSuchMethodException e) {
// skip
}
try {
return builderClass.getConstructor(BuildContext.class).newInstance(buildContext);
} catch (NoSuchMethodException e) {
// skip
}
try {
return builderClass.getConstructor().newInstance();
} catch (NoSuchMethodException e) {
// skip
}
StringJoiner joiner = new StringJoiner("\n- ", "\n- ", "\n");
joiner.add("constructor(" + parent.getClass().getName() + ", " + BuildContext.class.getName() + ")\n if you want fluent chaining and inherit current build context");
joiner.add("constructor(" + parent.getClass().getName() + ")\n if you want fluent chaining");
joiner.add("constructor(" + BuildContext.class.getName() + ")\n if you don't want fluent chaining but inherit current build context");
joiner.add("constructor()\n if you don't want fluent chaining and inherit current build context");
throw new BuildException("No matching constructor found. The builder implementation must provide one of following constructors:" + joiner.toString());
}
private static Converter getConverter() {
if (converter == null) {
converter = new DefaultConverter();
}
return converter;
}
private BuilderUtils() {
super();
}
}