/*
* Copyright 2008 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.constretto.internal;
import com.thoughtworks.paranamer.BytecodeReadingParanamer;
import com.thoughtworks.paranamer.Paranamer;
import org.constretto.ConfigurationDefaultValueFactory;
import org.constretto.ConstrettoConfiguration;
import org.constretto.GenericConverter;
import org.constretto.Property;
import org.constretto.annotation.Configuration;
import org.constretto.annotation.Configure;
import org.constretto.annotation.Tags;
import org.constretto.exception.ConstrettoConversionException;
import org.constretto.exception.ConstrettoException;
import org.constretto.exception.ConstrettoExpressionException;
import org.constretto.internal.converter.ValueConverterRegistry;
import org.constretto.internal.introspect.Constructors;
import org.constretto.model.CPrimitive;
import org.constretto.model.CValue;
import org.constretto.model.ConfigurationValue;
import java.lang.annotation.Annotation;
import java.lang.ref.WeakReference;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import static java.util.Arrays.asList;
import static org.constretto.internal.GenericCollectionTypeResolver.*;
/**
* @author <a href="mailto:kaare.nilsen@gmail.com">Kaare Nilsen</a>
*/
public class DefaultConstrettoConfiguration implements ConstrettoConfiguration {
private static final String NULL_STRING = "![![Null]!]!";
private final Paranamer paranamer = new BytecodeReadingParanamer();
protected final Map<String, List<ConfigurationValue>> configuration;
private Set<WeakReference<Object>> configuredObjects = new CopyOnWriteArraySet<WeakReference<Object>>();
private final List<String> originalTags = new ArrayList<String>();
protected final List<String> currentTags = new ArrayList<String>();
public DefaultConstrettoConfiguration(Map<String, List<ConfigurationValue>> configuration, List<String> originalTags) {
this.configuration = configuration;
this.originalTags.addAll(originalTags);
this.currentTags.addAll(originalTags);
}
public DefaultConstrettoConfiguration(Map<String, List<ConfigurationValue>> configuration) {
this.configuration = configuration;
}
@SuppressWarnings("unchecked")
public <K> K evaluateTo(String expression, K defaultValue) {
if (!hasValue(expression)) {
return defaultValue;
}
K value;
try {
value = (K) processAndConvert(defaultValue.getClass(), expression);
} catch (ConstrettoConversionException e) {
value = null;
}
return null != value ? value : defaultValue;
}
public <T> T evaluateWith(GenericConverter<T> converter, String expression) {
ConfigurationValue value = findElementOrThrowException(expression);
return converter.fromValue(value.value());
}
public CValue evaluate(String expression) throws ConstrettoExpressionException {
return findElementOrThrowException(expression).value();
}
@SuppressWarnings("unchecked")
public <K> List<K> evaluateToList(Class<K> targetClass, String expression) {
ConfigurationValue value = findElementOrThrowException(expression);
return (List<K>) ValueConverterRegistry.convert(targetClass, targetClass, value.value());
}
@SuppressWarnings("unchecked")
public <K, V> Map<K, V> evaluateToMap(Class<K> keyClass, Class<V> valueClass, String expression) {
ConfigurationValue value = findElementOrThrowException(expression);
return (Map<K, V>) ValueConverterRegistry.convert(valueClass, keyClass, value.value());
}
public <K> K evaluateTo(Class<K> targetClass, String expression) throws ConstrettoExpressionException {
return processAndConvert(targetClass, expression);
}
public String evaluateToString(String expression) throws ConstrettoExpressionException {
return processAndConvert(String.class, expression);
}
public Boolean evaluateToBoolean(String expression) throws ConstrettoExpressionException {
return processAndConvert(Boolean.class, expression);
}
public Double evaluateToDouble(String expression) throws ConstrettoExpressionException {
return processAndConvert(Double.class, expression);
}
public Long evaluateToLong(String expression) throws ConstrettoExpressionException {
return processAndConvert(Long.class, expression);
}
public Float evaluateToFloat(String expression) throws ConstrettoExpressionException {
return processAndConvert(Float.class, expression);
}
public Integer evaluateToInt(String expression) throws ConstrettoExpressionException {
return processAndConvert(Integer.class, expression);
}
public Short evaluateToShort(String expression) throws ConstrettoExpressionException {
return processAndConvert(Short.class, expression);
}
public Byte evaluateToByte(String expression) throws ConstrettoExpressionException {
return processAndConvert(Byte.class, expression);
}
public <T> T as(Class<T> configurationClass) throws ConstrettoException {
T objectToConfigure;
try {
objectToConfigure = createInstance(configurationClass);
} catch (ConstrettoException e) {
throw e;
}
catch (Exception e) {
throw new ConstrettoException("Could not instansiate class of type: " + configurationClass.getName()
+ " when trying to inject it with configuration, It may be missing a default or @Configure annotated constructor", e);
}
injectConfiguration(objectToConfigure);
return objectToConfigure;
}
public <T> T on(T objectToConfigure) throws ConstrettoException {
injectConfiguration(objectToConfigure);
return objectToConfigure;
}
public Map<String, String> asMap() {
Map<String, String> properties = new HashMap<String, String>();
for (Map.Entry<String, List<ConfigurationValue>> entry : configuration.entrySet()) {
ConfigurationValue value = findElementOrNull(entry.getKey());
if (value != null){
properties.put(entry.getKey(), value.value().toString());
}
}
return properties;
}
public boolean hasValue(String expression) {
return findElementOrNull(expression) != null;
}
public void appendTag(String... newtags) {
currentTags.addAll(asList(newtags));
reconfigure();
}
public void prependTag(String... newtags) {
currentTags.addAll(0, asList(newtags));
reconfigure();
}
public void resetTags(boolean reconfigure) {
currentTags.clear();
currentTags.addAll(originalTags);
if (reconfigure)
reconfigure();
}
public void clearTags(boolean reconfigure) {
currentTags.clear();
originalTags.clear();
if (reconfigure)
reconfigure();
}
public void removeTag(String... newTags) {
for (String newTag : newTags) {
currentTags.remove(newTag);
}
reconfigure();
}
public List<String> getCurrentTags() {
return currentTags;
}
public Iterator<Property> iterator() {
List<Property> properties = new ArrayList<Property>();
Map<String, String> map = asMap();
for (Map.Entry<String, String> entry : map.entrySet()) {
properties.add(new Property(entry.getKey(), entry.getValue()));
}
return properties.iterator();
}
@Override
public void reconfigure() {
WeakReference[] references = configuredObjects.toArray(new WeakReference[configuredObjects.size()]);
for (WeakReference reference : references) {
if (reference != null && reference.get() != null) {
on(reference.get());
}
}
}
//
// Helper methods
//
private <T> T createInstance(final Class<T> configurationClass) throws InstantiationException, IllegalAccessException {
if(configurationClass.isInterface()) {
throw new ConstrettoException("Can not instantiate interfaces. You need to create an concrete implementing class first");
}
if (configurationClass.isAnonymousClass()) {
throw new ConstrettoException("Can not instantiate anonymous classes using as(Class<T>. To inject configuration in to inner or anonymous classes, " +
"instantiate it first and call the on(T configuredObjecT) method");
}
Constructor<T>[] annotatedConstructors = findAnnotatedConstructorsOnClass(configurationClass);
if(configurationClass.isMemberClass() && annotatedConstructors != null) {
throw new ConstrettoException("Can not instantiate inner classes using a @Configure annotated constructor. " +
"To inject configuration, construct the instance yourself use the \"on(T configuredObject)\" method");
}
if(annotatedConstructors == null) {
return configurationClass.newInstance();
} else {
if(annotatedConstructors.length > 1) {
throw new ConstrettoException("More than one @Configure annotated constructor defined for class \"" + configurationClass.getName() + "\". It can only be one");
}
Constructor<T> constructor = annotatedConstructors[0];
final Object[] resolvedParameters = resolveParameters(constructor);
try {
constructor.setAccessible(true);
return constructor.newInstance(resolvedParameters);
} catch (InvocationTargetException e) {
throw new ConstrettoException("Could not instantiate class with @Configure annotated constructor");
}
}
}
private <T> Constructor<T>[] findAnnotatedConstructorsOnClass(final Class<T> configurationClass) {
return Constructors.findConstructorsWithConfigureAnnotation(configurationClass);
}
protected ConfigurationValue findElementOrThrowException(String expression) {
if (!configuration.containsKey(expression)) {
throw new ConstrettoExpressionException(expression, currentTags);
}
List<ConfigurationValue> values = configuration.get(expression);
ConfigurationValue resolvedNode = resolveMatch(values);
if (resolvedNode == null) {
throw new ConstrettoExpressionException(expression, currentTags);
}
if (resolvedNode.value().containsVariables()) {
for (String key : resolvedNode.value().referencedKeys()) {
resolvedNode.value().replace(key, evaluateToString(key));
}
}
return resolvedNode;
}
protected ConfigurationValue findElementOrNull(String expression) {
if (!configuration.containsKey(expression)) {
return null;
}
List<ConfigurationValue> values = configuration.get(expression);
ConfigurationValue resolvedNode = resolveMatch(values);
if (resolvedNode == null) {
return null;
}
if (resolvedNode.value().containsVariables()) {
for (String key : resolvedNode.value().referencedKeys()) {
resolvedNode.value().replace(key, evaluateToString(key));
}
}
return resolvedNode;
}
@SuppressWarnings("unchecked")
private <T> T processAndConvert(Class<T> clazz, String expression) throws ConstrettoException {
ConfigurationValue value = findElementOrThrowException(expression);
return (T) ValueConverterRegistry.convert(clazz, clazz, value.value());
}
private ConfigurationValue resolveMatch(List<ConfigurationValue> values) {
ConfigurationValue bestMatch = null;
for (ConfigurationValue configurationNode : values) {
if (ConfigurationValue.DEFAULT_TAG.equals(configurationNode.tag())) {
if (bestMatch == null || bestMatch.tag().equals(ConfigurationValue.DEFAULT_TAG)) {
bestMatch = configurationNode;
}
} else if (currentTags.contains(configurationNode.tag())) {
if (bestMatch == null) {
bestMatch = configurationNode;
} else {
int previousFoundPriority =
ConfigurationValue.DEFAULT_TAG.equals(bestMatch.tag()) ?
Integer.MAX_VALUE : currentTags.indexOf(bestMatch.tag());
if (currentTags.indexOf(configurationNode.tag()) <= previousFoundPriority) {
bestMatch = configurationNode;
}
}
} else if (ConfigurationValue.ALL_TAG.equals(configurationNode.tag())) {
bestMatch = configurationNode;
}
}
return bestMatch;
}
private <T> void injectConfiguration(T objectToConfigure) {
injectFields(objectToConfigure);
injectMethods(objectToConfigure);
boolean found = false;
for (WeakReference<Object> configuredObject : configuredObjects) {
if (configuredObject.get() == objectToConfigure) {
found = true;
break;
}
}
if (!found) {
this.configuredObjects.add(new WeakReference<Object>(objectToConfigure));
}
}
private Object[] resolveParameters(AccessibleObject accessibleObject) throws IllegalAccessException, InstantiationException {
Annotation[][] methodAnnotations;
String[] parameterNames;
Class<?>[] parameterTargetTypes;
if(accessibleObject instanceof Method) {
Method method = (Method) accessibleObject;
methodAnnotations = method.getParameterAnnotations();
parameterNames = paranamer.lookupParameterNames(method);
parameterTargetTypes = method.getParameterTypes();
} else if(accessibleObject instanceof Constructor) {
Constructor constructor = (Constructor) accessibleObject;
methodAnnotations = constructor.getParameterAnnotations();
parameterNames = paranamer.lookupParameterNames(constructor);
parameterTargetTypes = constructor.getParameterTypes();
} else {
throw new ConstrettoException("Could not resolve parameter names ");
}
Object[] resolvedArguments = new Object[methodAnnotations.length];
int i = 0;
for (Annotation[] parameterAnnotations : methodAnnotations) {
Object defaultValue = null;
boolean required = true;
String expression = "";
Class<?> parameterTargetClass = parameterTargetTypes[i];
if (parameterAnnotations.length != 0) {
for (Annotation parameterAnnotation : parameterAnnotations) {
if (parameterAnnotation.annotationType() == Configuration.class) {
Configuration configurationAnnotation = (Configuration) parameterAnnotation;
expression = configurationAnnotation.value();
required = configurationAnnotation.required();
if (hasAnnotationDefaults(configurationAnnotation)) {
if (configurationAnnotation.defaultValueFactory().equals(Configuration.EmptyValueFactory.class)) {
defaultValue = ValueConverterRegistry.convert(parameterTargetClass, parameterTargetClass, new CPrimitive(configurationAnnotation.defaultValue()));
} else {
ConfigurationDefaultValueFactory valueFactory = configurationAnnotation.defaultValueFactory().newInstance();
defaultValue = valueFactory.getDefaultValue();
}
}
}
}
}
if (expression.equals("")) {
if (parameterNames == null) {
throw new ConstrettoException("Could not resolve the expression of the property to look up. " +
"The cause of this could be that the class is compiled without debug enabled. " +
"when a class is compiled without debug, the @Configuration with a value attribute is required " +
"to correctly resolve the property expression.");
} else {
expression = parameterNames[i];
}
}
if (hasValue(expression)) {
if (parameterTargetClass.isAssignableFrom(List.class)) {
Class<?> collectionParameterType = getCollectionParameterType(createMethodParameter(accessibleObject, i));
resolvedArguments[i] = evaluateToList(collectionParameterType, expression);
} else if (parameterTargetClass.isAssignableFrom(Map.class)) {
Class<?> mapKeyType = getMapKeyParameterType(createMethodParameter(accessibleObject, i));
Class<?> mapValueType = getMapValueParameterType(createMethodParameter(accessibleObject, i));
resolvedArguments[i] = evaluateToMap(mapKeyType, mapValueType, expression);
} else {
resolvedArguments[i] = processAndConvert(parameterTargetClass, expression);
}
} else {
if (defaultValue != null || !required) {
resolvedArguments[i] = defaultValue;
} else {
if(accessibleObject instanceof Constructor) {
Constructor constructor = (Constructor) accessibleObject;
throw new ConstrettoException("Missing value or default value for expression [" + expression + "], in annotated constructor in class [" + constructor.getClass().getName() + "], with tags " + currentTags + ".");
}
else {
Method method = (Method) accessibleObject;
throw new ConstrettoException("Missing value or default value for expression [" + expression + "], in method [" + method.getName() + "], in class [" + method.getClass().getName() + "], with tags " + currentTags + ".");
}
}
}
i++;
}
return resolvedArguments;
}
private <T> void injectMethods(T objectToConfigure) {
Method[] methods = objectToConfigure.getClass().getMethods();
for (Method method : methods) {
try {
if (method.isAnnotationPresent(Configure.class)) {
Object[] resolvedArguments = resolveParameters(method);
method.setAccessible(true);
method.invoke(objectToConfigure, resolvedArguments);
}
} catch (IllegalAccessException e) {
throw new ConstrettoException("Cold not invoke method ["
+ method.getName() + "] annotated with @Configured,", e);
} catch (InvocationTargetException e) {
throw new ConstrettoException("Cold not invoke method ["
+ method.getName() + "] annotated with @Configured,", e);
} catch (InstantiationException e) {
throw new ConstrettoException("Cold not invoke method ["
+ method.getName() + "] annotated with @Configured,", e);
}
}
}
private <T extends AccessibleObject> MethodParameter createMethodParameter(T accessibleObject, final int parameterIndex) {
if(accessibleObject instanceof Constructor) {
return new MethodParameter((Constructor) accessibleObject, parameterIndex);
} else {
return new MethodParameter((Method) accessibleObject, parameterIndex);
}
}
private <T> void injectFields(T objectToConfigure) {
Class objectToConfigureClass = objectToConfigure.getClass();
do {
Field[] fields = objectToConfigureClass.getDeclaredFields();
for (Field field : fields) {
try {
if (field.isAnnotationPresent(Configuration.class)) {
Configuration configurationAnnotation = field.getAnnotation(Configuration.class);
String expression = "".equals(configurationAnnotation.value()) ? field.getName() : configurationAnnotation.value();
field.setAccessible(true);
Class<?> fieldType = field.getType();
if (hasValue(expression)) {
ConfigurationValue node = findElementOrThrowException(expression);
if (fieldType.isAssignableFrom(List.class)) {
field.set(objectToConfigure, evaluateToList(getCollectionFieldType(field), expression));
} else if (fieldType.isAssignableFrom(Map.class)) {
field.set(objectToConfigure, evaluateToMap(getMapKeyFieldType(field), getMapValueFieldType(field), expression));
} else {
field.set(objectToConfigure, processAndConvert(fieldType, expression));
}
} else {
if (hasAnnotationDefaults(configurationAnnotation)) {
if (configurationAnnotation.defaultValueFactory().equals(Configuration.EmptyValueFactory.class)) {
field.set(objectToConfigure, ValueConverterRegistry.convert(fieldType, fieldType, new CPrimitive(configurationAnnotation.defaultValue())));
} else {
ConfigurationDefaultValueFactory valueFactory = configurationAnnotation.defaultValueFactory().newInstance();
field.set(objectToConfigure, valueFactory.getDefaultValue());
}
} else if (configurationAnnotation.required()) {
throw new ConstrettoException("Missing value or default value for expression [" + expression + "] for field [" + field.getName() + "], in class [" + objectToConfigure.getClass().getName() + "] with tags " + currentTags + ".");
}
}
} else if (field.isAnnotationPresent(Tags.class)) {
field.setAccessible(true);
field.set(objectToConfigure, currentTags);
}
} catch (IllegalAccessException e) {
throw new ConstrettoException("Cold not inject configuration into field ["
+ field.getName() + "] annotated with @Configuration, in class [" + objectToConfigure.getClass().getName() + "] with tags " + currentTags, e);
} catch (InstantiationException e) {
throw new ConstrettoException("Cold not inject configuration into field ["
+ field.getName() + "] annotated with @Configuration, in class [" + objectToConfigure.getClass().getName() + "] with tags " + currentTags, e);
}
}
} while ((objectToConfigureClass = objectToConfigureClass.getSuperclass()) != null);
}
private boolean hasAnnotationDefaults(Configuration configurationAnnotation) {
return !("N/A".equals(configurationAnnotation.defaultValue()) && configurationAnnotation.defaultValueFactory().equals(Configuration.EmptyValueFactory.class));
}
}