package org.radargun.config;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.radargun.logging.Log;
import org.radargun.logging.LogFactory;
import org.radargun.utils.MapEntry;
/**
* Helper for retrieving properties from the class
*
* @author Radim Vansa <rvansa@redhat.com>
*/
public class PropertyHelper {
private static Log log = LogFactory.getLog(PropertyHelper.class);
private static final Comparator<Map.Entry<String, Path>> MAP_ENTRY_KEY_COMPARATOR = new Comparator<Map.Entry<String, Path>>() {
@Override
public int compare(Map.Entry<String, Path> o1, Map.Entry<String, Path> o2) {
return o1.getKey().compareTo(o2.getKey());
}
};
private PropertyHelper() {
}
/**
* Retrieve all properties from this class and all its superclasses.
*
* @param clazz
* @param useDashedName Convert property names to dashed form - e.g. myPropertyName becomes my-property-name.
* @param includeDelegates Include also those tagged with {@link PropertyDelegate}.
* @param includeAliases Include alternative names of the properties.
* @return Map of names of the properties (either dashed or camel cased) to {@link Path paths} in the object graph starting from the given class.
*/
public static Map<String, Path> getProperties(Class<?> clazz, boolean useDashedName, boolean includeDelegates, boolean includeAliases) {
ArrayList<Map.Entry<String, Path>> properties = new ArrayList<>();
addProperties(clazz, properties, useDashedName, includeDelegates, includeAliases, "", null);
// TODO: when there are two delegates with empty prefix, one of them won't be returned!
TreeMap<String, Path> props = new TreeMap<>();
for (Map.Entry<String, Path> entry : properties) {
props.put(entry.getKey(), entry.getValue());
}
return props;
}
/**
* Retrieve all properties from this class (not including its superclasses).
*
* @param clazz
* @return Map of names of the properties (either dashed or camel cased) to {@link Path paths}
* in the object graph starting from the given class.
*/
public static Collection<Map.Entry<String, Path>> getDeclaredProperties(Class<?> clazz, boolean includeDelegates, boolean includeAliases) {
ArrayList<Map.Entry<String, Path>> properties = new ArrayList<>();
addDeclaredProperties(clazz, properties, false, includeDelegates, includeAliases, "", null);
Collections.sort(properties, MAP_ENTRY_KEY_COMPARATOR);
return properties;
}
/**
* Retrieve string representation of property's value on the source object.
* @param path
* @param source Object where the paths start.
* @return String representation of the value (as retrieved from its converter).
*/
public static String getPropertyString(Path path, Object source) {
Object value = null;
try {
value = path.get(source);
Constructor<? extends Converter<?>> ctor = path.getTargetAnnotation().converter().getDeclaredConstructor();
ctor.setAccessible(true);
Converter converter = ctor.newInstance();
return converter.convertToString(value);
} catch (IllegalAccessException e) {
return "<not accessible>";
} catch (InstantiationException e) {
return "<cannot create converter: " + value + ">";
} catch (ClassCastException e) {
return "<cannot convert: " + value + ">";
} catch (Throwable t) {
return "<error " + t + ": " + value + ">";
}
}
private static String getPropertyName(Field property, Property annotation) {
return annotation.name().equals(Property.FIELD_NAME) ? property.getName() : annotation.name();
}
private static void addProperties(Class<?> clazz, Collection<Map.Entry<String, Path>> properties, boolean useDashedName, boolean includeDelegates, boolean includeAliases, String prefix, Path path) {
if (clazz == null) return;
addDeclaredProperties(clazz, properties, useDashedName, includeDelegates, includeAliases, prefix, path);
addProperties(clazz.getSuperclass(), properties, useDashedName, includeDelegates, includeAliases, prefix, path);
}
private static void addDeclaredProperties(Class<?> clazz, Collection<Map.Entry<String, Path>> properties, boolean useDashedName, boolean includeDelegates, boolean includeAliases, String prefix, Path path) {
for (Field field : clazz.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers())) continue; // property cannot be static
Property property = field.getAnnotation(Property.class);
PropertyDelegate delegate = field.getAnnotation(PropertyDelegate.class);
if (property != null && delegate != null) {
// TODO: this is not necessary, but setting more fields from one property would be complicated
throw new IllegalArgumentException(String.format("Field %s.%s cannot be declared with both @Property and @PropertyDelegate", clazz.getName(), field.getName()));
}
Path newPath = path == null ? new Path(field) : path.with(field);
if (property != null) {
String name = prefix + getPropertyName(field, property);
if (useDashedName) {
name = XmlHelper.camelCaseToDash(name);
}
newPath.setComplete(true);
properties.add(new MapEntry<>(name, newPath));
if (includeAliases) {
String deprecatedName = property.deprecatedName();
if (!deprecatedName.equals(Property.NO_DEPRECATED_NAME)) {
if (useDashedName) {
deprecatedName = XmlHelper.camelCaseToDash(deprecatedName);
}
properties.add(new MapEntry<>(deprecatedName, newPath));
}
}
}
if (delegate != null) {
String delegatePrefix = useDashedName ? XmlHelper.camelCaseToDash(delegate.prefix()) : delegate.prefix();
if (includeDelegates) {
int i = delegatePrefix.length();
for (; i > 0; --i) {
if (Character.isLetterOrDigit(delegatePrefix.charAt(i - 1))) break;
}
properties.add(new MapEntry<>(delegatePrefix.substring(0, i), newPath));
}
// TODO: delegate properties are added according to field type, this does not allow polymorphism
addProperties(field.getType(), properties, useDashedName,
includeDelegates, includeAliases, prefix + delegatePrefix, newPath);
}
}
}
/**
* Copy the values of identical properties from source to destination. No evaluation or conversion occurs.
*
* @param source
* @param destination
*/
public static void copyProperties(Object source, Object destination) {
Map<String, Path> sourceProperties = getProperties(source.getClass(), false, false, false);
Map<String, Path> destProperties = getProperties(destination.getClass(), false, false, true);
for (Map.Entry<String, Path> property : sourceProperties.entrySet()) {
Path destPath = destProperties.get(property.getKey());
if (destPath == null) {
log.trace("Property " + property.getKey() + " not found on destination, skipping");
continue;
}
try {
destPath.set(destination, property.getValue().get(source));
} catch (IllegalAccessException e) {
log.errorf(e, "Failed to copy %s (%s) to %s.%s (%s)",
property.getValue(), source, destPath, destination);
}
}
}
/**
* Set properties on the target object using values from the propertyMap.
* The keys in propertyMap use property name, values are evaluated and converted here.
*
* @param target The modified object.
* @param propertyMap Source of the data, not evaluated
* @param ignoreMissingProperty If the property is not found on the target object, should we throw and exception?
* @param useDashedName Expect that the property names in propertyMap use the dashed form.
*/
public static void setProperties(Object target, Map<String, String> propertyMap, boolean ignoreMissingProperty, boolean useDashedName) {
Class targetClass = target.getClass();
Map<String, Path> properties = getProperties(target.getClass(), useDashedName, false, true);
for (Map.Entry<String, String> entry : propertyMap.entrySet()) {
String propName = entry.getKey();
Path path = properties.get(propName);
if (path != null) {
Property propertyAnnotation = path.getTargetAnnotation();
if (propertyAnnotation.readonly()) {
throw new IllegalArgumentException("Property " + propName + " -> " + path + " is readonly and therefore cannot be set!");
}
setPropertyFromString(target, propName, path, entry.getValue());
continue;
}
if (ignoreMissingProperty) {
log.trace("Property " + propName + " could not be set on class [" + targetClass + "]");
} else {
throw new IllegalArgumentException("Couldn't find a property for parameter " + propName + " on class [" + targetClass + "]");
}
}
}
public static void setPropertiesFromDefinitions(Object target, Map<String, Definition> propertyMap, Map<String, String>... extras) {
Map<String, String> backups = new HashMap<>();
for (Map<String, String> extra : extras) {
backupSystemProperties(extra.keySet(), backups);
}
for (Map<String, String> extra : extras) {
setSystemProperties(extra);
}
PropertyHelper.setPropertiesFromDefinitions(target, propertyMap, false, true);
setSystemProperties(backups);
}
private static void setSystemProperties(Map<String, String> properties) {
for (Map.Entry<String, String> property : properties.entrySet()) {
System.setProperty(property.getKey(), property.getValue() == null ? "" : property.getValue());
}
}
private static void backupSystemProperties(Set<String> properties, Map<String, String> backups) {
for (String property : properties) {
backups.put(property, System.getProperty(property));
}
}
/**
* Set properties on the target object using values from the propertyMap.
*
* @param target The modified object.
* @param propertyMap Map of property names to the (possibly complex) definitions.
* @param ignoreMissingProperty If the property is not found on the target object, should we throw and exception?
* @param useDashedName Expect that the property names in propertyMap use the dashed form.
*/
public static void setPropertiesFromDefinitions(Object target, Map<String, Definition> propertyMap, boolean ignoreMissingProperty, boolean useDashedName) {
Class targetClass = target.getClass();
Map<String, Path> properties = getProperties(target.getClass(), useDashedName, true, true);
for (Map.Entry<String, Definition> entry : propertyMap.entrySet()) {
String propName = entry.getKey();
Path path = properties.get(propName);
if (path != null) {
if (!path.isComplete()) {
try {
if (entry.getValue() instanceof SimpleDefinition) {
setProperties(path.get(target), Collections.singletonMap("", ((SimpleDefinition) entry.getValue()).value), ignoreMissingProperty, useDashedName);
} else if (entry.getValue() instanceof ComplexDefinition) {
setPropertiesFromDefinitions(path.get(target), ((ComplexDefinition) entry.getValue()).getAttributeMap(), ignoreMissingProperty, useDashedName);
} else throw new IllegalArgumentException("Unknown definition type: " + entry.getValue());
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Failed to set " + propName + " on " + target, e);
}
continue;
}
Property propertyAnnotation = path.getTargetAnnotation();
if (propertyAnnotation.readonly()) {
throw new IllegalArgumentException("Property " + propName + " -> " + path + " is readonly and therefore cannot be set!");
}
if (entry.getValue() instanceof SimpleDefinition) {
setPropertyFromString(target, propName, path, ((SimpleDefinition) entry.getValue()).value);
} else if (entry.getValue() instanceof ComplexDefinition) {
Class<? extends ComplexConverter<?>> converterClass = propertyAnnotation.complexConverter();
try {
Constructor<? extends ComplexConverter> ctor = converterClass.getDeclaredConstructor();
ctor.setAccessible(true);
ComplexConverter converter = ctor.newInstance();
path.set(target, converter.convert((ComplexDefinition) entry.getValue(), path.getTargetGenericType()));
} catch (InstantiationException e) {
log.errorf(e, "Cannot instantiate converter %s for setting %s (%s)",
converterClass.getName(), path, propName);
throw new IllegalArgumentException(e);
} catch (IllegalAccessException e) {
log.errorf(e, "Cannot access converter %s for setting %s (%s)",
converterClass.getName(), path, propName);
throw new IllegalArgumentException(e);
} catch (Throwable t) {
log.error("Failed to convert definition " + entry.getValue(), t);
throw new IllegalArgumentException(t);
}
} else {
throw new IllegalArgumentException("Unknown definition type: " + entry.getValue());
}
continue;
}
if (ignoreMissingProperty) {
log.trace("Property " + propName + " could not be set on class [" + targetClass + "]");
} else {
throw new IllegalArgumentException("Couldn't find a property for parameter " + propName + " on class [" + targetClass + "]");
}
}
}
private static void setPropertyFromString(Object target, String propName, Path path, String propertyString) {
Class<? extends Converter> converterClass = path.getTargetAnnotation().converter();
String evaluated = null;
try {
Constructor<? extends Converter> ctor = converterClass.getDeclaredConstructor();
ctor.setAccessible(true);
Converter converter = ctor.newInstance();
evaluated = Evaluator.parseString(propertyString);
log.tracef("Evaluated property %s to %s", propName, evaluated);
path.set(target, converter.convert(evaluated, path.getTargetGenericType()));
} catch (InstantiationException e) {
log.errorf(e, "Cannot instantiate converter %s for setting %s (%s)",
converterClass.getName(), path, propName);
throw new IllegalArgumentException(e);
} catch (IllegalAccessException e) {
log.errorf(e, "Cannot access converter %s for setting %s (%s)",
converterClass.getName(), path, propName);
throw new IllegalArgumentException(e);
} catch (Throwable t) {
log.errorf(t, "Failed to convert value '%s' evaluated to '%s'", propertyString, evaluated);
throw new IllegalArgumentException(t);
}
}
/**
* Write all properties of this object to string.
*
* @param target
* @return String in form ' {property1=value1, property2=value2, ... }'
*/
public static String toString(Object target) {
StringBuilder sb = new StringBuilder(" {");
Set<Map.Entry<String, Path>> properties = PropertyHelper.getProperties(target.getClass(), false, false, false).entrySet();
for (Iterator<Map.Entry<String, Path>> iterator = properties.iterator(); iterator.hasNext(); ) {
Map.Entry<String, Path> property = iterator.next();
String propertyName = property.getKey();
Path path = property.getValue();
sb.append(propertyName).append('=');
sb.append(PropertyHelper.getPropertyString(path, target));
if (iterator.hasNext()) {
sb.append(", ");
}
}
return sb.append(" }").toString();
}
/**
* @param clazz
* @return Name used in the {@link org.radargun.config.DefinitionElement} annotation, or simple name.
*/
public static String getDefinitionElementName(Class<?> clazz) {
DefinitionElement de = clazz.getAnnotation(DefinitionElement.class);
return de != null ? de.name() : clazz.getSimpleName();
}
}