/* * Rapid Beans Framework: Property.java * * Copyright (C) 2009 Martin Bluemel * * Creation Date: 11/22/2005 * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation; * either version 3 of the License, or (at your option) any later version. * This program 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 Lesser General Public License for more details. * You should have received a copies of the GNU Lesser General Public License and the * GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>. */ package org.rapidbeans.core.basic; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.MissingResourceException; import org.rapidbeans.core.common.RapidBeansLocale; import org.rapidbeans.core.common.ThreadLocalProperties; import org.rapidbeans.core.event.PropertyChangeEvent; import org.rapidbeans.core.event.PropertyChangeEventType; import org.rapidbeans.core.exception.RapidBeansRuntimeException; import org.rapidbeans.core.exception.ValidationInstanceAssocTwiceException; import org.rapidbeans.core.exception.ValidationMandatoryException; import org.rapidbeans.core.exception.ValidationReadonlyException; import org.rapidbeans.core.type.TypeProperty; import org.rapidbeans.core.type.TypePropertyCollection; import org.rapidbeans.core.type.TypeRapidBean; import org.rapidbeans.core.util.ClassHelper; import org.rapidbeans.core.util.StringHelper; /** * The base class for every bean property. Bean properties encapsulate value * objects and come with the following features:<br/> * - They can be accessed in a generic manner (Object getValue(), void * setValue(Object))<br/> * - New property values are always validated before being set event if they're * set internally<br/> * - Every property can be converted from (parse) or to (format) a normal string * representation<br/> * - They ensure immutability in case the value objects are mutable (for example * date)<br/> * <br/> * The following basic property types are supported: <li><b> <code>association (end)</code></b> a collection of references to other beans (Links)</li> <li><b><code>boolean    </code></b>a boolean value ('true' or 'false')</li> <li><b> <code>choice     </code></b>(multiple) choice of enum entries (RapidEnum)</li> <li><b> <code>date       </code></b>a date</li> <li><b> <code>decimal (not yet implemented)    </code> * </b>numeric value stored in decimal format (BigDecimal)</li> <li><b> <code>file      </code></b>File or Directory</li> <li> * <b><code>float (not yet implemented)      </code> </b>numeric value stored in floating point format (double)</li> <li><b> <code>integer    </code></b>numeric value stored in integer format (long)</li> <li><b><code>quantity   </code></b>quantity consisting in magnitude (Number) and unit (RapidEnum)</li> <li><b> <code>string     </code></b> a (Java) string consisting of Unicode characters (internal encoding: UTF-16)</li> <br/> * <br/> * Please note the this schema could be extended in the future and can be also * extended by yourself. That means you can invent, implement and use your own * property types deriving one of these basic property types. <br/> * <br/> * * @see RapidBean * * @author Martin Bluemel */ public abstract class Property implements Cloneable, Comparable<Property> { /** * the property's type. */ private TypeProperty type = null; /** * @return the property's type. */ public final TypeProperty getType() { return this.type; } /** * Simple and very convenient. * * @return the property name. */ public final String getName() { return this.getType().getPropName(); } /** * the parent bean. */ private RapidBean bean = null; /** * @return the parent bean. */ public RapidBean getBean() { return this.bean; } /** * @return the Property's String value. */ public abstract Object getValue(); /** * set the Property's value. * * @param value * the value object. */ public abstract void setValue(final Object value); /** * every subclass must deliver it's specific implementation of toString(). * * @return the String representation of the property's value */ public abstract String toString(); /** * constructor for a new Property object. * * @param propType * the Property type * @param parentBean * the parent bean */ protected Property(final TypeProperty propType, final RapidBean parentBean) { this.bean = parentBean; this.type = propType; if (!(this.bean instanceof RapidBeanImplSimple)) { try { ThreadLocalValidationSettings.mandatoryOff(); ThreadLocalValidationSettings.readonlyOff(); if (!isDependent()) { setValue(propType.getDefaultValue()); } } finally { ThreadLocalValidationSettings.remove(); } } } /** * constant. */ private static final Class<?>[] CONSTRUCTOR_PARAM_TYPES = { TypeProperty.class, RapidBean.class }; /** * factory method for a property with given typeinfo (metainfo). * * @param type * the type info (constraints) for this property * @param parentBean * the parent bean * * @return the created Property instance * <p> * throws <b>RapidBeansRuntimeException:</b> thrown if creation fails * </p> */ public static Property createInstance(final TypeProperty type, final RapidBean parentBean) { Property prop = null; Class<?> propClass = type.getPropClass(); // construct property via reflection try { Constructor<?> constructor = propClass.getConstructor(CONSTRUCTOR_PARAM_TYPES); Object[] oa = new Object[2]; oa[0] = type; oa[1] = parentBean; prop = (Property) constructor.newInstance(oa); } catch (NoSuchMethodException e) { throw new RapidBeansRuntimeException(e); } catch (IllegalAccessException e) { throw new RapidBeansRuntimeException(e); } catch (InstantiationException e) { throw new RapidBeansRuntimeException(e); } catch (InvocationTargetException e) { throw new RapidBeansRuntimeException(e); } return prop; } /** * generic value converter. * * @param value * the value object to convert. Every property has to support at * least the internal representation or if a primitive type the * corresponding value object and String (for serialization). * * @return the converted value which is the internal representation or if a * primitive type the corresponding value object */ public abstract Object convertValue(Object value); /** * Check if the property's (initial) value is valid. Remember that * properties never are allowed to get invalid values. */ public void validate() { validate(getValue()); } /** * The basic property validation. Should be called with super.validate by * every overloaded validate method.<br> * - checks a write lock (read only)<br> * - check if an empty (null) value is to be written to a key or mandatory * property<br> * - delegate validation to all components. * * @param newValue * the value object to validate */ public Object validate(final Object newValue) { final Object newValConverted = this.convertValue(newValue); if (!ThreadLocalValidationSettings.getValidation()) { return newValConverted; } // check if the property may be written // System.out.println("@@@ readonly = " + this.getReadonly()); // System.out.println("@@@ check = " + // ThreadLocalValidationSettings.getReadonlyCheck()); if (this.getReadonly() && ThreadLocalValidationSettings.getReadonlyCheck()) { throw new ValidationReadonlyException("invalid.prop.readonly.proptype", this, "Property \"" + this.getName() + "\" must not be written at all."); } // check if a null (= not defined) value is // to be written to a mandatory property if (newValConverted == null && this.type.getMandatory() && ThreadLocalValidationSettings.getMandatoryCheck()) { if (this.getBean() != null) { throw new ValidationMandatoryException("invalid.prop.mandatory", this, "Bean \"" + this.getBean().getType().getName() + "::" + this.getBean().toString() + ", " + "Property \"" + this.getType().getPropName() + "\": " + "an empty (null) value must not be written" + " to a mandatory property"); } else { throw new ValidationMandatoryException("invalid.prop.mandatory", this, "Property \"" + this.getType().getPropName() + "\": " + "an empty (null) value must not be written" + " to a mandatory property"); } } return newValConverted; } /** * the comparison of two properties. Handles empty values and delegates * comparison to the value objects if both are not empty. * * @param prop * the other Property to compare this Property with * * @return -1: the comparison result as integer, this.value < o.value, 1: * this.value > o.value, 0: this.value == o.value */ @SuppressWarnings("unchecked") public int compareTo(final Property prop) { int compResult = 0; Object v1 = this.getValue(); Object v2 = prop.getValue(); if ((v1 == null) && (v2 == null)) { return 0; } else if ((v1 == null) && (v2 != null)) { return -1; } else if ((v1 != null) && (v2 == null)) { return 1; } else { if (v1 instanceof Boolean) { boolean b1 = ((Boolean) v1).booleanValue(); boolean b2 = ((Boolean) v2).booleanValue(); if (!b1) { if (!b2) { return 0; } else { return -1; } } else { if (b2) { return 0; } else { return 1; } } } if ((v1 instanceof Comparable)) { compResult = ((Comparable<Object>) v1).compareTo(v2); } else { if (v1 instanceof Collection && v2 instanceof Collection) { Collection<?> c1 = (Collection<?>) v1; Collection<?> c2 = (Collection<?>) v2; Iterator<?> i2 = c2.iterator(); Object o2; for (Object o1 : c1) { o2 = i2.next(); if (o1 instanceof RapidBean) { try { compResult = ((RapidBean) o1).getId().compareTo(((RapidBean) o2).getId()); } catch (ClassCastException e) { if (o1 instanceof LinkFrozen) { throw new RapidBeansRuntimeException("This bean property " + this.getBean().getType().getName() + "::" + this.getBean().getIdString() + "." + this.getName() + ", link to " + ((LinkFrozen) o1).getIdString() + " is not resolved."); } if (o2 instanceof LinkFrozen) { throw new RapidBeansRuntimeException("Other bean property " + prop.getBean().getType().getName() + "::" + prop.getBean().getIdString() + "." + prop.getName() + ", link to bean " + ((TypePropertyCollection) prop.getType()).getTargetType().getName() + "::" + o2.toString() + " is not resolved."); } throw e; } } else { compResult = ((Comparable<Object>) o1).compareTo(o2); } if (compResult != 0) { break; } } } else { throw new RapidBeansRuntimeException("Value objects v1 (class \"" + v1.getClass().getName() + "\") and v2 (class \"" + v2.getClass().getName() + "\") are not comparable.\n" + "Prop 1: " + this.getBean().getIdString() + "." + this.getType().getPropName() + "\n" + "Prop 2: " + prop.getBean().getIdString() + "." + prop.getType().getPropName() + "\n"); } } } return compResult; } /** * the test for equality of two properties. Handles empty values and * delegates test to the value objects if both are not empty. * * @param o * the property to compare this property with * * @return a boolean flag that indicates equality or not */ public boolean equals(final Object o) { if (ClassHelper.classOf(this.getClass(), o.getClass())) { final Object v2 = ((Property) o).getValue(); if (this == o) { return true; } if (!(o instanceof Property)) { return false; } if ((this.getValue() == null) && (v2 == null)) { // both values are empty => they're equal return true; } else if ((this.getValue() == null) ^ (v2 == null)) { // only one value is empty return false; } else { // no value is empty => test for equality return this.getValue().equals(v2); } } else { return false; } } /** * @return hash code. */ public int hashCode() { if (this.toString() == null) { return "null#RapidBean".hashCode(); } else { return this.toString().hashCode(); } } /** * Determines if this property may be written. * * @return if this property may be written. */ public boolean getReadonly() { if (this.type.getReadonly()) { return true; } final RapidBean bean = this.getBean(); if (bean != null) { final Container container = bean.getContainer(); if (container != null && container.getReadonly()) { return true; } } return false; } /** * clones a Property's value. * * @param pClone * the property that receives the cloned value * @param cloneContainer * the container for the cloned property's sub beans * * @return the cloned property */ @SuppressWarnings("unchecked") public Property cloneValue(final Property pClone, final Container cloneContainer) { boolean threadLocPropSet = ThreadLocalProperties.set("bean.setparentbean.donotchangeids", true); try { if (this instanceof PropertyCollection) { Collection<Link> colBeans = (Collection<Link>) this.getValue(); if (colBeans == null) { pClone.setValue(null); } else { Collection<Link> clonedBeans = new ArrayList<Link>(); if (((TypePropertyCollection) this.getType()).isComposition()) { for (Object o : colBeans) { if (o instanceof RapidBean) { clonedBeans.add(((RapidBean) o).cloneExternal(cloneContainer)); } else { LinkFrozen lf = (LinkFrozen) o; clonedBeans.add(lf.clone()); } } } else { for (Object o : colBeans) { if (o instanceof RapidBean) { clonedBeans.add(new LinkFrozen((RapidBean) o)); } else { clonedBeans.add(((LinkFrozen) o).clone()); } } } try { ThreadLocalValidationSettings.validationOff(); ((PropertyCollection) pClone).setValue(clonedBeans, true, true); } finally { ThreadLocalValidationSettings.remove(); } } } else { try { ThreadLocalValidationSettings.validationOff(); pClone.setValue(this.getValue()); } finally { ThreadLocalValidationSettings.remove(); } } } catch (ValidationInstanceAssocTwiceException e) { // do not do anything else return pClone; } finally { if (threadLocPropSet) { ThreadLocalProperties.unset("bean.setparentbean.donotchangeids"); } } return pClone; } /** * clones a property. * * @param parentBean * the parent bean. * * @return the cloned property */ public Property clone(final RapidBean parentBean) { Property pClone = null; try { pClone = Property.createInstance(this.type, parentBean); ThreadLocalValidationSettings.validationOff(); if (this instanceof PropertyCollection) { ((PropertyCollection) pClone).setValue(this.getValue(), false, true); } else { if (!isDependent()) { pClone.setValue(this.getValue()); } } } catch (ValidationInstanceAssocTwiceException e) { // do not do anything else } finally { ThreadLocalValidationSettings.remove(); } return pClone; } /** * @param locale * the Locale * @return a string for the property's value for UI */ public String toStringGui(final RapidBeansLocale locale) { if (this.getValue() == null) { return ""; } else { return this.toString(); } } /** * @param locale * the Locale * @return a string for the propertie's name UI */ public String getNameGui(final RapidBeansLocale locale) { return getNameGuiS(this.getType(), this.getBean().getType(), locale); } /** * @param locale * the Locale * @return a string for the propertie's name UI */ public static String getNameGuiS(final TypeProperty propType, final TypeRapidBean beanType, final RapidBeansLocale locale) { String text = null; if (text == null) { if (beanType != null) { try { final String key = "bean." + beanType.getName().toLowerCase() + ".prop." + propType.getPropName().toLowerCase(); text = locale.getStringGui(key); } catch (MissingResourceException e) { text = null; } } } if (text == null && propType instanceof TypePropertyCollection) { try { TypePropertyCollection colPropType = (TypePropertyCollection) propType; String pattern = "bean." + colPropType.getTargetType().getName().toLowerCase(); if (colPropType.getMaxmult() != 1) { pattern += ".plural"; } text = locale.getStringGui(pattern); } catch (MissingResourceException e) { text = null; } } if (text == null && beanType.getSupertype() != null && beanType.getSupertype().getPropertyType(propType.getPropName()) != null) { final TypeRapidBean beanSupertype = beanType.getSupertype(); final TypeProperty propSupertype = beanSupertype.getPropertyType(propType.getPropName()); text = getNameGuiS(propSupertype, beanSupertype, locale); } if (text == null) { text = propType.getPropName(); } return text; } /** * fire the bean changed event * * @param prop * the property * @param changeType * change type * @param oldVal * old value * @param newVal * new value * @param link * the link */ protected void fireChangePre(final Property prop, final PropertyChangeEventType changeType, final Object oldVal, final Object newVal, final Link link) { if (this.bean != null) { final PropertyChangeEvent event = new PropertyChangeEvent(prop, oldVal, newVal, changeType, link); this.bean.propertyChangePre(event); } } /** * fire the bean changed event * * @param prop * the property * @param changeType * change type * @param oldVal * old value * @param newVal * new value * @param link * the link */ protected void fireChanged(final Property prop, final PropertyChangeEventType changeType, final Object oldVal, final Object newVal, final Link link) { if (this.bean != null && this.bean.getBeanState() != RapidBeanState.initializing) { final PropertyChangeEvent event = new PropertyChangeEvent(prop, oldVal, newVal, changeType, link); this.bean.propertyChanged(event); } } /** * The template method for the pure value change and firing the events. * * @param oldValue * @param newValue * @param valueSetter */ protected void setValueWithEvents(final Object oldValue, final Object newValue, final PropertyValueSetter valueSetter) { final Object validatedNewValue = validate(newValue); if ((validatedNewValue == null && oldValue != null) || (validatedNewValue != null && oldValue == null) || (validatedNewValue != null && oldValue != null && (!oldValue.equals(validatedNewValue)))) { fireChangePre(this, PropertyChangeEventType.set, oldValue, validatedNewValue, null); valueSetter.setValue(validatedNewValue); fireChanged(this, PropertyChangeEventType.set, oldValue, validatedNewValue, null); } } /** * @return if this is a dependent property */ public boolean isDependent() { return this.type.getDependentFromProps().size() > 0; } protected static Object getValueFieldByReflection(final RapidBean bean, final String propname) { try { // final Field field = bean.getClass().getDeclaredField(propname); final Field field = getDeclaredField(bean.getClass(), propname); field.setAccessible(true); return field.get(bean); } catch (SecurityException e) { throw new RapidBeansRuntimeException(e); } catch (NoSuchFieldException e) { throw new RapidBeansRuntimeException(e); } catch (IllegalArgumentException e) { throw new RapidBeansRuntimeException(e); } catch (IllegalAccessException e) { throw new RapidBeansRuntimeException(e); } } protected static Object getValueByReflection(final RapidBean bean, final String propname) { try { final Method getter = bean.getClass().getMethod("get" + StringHelper.upperFirstCharacter(propname)); return getter.invoke(bean); } catch (SecurityException e) { throw new RapidBeansRuntimeException(e); } catch (NoSuchMethodException e) { throw new RapidBeansRuntimeException(e); } catch (IllegalArgumentException e) { throw new RapidBeansRuntimeException(e); } catch (IllegalAccessException e) { throw new RapidBeansRuntimeException(e); } catch (InvocationTargetException e) { throw new RapidBeansRuntimeException(e); } } private static Field getDeclaredField(final Class<?> clazz, final String name) throws NoSuchFieldException { try { return clazz.getDeclaredField(name); } catch (NoSuchFieldException e) { if (clazz.getSuperclass() != null && (!clazz.getSuperclass().equals(Object.class))) { return getDeclaredField(clazz.getSuperclass(), name); } throw e; } } protected static void setValueByReflection(final RapidBean bean, final String propname, final Object newValue) { try { // final Field field = bean.getClass().getDeclaredField(propname); final Field field = getDeclaredField(bean.getClass(), propname); field.setAccessible(true); field.set(bean, newValue); } catch (SecurityException e) { throw new RapidBeansRuntimeException(e); } catch (NoSuchFieldException e) { throw new RapidBeansRuntimeException(e); } catch (IllegalArgumentException e) { throw new RapidBeansRuntimeException(e); } catch (IllegalAccessException e) { throw new RapidBeansRuntimeException(e); } } }