/* * (c) Copyright 2010-2011 AgileBirds * * This file is part of OpenFlexo. * * OpenFlexo 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. * * OpenFlexo 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 OpenFlexo. If not, see <http://www.gnu.org/licenses/>. * */ package org.openflexo.xmlcode; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Enumeration; import java.util.List; import java.util.StringTokenizer; import java.util.TreeSet; import java.util.Vector; /** * <p> * A KeyValue property represents a property, accessible directly by related field or through accessors methods (get/set pair for single * properties (see {@link SingleKeyValueProperty}), addTo../removeFrom.. pair for vector-like properties (see {@link VectorKeyValueProperty} * ), and set...ForKey/remove...WithKey for hashtable-like properties {@link HashtableKeyValueProperty}. * </p> * <p> * NB: to be valid, a KeyValueProperty object should be identified by at least the field or accessors methods. * </p> * <p> * <b>Important note:</b>If related field exist (is public) and accessors methods exists also, the operations are processed using accessors * (field won't be used directly). * </p> * * @author <a href="mailto:Sylvain.Guerin@enst-bretagne.fr">Sylvain Guerin</a> * @see KeyValueCoder * @see KeyValueDecoder * */ public abstract class KeyValueProperty { /** Stores date format */ public static final String dateFormat = "yyyy.MM.dd G 'at' HH:mm:ss a zzz"; /** Stores property's name */ protected String name; /** Stores related object (could be null) */ // protected Object object; /** Stores related object'class */ protected Class<?> objectClass; /** * Stores related field (if this one is public) or null if field is protected or non-existant */ protected Field field; /** Stores related type (the class of related property) */ protected Class<?> type; /** * Stores related "get" method (if this one is public) or null if method is protected or non-existant */ protected Method getMethod; /** * Stores related "set" method (if this one is public) or null if method is protected or non-existant */ protected Method setMethod; /** * Creates a new <code>KeyValueProperty</code> instance, given an object class.<br> * To be usable, this property should be set with a correct object (according to object class) * * @param anObject * an <code>Object</code> value * @param propertyName * a <code>String</code> value * @exception InvalidKeyValuePropertyException * if an error occurs */ public KeyValueProperty(Class<?> anObjectClass, String propertyName) throws InvalidKeyValuePropertyException { super(); objectClass = anObjectClass; } protected List<KeyValueProperty> compoundKeyValueProperties; protected boolean isCompound; /** * Initialize this property, given a propertyName.<br> * This method is called during constructor invokation. NB: to be valid, a property should be identified by at least the field or the * get/set methods pair. If the field is accessible, and only the get or the set method is accessible, a warning will be thrown. */ protected void init(String propertyName, boolean setMethodIsMandatory) throws InvalidKeyValuePropertyException { String lastName; Class<?> lastClass = null; name = propertyName; if (name.lastIndexOf(".") > -1) { isCompound = true; // System.out.println ("Register compound key-value property // "+propertyName); PathTokenizer st = new PathTokenizer(name); String nextKey = st.nextToken(); Class<?> nextClass = objectClass; compoundKeyValueProperties = new Vector<KeyValueProperty>(); while (st.hasMoreTokens()) { SingleKeyValueProperty skvp; if (ParameteredKeyValueProperty.isParameteredKeyValuePropertyPattern(nextKey)) { skvp = new ParameteredKeyValueProperty(nextClass, nextKey, false); } else { skvp = new SingleKeyValueProperty(nextClass, nextKey, false); } compoundKeyValueProperties.add(skvp); // System.out.println ("Register compound "+nextKey+" with // "+nextClass); nextKey = st.nextToken(); nextClass = skvp.getType(); } lastName = nextKey; lastClass = nextClass; } else { isCompound = false; lastName = propertyName; lastClass = objectClass; } String propertyNameWithFirstCharToUpperCase = lastName.substring(0, 1).toUpperCase() + lastName.substring(1, lastName.length()); field = null; try { field = lastClass.getField(lastName); } catch (NoSuchFieldException e) { // Debugging.debug ("NoSuchFieldException, trying to find get/set // methods pair"); } catch (SecurityException e) { // Debugging.debug ("SecurityException, trying to find get/set // methods pair"); } getMethod = searchMatchingGetMethod(lastClass, lastName); if (field == null) { if (getMethod == null) { throw new InvalidKeyValuePropertyException("No public field " + lastName + " found, nor method matching " + lastName + "() nor " + "_" + lastName + "() nor " + "get" + propertyNameWithFirstCharToUpperCase + "() nor " + "_get" + propertyNameWithFirstCharToUpperCase + "() found in class:" + lastClass.getName()); } else { type = getMethod.getReturnType(); } } else { // field != null type = field.getType(); if (getMethod != null) { if (getMethod.getReturnType() != type) { Debugging.warn("Class " + objectClass + " Public field " + lastName + " found, with type " + type.getName() + " found " + " and method " + getMethod.getName() + " found " + " declaring return type " + getMethod.getReturnType() + " Ignoring method..."); getMethod = null; } } } setMethod = searchMatchingSetMethod(lastClass, lastName, type); if (setMethodIsMandatory) { if (setMethod == null) { if (field == null) { throw new InvalidKeyValuePropertyException("No public field " + lastName + " found, nor method matching " + "set" + propertyNameWithFirstCharToUpperCase + "(" + type.getName() + ") or " + "_set" + propertyNameWithFirstCharToUpperCase + "(" + type.getName() + ") found " + "in class " + lastClass); } else { if (getMethod != null) { // Debugging.debug ("Public field "+propertyName+" // found, with type " // + type.getName()+ " found " // + " and method "+getMethod.getName()+" found " // + " but no method matching " // +"set"+propertyNameWithFirstCharToUpperCase+"("+type.getName()+") // or " // +"_set"+propertyNameWithFirstCharToUpperCase+"("+type.getName()+") // found." // +" Will use directly the field to set values."); } } } } if (getMethod != null && setMethod != null) { // If related field exist (is public) and accessors methods exists // also, // the operations are processed using accessors (field won't be used // directly, // and should be set to null). field = null; } } /** * Try to find a matching "get" method, such as (in order): * <ul> * <li>propertyName()</li> * <li>_propertyName()</li> * <li>getPropertyName()</li> * <li>_getPropertyName()</li> * </ul> * Returns corresponding method, null if no such method exist */ protected Method searchMatchingGetMethod(Class<?> lastClass, String propertyName) { String propertyNameWithFirstCharToUpperCase = propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1, propertyName.length()); String[] tries = new String[5]; tries[0] = "get" + propertyNameWithFirstCharToUpperCase; tries[1] = propertyName; tries[2] = "_" + propertyName; tries[3] = "_get" + propertyNameWithFirstCharToUpperCase; tries[4] = "is" + propertyNameWithFirstCharToUpperCase; for (String trie : tries) { try { return lastClass.getMethod(trie, (Class<?>[]) null); } catch (SecurityException err) { // we continue } catch (NoSuchMethodException err) { // we continue } } // Debugging.debug ("No method matching " // +propertyName+"() or " // +"_"+propertyName+"() or " // +"get"+propertyNameWithFirstCharToUpperCase+"() or " // +"_get"+propertyNameWithFirstCharToUpperCase+"() found."); return null; } /** * Try to find a matching "set" method, such as (in order): * <ul> * <li>setPropertyName(Type)</li> * <li>_setPropertyName(Type)</li> * </ul> * Returns corresponding method, null if no such method exist */ protected Method searchMatchingSetMethod(Class<?> lastClass, String propertyName, Class<?> aType) { String propertyNameWithFirstCharToUpperCase = propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1, propertyName.length()); Class<?> params[] = new Class[1]; params[0] = aType; String[] tries; if ((aType == Boolean.class || aType == boolean.class) && propertyName.startsWith("is")) { tries = new String[4]; String propertyNameWithFirstCharToUpperCase2 = propertyName.substring(2); tries[2] = "set" + propertyNameWithFirstCharToUpperCase2; tries[3] = "_set" + propertyNameWithFirstCharToUpperCase2; } else { tries = new String[2]; } tries[0] = "set" + propertyNameWithFirstCharToUpperCase; tries[1] = "_set" + propertyNameWithFirstCharToUpperCase; for (int i = 0; i < tries.length; i++) { try { return lastClass.getMethod(tries[i], params); } catch (SecurityException err) { // we continue } catch (NoSuchMethodException err) { // we continue } } // Debugging.debug ("No method matching " // +"set"+propertyNameWithFirstCharToUpperCase+"("+type.getName()+") or // " // +"_set"+propertyNameWithFirstCharToUpperCase+"("+type.getName()+") // found."); if (aType.getSuperclass() != null) { // Try with a super class return searchMatchingSetMethod(lastClass, propertyName, aType.getSuperclass()); } return null; } /** * Stores related "get" method (if this one is public) or null if method is protected/private or non-existant */ public Method getGetMethod() { return getMethod; } /** * Stores related "set" method (if this one is public) or null if method is protected/private or non-existant */ public Method getSetMethod() { return setMethod; } /** * Returns name of this property */ public String getName() { return name; } /** * Sets related object, asserting that the type is correct according to property type. Don't forget to encapsulate your calls within a * synchronized block/method on this KeyValueProperty object. */ public void setObject(Object anObject) { if (!objectClass.isAssignableFrom(anObject.getClass())) { throw new InvalidKeyValuePropertyException("Invalid object type, expected: " + objectClass.getName()); } } /** * Returns related object class (never null) */ public Class<?> getObjectClass() { return objectClass; } /** * Returns related field (if this one is public) or null if field is protected or non-existant */ public Field getField() { return field; } /** * Returns related type */ public Class<?> getType() { return type; } /** * Search and returns all methods (as {@link AccessorMethod} objects) of related class whose names is in the specified string list, with * exactly the specified number of parameters, ascendant ordered regarding parameters specialization. * * @see AccessorMethod */ protected TreeSet<AccessorMethod> searchMethodsWithNameAndParamsNumber(String[] searchedNames, int paramNumber) { TreeSet<AccessorMethod> returnedTreeSet = new TreeSet<AccessorMethod>(); Method[] allMethods = objectClass.getMethods(); for (int i = 0; i < allMethods.length; i++) { Method tempMethod = allMethods[i]; for (int j = 0; j < searchedNames.length; j++) { if (tempMethod.getName().equalsIgnoreCase(searchedNames[j]) && tempMethod.getParameterTypes().length == paramNumber) { // This is a good candidate returnedTreeSet.add(new AccessorMethod(this, tempMethod)); } } } // Debugging.debug ("Class "+objectClass.getName()+": found " // +returnedTreeSet.size()+" accessors:"); // for (Iterator i = returnedTreeSet.iterator(); i.hasNext();) { // Debugging.debug ("> "+((AccessorMethod)i.next()).getMethod()); // } return returnedTreeSet; } /** * Returns Object value, asserting that this property represents an Object property (if not, throw an InvalidKeyValuePropertyException * exception) * * @return an <code>Object</code> value * @exception InvalidKeyValuePropertyException * if an error occurs */ public synchronized Object getObjectValue(Object object) { return getObjectValue(object, object); } /** * Returns Object value, asserting that this property represents an Object property (if not, throw an InvalidKeyValuePropertyException * exception) * * @return an <code>Object</code> value * @exception InvalidKeyValuePropertyException * if an error occurs */ protected synchronized Object getObjectValue(Object object, Object initialObject) { if (object == null) { throw new InvalidKeyValuePropertyException("No object is specified"); } else { Object currentObject = object; if (isCompound) { for (KeyValueProperty p : compoundKeyValueProperties) { if (currentObject != null) { currentObject = p.getObjectValue(currentObject, initialObject); } } if (currentObject == null) { return null; } } if (field != null) { try { return field.get(currentObject); } catch (Exception e) { throw new InvalidKeyValuePropertyException("InvalidKeyValuePropertyException: class " + getObjectClass().getName() + ": field " + field.getName() + " Exception raised: " + e.toString()); } } else if (getMethod != null) { try { return getMethod.invoke(currentObject, (Object[]) null); } catch (InvocationTargetException e) { e.getTargetException().printStackTrace(); throw new AccessorInvocationException("AccessorInvocationException: class " + getObjectClass().getName() + ": method " + getMethod.getName() + " Exception raised: " + e.getTargetException().toString(), e); } catch (Exception e) { throw new InvalidKeyValuePropertyException("InvalidKeyValuePropertyException: class " + getObjectClass().getName() + ": method " + getMethod.getName() + " Exception raised: " + e.toString()); } } else { throw new InvalidKeyValuePropertyException("InvalidKeyValuePropertyException: no field nor get method found !!!"); } } } /** * Sets Object value, asserting that this property represents an Object property (if not, throw an InvalidKeyValuePropertyException * exception) * * @param aValue * an <code>Object</code> value * @exception InvalidKeyValuePropertyException * if an error occurs */ /** * Sets Object value, asserting that this property represents an Object property (if not, throw an InvalidKeyValuePropertyException * exception) * * @param aValue * an <code>Object</code> value * @exception InvalidKeyValuePropertyException * if an error occurs */ public synchronized void setObjectValue(Object aValue, Object object) { setObjectValue(aValue, object, object); } /** * Sets Object value, asserting that this property represents an Object property (if not, throw an InvalidKeyValuePropertyException * exception) * * @param aValue * an <code>Object</code> value * @exception InvalidKeyValuePropertyException * if an error occurs */ /** * Sets Object value, asserting that this property represents an Object property (if not, throw an InvalidKeyValuePropertyException * exception) * * @param aValue * an <code>Object</code> value * @exception InvalidKeyValuePropertyException * if an error occurs */ public synchronized void setObjectValue(Object aValue, Object object, Object initialObject) { if (object == null) { throw new InvalidKeyValuePropertyException("No object is specified"); } else { Object currentObject = object; if (isCompound) { for (KeyValueProperty p : compoundKeyValueProperties) { if (currentObject != null) { currentObject = p.getObjectValue(currentObject, initialObject); } } if (currentObject == null) { return; } } if (field != null) { try { field.set(currentObject, aValue); } catch (Exception e) { throw new InvalidKeyValuePropertyException("InvalidKeyValuePropertyException: class " + getObjectClass().getName() + ": field " + field.getName() + " Exception raised: " + e.toString()); } } else if (setMethod != null) { Object params[] = new Object[1]; params[0] = aValue; try { setMethod.invoke(currentObject, params); } catch (InvocationTargetException e) { // e.getTargetException().printStackTrace(); throw new AccessorInvocationException("AccessorInvocationException: class " + getObjectClass().getName() + ": method " + setMethod.getName() + " Exception raised: " + e.getTargetException().toString(), e); } catch (IllegalArgumentException e) { // e.printStackTrace(); throw new InvalidKeyValuePropertyException("InvalidKeyValuePropertyException: class " + getObjectClass().getName() + ": method " + setMethod.getName() + "Argument mismatch: tried to pass a '" + aValue.getClass().getName() + " instead of a " + setMethod.getParameterTypes()[0] + " Exception raised: " + e.toString()); } catch (Exception e) { // e.printStackTrace(); throw new InvalidKeyValuePropertyException("InvalidKeyValuePropertyException: class " + getObjectClass().getName() + ": field " + setMethod.getName() + " Exception raised: " + e.toString()); } } else { throw new InvalidKeyValuePropertyException("InvalidKeyValuePropertyException: no field nor set method found !!!"); } } } public static class PathTokenizer { private Vector<String> _tokens; private Enumeration<String> enumeration; public PathTokenizer(String value) { super(); _tokens = new Vector<String>(); StringTokenizer st = new StringTokenizer(value, ".()", true); String current = ""; int level = 0; while (st.hasMoreElements()) { String next = st.nextToken(); if (next.equals(".") && current.trim().length() > 0 && level == 0) { _tokens.add(current); current = ""; } else if (next.equals("(")) { current += next; level++; } else if (next.equals(")")) { current += next; level--; } else { current += next; } } if (current.trim().length() > 0 && level == 0) { _tokens.add(current); current = ""; } enumeration = _tokens.elements(); } public boolean hasMoreTokens() { return enumeration.hasMoreElements(); } public String nextToken() { String returned = enumeration.nextElement(); return returned; } } public static boolean isBoolean(Type type) { return type.equals(Boolean.class) || type.equals(Boolean.TYPE); } public static boolean isByte(Type type) { return type.equals(Byte.class) || type.equals(Byte.TYPE); } public static boolean isChar(Type type) { return type.equals(Character.class) || type.equals(Character.TYPE); } public static boolean isClassAncestorOf(Class<?> parentClass, Class<?> childClass) { return parentClass.isAssignableFrom(childClass); } public static boolean isDouble(Type type) { return type.equals(Double.class) || type.equals(Double.TYPE); } public static boolean isFloat(Type type) { return type.equals(Float.class) || type.equals(Float.TYPE); } public static boolean isInteger(Type type) { return type.equals(Integer.class) || type.equals(Integer.TYPE); } public static boolean isLong(Type type) { return type.equals(Long.class) || type.equals(Long.TYPE); } public static boolean isObject(Type type) { return type.equals(Object.class); } public static boolean isShort(Type type) { return type.equals(Short.class) || type.equals(Short.TYPE); } public static boolean isString(Type type) { return type.equals(String.class); } public static boolean isVoid(Type type) { return type.equals(Void.class) || type.equals(Void.TYPE); } }