org.skife.config.ConfigurationObjectFactory Maven / Gradle / Ivy
package org.skife.config;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.Factory;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import net.sf.cglib.proxy.NoOp;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ConfigurationObjectFactory
{
private static final Logger logger = LoggerFactory.getLogger(ConfigurationObjectFactory.class);
private static final ConcurrentMap, Factory> factories = new ConcurrentHashMap, Factory>();
private final ConfigSource config;
private final Bully bully;
public ConfigurationObjectFactory(Properties props)
{
this(new SimplePropertyConfigSource(props));
}
public ConfigurationObjectFactory(ConfigSource config)
{
this.config = config;
this.bully = new Bully();
}
public void addCoercible(final Coercible> coercible)
{
this.bully.addCoercible(coercible);
}
public T buildWithReplacements(Class configClass, Map mappedReplacements)
{
return internalBuild(configClass, mappedReplacements);
}
public T build(Class configClass)
{
return internalBuild(configClass, null);
}
private T internalBuild(Class configClass, Map mappedReplacements)
{
final List callbacks = new ArrayList();
final Map slots = new HashMap();
callbacks.add(NoOp.INSTANCE);
int count = 1;
// Hook up a toString method that prints out the settings for that bean if possible.
final Method toStringMethod = findToStringMethod(configClass);
if (toStringMethod != null) {
slots.put(toStringMethod, count++);
callbacks.add(new ConfigMagicBeanToString(callbacks));
}
// Now hook up the actual value interceptors.
for (final Method method : configClass.getMethods()) {
if (method.isAnnotationPresent(Config.class)) {
final Config annotation = method.getAnnotation(Config.class);
slots.put(method, count++);
if (method.getParameterTypes().length > 0) {
if (mappedReplacements != null) {
throw new RuntimeException("Replacements are not supported for parameterized config methods");
}
buildParameterized(callbacks, method, annotation);
}
else {
buildSimple(callbacks, method, annotation, mappedReplacements, null);
}
}
else if (method.isAnnotationPresent(ConfigReplacements.class)) {
final ConfigReplacements annotation = method.getAnnotation(ConfigReplacements.class);
slots.put(method, count++);
if (ConfigReplacements.DEFAULT_VALUE.equals(annotation.value())) {
Map fixedMap = mappedReplacements == null ?
Collections.emptyMap() : Collections.unmodifiableMap(mappedReplacements);
callbacks.add(new ConfigMagicFixedValue(method, "annotation: @ConfigReplacements", fixedMap, false));
} else {
buildSimple(callbacks, method, null, mappedReplacements, annotation);
}
}
else if (Modifier.isAbstract(method.getModifiers())) {
throw new AbstractMethodError(String.format("Method [%s] is abstract and lacks an @Config annotation",
method.toGenericString()));
}
}
if (factories.containsKey(configClass)) {
Factory f = factories.get(configClass);
return configClass.cast(f.newInstance(callbacks.toArray(new Callback[callbacks.size()])));
}
else {
Enhancer e = new Enhancer();
e.setSuperclass(configClass);
e.setCallbackFilter(new ConfigMagicCallbackFilter(slots));
e.setCallbacks(callbacks.toArray(new Callback[callbacks.size()]));
T rt = configClass.cast(e.create());
factories.putIfAbsent(configClass, (Factory) rt);
return rt;
}
}
private void buildSimple(List callbacks, Method method, Config annotation,
Map mappedReplacements, ConfigReplacements mapAnnotation)
{
String assignedFrom = null;
String[] propertyNames = new String[0];
String value = null;
// Annotation will be null for an @ConfigReplacements, in which case "value" will
// be preset and ready to be defaulted + bullied
if (annotation != null) {
propertyNames = annotation.value();
if (propertyNames == null || propertyNames.length == 0) {
throw new IllegalArgumentException("Method " +
method.toGenericString() +
" declares config annotation but no field name!");
}
for (String propertyName : propertyNames) {
if (mappedReplacements != null) {
propertyName = applyReplacements(propertyName, mappedReplacements);
}
value = config.getString(propertyName);
// First value found wins
if (value != null) {
assignedFrom = "property: '" + propertyName + "'";
logger.info("Assigning value [{}] for [{}] on [{}#{}()]",
new Object[] { value, propertyName, method.getDeclaringClass().getName(), method.getName() });
break;
}
}
} else {
if (mapAnnotation == null) {
throw new IllegalStateException("Neither @Config nor @ConfigReplacements provided, this should not be possible!");
}
String key = mapAnnotation.value();
value = mappedReplacements == null ? null : mappedReplacements.get(key);
if (value != null) {
assignedFrom = "@ConfigReplacements: key '" + key + "'";
logger.info("Assigning mappedReplacement value [{}] for [{}] on [{}#{}()]",
new Object[] { value, key, method.getDeclaringClass().getName(), method.getName() });
}
}
final boolean hasDefault = method.isAnnotationPresent(Default.class);
final boolean hasDefaultNull = method.isAnnotationPresent(DefaultNull.class);
if (hasDefault && hasDefaultNull) {
throw new IllegalArgumentException(String.format("@Default and @DefaultNull present in [%s]", method.toGenericString()));
}
boolean useMethod = false;
//
// This is how the value logic works if no value has been set by the config:
//
// - if the @Default annotation is present, use its value.
// - if the @DefaultNull annotation is present, accept null as the value
// - otherwise, check whether the method is not abstract. If it is not, mark the callback that it should call the method and
// ignore the passed in value (which will be null)
// - if all else fails, throw an exception.
//
if (value == null) {
if (hasDefault) {
value = method.getAnnotation(Default.class).value();
assignedFrom = "annotation: @Default";
logger.info("Assigning default value [{}] for {} on [{}#{}()]",
new Object[] { value, propertyNames, method.getDeclaringClass().getName(), method.getName() });
}
else if (hasDefaultNull) {
logger.info("Assigning null default value for {} on [{}#{}()]",
new Object[] { propertyNames, method.getDeclaringClass().getName(), method.getName() });
assignedFrom = "annotation: @DefaultNull";
}
else {
// Final try: Is the method is actually callable?
if (!Modifier.isAbstract(method.getModifiers())) {
useMethod = true;
assignedFrom = "method: '" + method.getName() + "()'";
logger.info("Using method itself for {} on [{}#{}()]",
new Object[] { propertyNames, method.getDeclaringClass().getName(), method.getName() });
}
else {
throw new IllegalArgumentException(String.format("No value present for '%s' in [%s]",
prettyPrint(propertyNames, mappedReplacements),
method.toGenericString()));
}
}
}
final Object finalValue = bully.coerce(method.getGenericReturnType(), value, method.getAnnotation(Separator.class));
callbacks.add(new ConfigMagicFixedValue(method, assignedFrom, finalValue, useMethod));
}
private String applyReplacements(String propertyName, Map mappedReplacements)
{
for (String key : mappedReplacements.keySet()) {
String token = makeToken(key);
String replacement = mappedReplacements.get(key);
propertyName = propertyName.replace(token, replacement);
}
return propertyName;
}
private void buildParameterized(List callbacks, Method method, Config annotation)
{
String defaultValue = null;
final boolean hasDefault = method.isAnnotationPresent(Default.class);
final boolean hasDefaultNull = method.isAnnotationPresent(DefaultNull.class);
if (hasDefault && hasDefaultNull) {
throw new IllegalArgumentException(String.format("@Default and @DefaultNull present in [%s]", method.toGenericString()));
}
if (hasDefault) {
defaultValue = method.getAnnotation(Default.class).value();
}
else if (!hasDefaultNull) {
throw new IllegalArgumentException(String.format("No value present for '%s' in [%s]",
prettyPrint(annotation.value(), null),
method.toGenericString()));
}
final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
final List paramTokenList = new ArrayList();
for (Annotation[] parameterTab : parameterAnnotations) {
for (Annotation parameter : parameterTab) {
if (parameter.annotationType().equals(Param.class)) {
Param paramAnnotation = (Param) parameter;
paramTokenList.add(makeToken(paramAnnotation.value()));
break;
}
}
}
if (paramTokenList.size() != method.getParameterTypes().length) {
throw new RuntimeException(String.format("Method [%s] is missing one or more @Param annotations",
method.toGenericString()));
}
final Object bulliedDefaultValue = bully.coerce(method.getGenericReturnType(), defaultValue, method.getAnnotation(Separator.class));
final String[] annotationValues = annotation.value();
if (annotationValues == null || annotationValues.length == 0) {
throw new IllegalArgumentException("Method " +
method.toGenericString() +
" declares config annotation but no field name!");
}
callbacks.add(new ConfigMagicMethodInterceptor(method,
config,
annotationValues,
paramTokenList,
bully,
bulliedDefaultValue));
}
private String makeToken(String temp)
{
return "${" + temp + "}";
}
private String prettyPrint(String[] values, final Map mappedReplacements)
{
if (values == null || values.length == 0) {
return "";
}
final StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < values.length; i++) {
sb.append(values[i]);
if (i < (values.length - 1)) {
sb.append(", ");
}
}
sb.append(']');
if (mappedReplacements != null && mappedReplacements.size() > 0) {
sb.append(" translated to [");
for (int i = 0; i < values.length; i++) {
sb.append(applyReplacements(values[i], mappedReplacements));
if (i < (values.length - 1)) {
sb.append(", ");
}
}
sb.append("]");
}
return sb.toString();
}
private static final class ConfigMagicFixedValue implements MethodInterceptor
{
private final Method method;
private final String assignedFrom;
private final Handler handler;
private ConfigMagicFixedValue(final Method method, final String assignedFrom, final Object value, final boolean callSuper)
{
this.method = method;
this.assignedFrom = assignedFrom;
// This is a workaround for broken cglib
if (callSuper) {
this.handler = new InvokeSuperHandler();
}
else {
handler = new FixedValueHandler(value);
}
}
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable
{
return handler.handle(methodProxy, o, objects);
}
private static interface Handler
{
Object handle(MethodProxy m, Object o, Object[] args) throws Throwable;
}
private static class InvokeSuperHandler implements Handler
{
public Object handle(MethodProxy m, Object o, Object[] args) throws Throwable
{
return m.invokeSuper(o, args);
}
}
private static class FixedValueHandler implements Handler
{
private final Object finalValue;
public FixedValueHandler(final Object finalValue)
{
this.finalValue = finalValue;
}
public Object handle(MethodProxy m, Object o, Object[] args) throws Throwable
{
return finalValue;
}
private transient String toStringValue = null;
@Override
public String toString()
{
if (toStringValue == null) {
final StringBuilder sb = new StringBuilder("value: ");
if (finalValue != null) {
sb.append(finalValue.toString());
sb.append(", class: ");
sb.append(finalValue.getClass().getName());
}
else {
sb.append("");
}
toStringValue = sb.toString();
}
return toStringValue;
}
}
private transient String toStringValue = null;
@Override
public String toString()
{
if (toStringValue == null) {
final StringBuilder sb = new StringBuilder(method.getName());
sb.append("(): ");
sb.append(assignedFrom);
sb.append(", ");
sb.append(handler.toString());
toStringValue = sb.toString();
}
return toStringValue;
}
}
private static final class ConfigMagicCallbackFilter implements CallbackFilter
{
private final Map slots;
private ConfigMagicCallbackFilter(final Map slots)
{
this.slots = slots;
}
public int accept(Method method)
{
return slots.containsKey(method) ? slots.get(method) : 0;
}
}
private static final class ConfigMagicMethodInterceptor implements MethodInterceptor
{
private final Method method;
private final ConfigSource config;
private final String[] properties;
private final Bully bully;
private final Object defaultValue;
private final List paramTokenList;
private ConfigMagicMethodInterceptor(final Method method,
final ConfigSource config,
final String[] properties,
final List paramTokenList,
final Bully bully,
final Object defaultValue)
{
this.method = method;
this.config = config;
this.properties = properties;
this.paramTokenList = paramTokenList;
this.bully = bully;
this.defaultValue = defaultValue;
}
public Object intercept(final Object o,
final Method method,
final Object[] args,
final MethodProxy methodProxy) throws Throwable
{
for (String property : properties) {
if (args.length == paramTokenList.size()) {
for (int i = 0; i < args.length; ++i) {
property = property.replace(paramTokenList.get(i), String.valueOf(args[i]));
}
String value = config.getString(property);
if (value != null) {
logger.info("Assigning value [{}] for [{}] on [{}#{}()]",
new Object[] { value, property, method.getDeclaringClass().getName(), method.getName() });
return bully.coerce(method.getGenericReturnType(), value, method.getAnnotation(Separator.class));
}
}
else {
throw new IllegalStateException("Argument list doesn't match @Param list");
}
}
logger.info("Assigning default value [{}] for {} on [{}#{}()]",
new Object[] { defaultValue, properties, method.getDeclaringClass().getName(), method.getName() });
return defaultValue;
}
private transient String toStringValue = null;
@Override
public String toString()
{
if (toStringValue == null) {
toStringValue = method.getName() + ": " + super.toString();
}
return toStringValue;
}
}
private Method findToStringMethod(final Class> clazz)
{
try {
return clazz.getMethod("toString", new Class [] {});
}
catch (NoSuchMethodException nsme) {
try {
return Object.class.getMethod("toString", new Class [] {});
}
catch (NoSuchMethodException nsme2) {
throw new IllegalStateException("Could not intercept toString method!", nsme);
}
}
}
private static final class ConfigMagicBeanToString implements MethodInterceptor
{
private final List callbacks;
private transient String toStringValue = null;
private ConfigMagicBeanToString(final List callbacks)
{
this.callbacks = callbacks;
}
public Object intercept(final Object o,
final Method method,
final Object[] args,
final MethodProxy methodProxy) throws Throwable
{
if (toStringValue == null) {
final StringBuilder sb = new StringBuilder();
for (int i = 2; i < callbacks.size(); i++) {
sb.append(callbacks.get(i).toString());
if (i < callbacks.size() - 1) {
sb.append("\n");
}
}
toStringValue = sb.toString();
}
return toStringValue;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy