package silentium.commons.configuration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import silentium.commons.configuration.annotations.Property; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Objects; import java.util.Properties; /** * This class is designed to process classes and interfaces that have fields marked with {@link org.bnsworld.commons.configuration.annotations.Property} annotation * * @author SoulKeeper */ public class ConfigurableProcessor { private static final Logger log = LoggerFactory.getLogger(ConfigurableProcessor.class); /** * This method is an entry point to the parser logic.<br> * Any object or class that have {@link org.bnsworld.commons.configuration.annotations.Property} annotation in it or it's parent class/interface can be submitted * here.<br> * If object(new Something()) is submitted, object fields are parsed. (non-static)<br> * If class is submitted(Sotmething.class), static fields are parsed.<br> * <p/> * * @param object Class or Object that has {@link org.bnsworld.commons.configuration.annotations.Property} annotations. * @param properties Properties that should be used while seraching for a {@link org.bnsworld.commons.configuration.annotations.Property#key()} */ public static void process(Object object, final Properties... properties) { final Class clazz; if (object instanceof Class) { clazz = (Class) object; object = null; } else clazz = object.getClass(); process(clazz, object, properties); } /** * This method uses recurcieve calls to launch search for {@link org.bnsworld.commons.configuration.annotations.Property} annotation on itself and * parents\interfaces. * * @param clazz Class of object * @param obj Object if any, null if parsing class (static fields only) * @param props Properties with keys\values */ private static void process(final Class<?> clazz, final Object obj, final Properties... props) { processFields(clazz, obj, props); // Interfaces can't have any object fields, only static // So there is no need to parse interfaces for instances of objects // Only classes (static fields) can be located in interfaces if (obj == null) for (final Class<?> itf : clazz.getInterfaces()) process(itf, obj, props); final Class<?> superClass = clazz.getSuperclass(); if (!Objects.equals(superClass, Object.class)) process(superClass, obj, props); } /** * This method runs throught the declared fields watching for the {@link org.bnsworld.commons.configuration.annotations.Property} annotation. It also watches for * the field modifiers like {@link java.lang.reflect.Modifier#STATIC} and {@link java.lang.reflect.Modifier#FINAL} * * @param clazz Class of object * @param obj Object if any, null if parsing class (static fields only) * @param props Properties with keys\values */ private static void processFields(final Class<?> clazz, final Object obj, final Properties... props) { for (final Field field : clazz.getDeclaredFields()) { // Static fields should not be modified when processing object if (Modifier.isStatic(field.getModifiers()) && obj != null) continue; // Not static field should not be processed when parsing class if (!Modifier.isStatic(field.getModifiers()) && obj == null) continue; if (field.isAnnotationPresent(Property.class)) // Final fields should not be processed if (Modifier.isFinal(field.getModifiers())) { final RuntimeException re = new RuntimeException("Attempt to proceed final field " + field.getName() + " of class " + clazz.getName()); log.error(re.getLocalizedMessage(), re); throw re; } else processField(field, obj, props); } } /** * This method takes {@link Property} annotation and does sets value according to annotation property. For this * reason {@link #getFieldValue(java.lang.reflect.Field, java.util.Properties[])} can be called, however if method * sees that there is no need - field can remain with it's initial value. * <p/> * Also this method is capturing and logging all {@link Exception} that are thrown by underlying methods. * * @param field field that is going to be processed * @param obj Object if any, null if parsing class (static fields only) * @param props Properties with kyes\values */ private static void processField(final Field field, final Object obj, final Properties... props) { final boolean oldAccessible = field.isAccessible(); field.setAccessible(true); try { final Property property = field.getAnnotation(Property.class); if (!Property.DEFAULT_VALUE.equals(property.defaultValue()) || isKeyPresent(property.key(), props)) field.set(obj, getFieldValue(field, props)); else log.debug("Field " + field.getName() + " of class " + field.getDeclaringClass().getName() + " wasn't modified"); } catch (Exception e) { final RuntimeException re = new RuntimeException("Can't transform field " + field.getName() + " of class " + field.getDeclaringClass(), e); log.error(re.getLocalizedMessage(), re); throw re; } field.setAccessible(oldAccessible); } /** * This method is responsible for receiving field value.<br> * It tries to load property by key, if not found - it uses default value.<br> * Transformation is done using {@link org.bnsworld.commons.configuration.PropertyTransformerFactory} * * @param field field that has to be transformed * @param props properties with key\values * @return transformed object that will be used as field value * @throws TransformationException if something goes wrong during transformation */ private static Object getFieldValue(final Field field, final Properties... props) throws TransformationException { final Property property = field.getAnnotation(Property.class); final String defaultValue = property.defaultValue(); final String key = property.key(); String value = null; if (key.isEmpty()) log.warn("Property " + field.getName() + " of class " + field.getDeclaringClass().getName() + " has empty key"); else value = findPropertyByKey(key, props); if (value == null) { value = defaultValue; log.debug("Using default value for field " + field.getName() + " of class " + field.getDeclaringClass().getName()); } final PropertyTransformer<?> pt = PropertyTransformerFactory.newTransformer(field.getType(), property .propertyTransformer()); return pt.transform(value, field); } /** * Finds value by key in properties * * @param key value key * @param props properties to loook for the key * @return value if found, null otherwise */ private static String findPropertyByKey(final String key, final Properties... props) { for (final Properties p : props) if (p.containsKey(key)) return p.getProperty(key); return null; } /** * Checks if key is present in the given properties * * @param key key to check * @param props prperties to look for key * @return true if key present, false in other case */ private static boolean isKeyPresent(final String key, final Properties... props) { return findPropertyByKey(key, props) != null; } }