/* * This file is part of aion-emu <aion-emu.com>. * * aion-emu is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * aion-emu is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with aion-emu. If not, see <http://www.gnu.org/licenses/>. */ package com.aionemu.commons.configuration; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Properties; import org.apache.log4j.Logger; /** * This class is designed to process classes and interfaces that have fields marked with {@link Property} annotation * * @author SoulKeeper */ public class ConfigurableProcessor { /** * Logger */ private static final Logger log = Logger.getLogger(ConfigurableProcessor.class); /** * This method is an entry point to the parser logic.<br> * Any object or class that have {@link 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 Property} annotations. * @param properties * Properties that should be used while seraching for a {@link Property#key()} */ @SuppressWarnings("unchecked") public static void process(Object object, Properties... properties) { 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 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 */ @SuppressWarnings("unchecked") private static void process(Class clazz, Object obj, 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(Class itf : clazz.getInterfaces()) { process(itf, obj, props); } } Class superClass = clazz.getSuperclass(); if(superClass != null && superClass != Object.class) { process(superClass, obj, props); } } /** * This method runs throught the declared fields watching for the {@link 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 */ @SuppressWarnings("unchecked") private static void processFields(Class clazz, Object obj, Properties[] props) { for(Field f : clazz.getDeclaredFields()) { // Static fields should not be modified when processing object if(Modifier.isStatic(f.getModifiers()) && obj != null) { continue; } // Not static field should not be processed when parsing class if(!Modifier.isStatic(f.getModifiers()) && obj == null) { continue; } if(f.isAnnotationPresent(Property.class)) { // Final fields should not be processed if(Modifier.isFinal(f.getModifiers())) { RuntimeException re = new RuntimeException("Attempt to proceed final field " + f.getName() + " of class " + clazz.getName()); log.error(re); throw re; } else { processField(f, 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 f * 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(Field f, Object obj, Properties[] props) { boolean oldAccessible = f.isAccessible(); f.setAccessible(true); try { Property property = f.getAnnotation(Property.class); if(!Property.DEFAULT_VALUE.equals(property.defaultValue()) || isKeyPresent(property.key(), props)) { f.set(obj, getFieldValue(f, props)); } else if(log.isDebugEnabled()) { log.debug("Field " + f.getName() + " of class " + f.getDeclaringClass().getName() + " wasn't modified"); } } catch(Exception e) { RuntimeException re = new RuntimeException("Can't transform field " + f.getName() + " of class " + f.getDeclaringClass(), e); log.error(re); throw re; } f.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 com.aionemu.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 */ @SuppressWarnings("unchecked") private static Object getFieldValue(Field field, Properties[] props) throws TransformationException { Property property = field.getAnnotation(Property.class); String defaultValue = property.defaultValue(); 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; if(log.isDebugEnabled()) { log.debug("Using default value for field " + field.getName() + " of class " + field.getDeclaringClass().getName()); } } 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(String key, Properties[] props) { for(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(String key, Properties[] props) { return findPropertyByKey(key, props) != null; } }