package org.codehaus.mojo.pomtools.wrapper; /* * Copyright 2005-2006 The Apache Software Foundation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.beanutils.ConstructorUtils; import org.apache.commons.beanutils.PropertyUtils; import org.codehaus.mojo.pomtools.PomToolsPluginContext; import org.codehaus.mojo.pomtools.config.FieldConfiguration; import org.codehaus.mojo.pomtools.wrapper.modify.AbstractModifiableObject; import org.codehaus.mojo.pomtools.wrapper.modify.Modifiable; import org.codehaus.mojo.pomtools.wrapper.reflection.BeanField; import org.codehaus.mojo.pomtools.wrapper.reflection.BeanFields; import org.codehaus.mojo.pomtools.wrapper.reflection.FactoryBeanField; import org.codehaus.mojo.pomtools.wrapper.reflection.ModelReflectionException; import org.codehaus.mojo.pomtools.wrapper.reflection.tostring.ObjectToStringBuilder; import org.codehaus.mojo.pomtools.wrapper.reflection.tostring.ObjectWrapperToStringBuilder; import org.codehaus.mojo.pomtools.wrapper.reflection.tostring.ToStringBuilderFactory; import org.codehaus.plexus.util.StringUtils; /** * * @author <a href="mailto:dhawkins@codehaus.org">David Hawkins</a> * @version $Id$ */ public class ObjectWrapper extends AbstractModifiableObject { public static final String FIELD_PATH_SEPARATOR = "."; private static final ObjectToStringBuilder DEFAULT_TO_STRING_BUILDER = new ObjectWrapperToStringBuilder(); private BeanFields fields; private final FieldConfiguration fieldConfig; private final ObjectWrapper parent; private Object wrappedObject; private Class wrappedObjectClass; private final Map wrappedValueMap = new HashMap(); private final Set createdValueSet = new HashSet(); private final String name; private String fullName; private final Set modifiedFields = new HashSet(); private final ObjectToStringBuilder toStringBuilder; /** Constructs a new ObjectWrapper and specifies the implClass that is used to create * an empty object if the objectToWrap is null. * The {@link Modifiable} parent is set to the parent; * * @param parent the parent ObjectWrapper of this object * @param objectToWrap the value object to wrap. This is a field of the parent * @param name the field name that the objectToWrap is called within the parent * @param objectToWrapClass the Class to use to create a new objectToWrap if the supplied one is null */ public ObjectWrapper( ObjectWrapper parent, Object objectToWrap, String name, Class objectToWrapClass ) { this( parent, parent, objectToWrap, name, objectToWrapClass ); } /** Constructs a new ObjectWrapper and does not specify a implClass. * The objectToWrap cannot be null when using this constructor. ObjectWrapper * needs an instance of an object to wrap and will create one if it is not supplied. * The {@link Modifiable} parent is set to the parent; * * @param parent the parent ObjectWrapper of this object * @param objectToWrap the value object to wrap. This is a field of the parent * @param name the field name that the objectToWrap is called within the parent */ public ObjectWrapper( ObjectWrapper parent, Object objectToWrap, String name ) { this( parent, parent, objectToWrap, name, (Class) null ); } /** Constructs an ObjectWrapper that wraps the objectToWrap. * ObjectWrapper needs an instance of an object to wrap will create one * the supplied objectToWrap is null. The default constructor will be called * on the implClass. * * @param parentModifiable allows separate specification of the parentModifiable * @param parent the parent ObjectWrapper of this object * @param objectToWrap the value object to wrap. This is a field of the parent * @param name the field name that the objectToWrap is called within the parent * @param objectToWrapClass the Class to use to create a new objectToWrap if the supplied one is null */ public ObjectWrapper( Modifiable parentModifiable, ObjectWrapper parent, Object objectToWrap, String name, Class objectToWrapClass ) { super( parentModifiable ); this.parent = parent; this.name = name; this.fieldConfig = PomToolsPluginContext.getInstance().getFieldConfiguration( this.getFullName() ); if ( objectToWrap == null ) { if ( objectToWrapClass == null ) { throw new IllegalStateException( "ObjectWrapper needs an underlying object, but no object was " + "supplied and no implClass was supplied to create an empty object." ); } this.wrappedObjectClass = objectToWrapClass; try { this.wrappedObject = ConstructorUtils.invokeConstructor( objectToWrapClass, null ); } catch ( NoSuchMethodException e ) { throw new ModelReflectionException( e ); } catch ( IllegalAccessException e ) { throw new ModelReflectionException( e ); } catch ( InvocationTargetException e ) { throw new ModelReflectionException( e ); } catch ( InstantiationException e ) { throw new ModelReflectionException( e ); } } else { this.wrappedObject = objectToWrap; this.wrappedObjectClass = objectToWrap.getClass(); } this.fields = new BeanFields( this.getFullName(), this.wrappedObject ); this.toStringBuilder = initToStringBuilder(); } private ObjectToStringBuilder initToStringBuilder() { if ( fieldConfig != null && fieldConfig.getToStringBuilder() != null ) { return ToStringBuilderFactory.get( fieldConfig.getToStringBuilder() ); } else { return DEFAULT_TO_STRING_BUILDER; } } /** Creates an instance of our wrapped class by calling its default constructor. * TODO: This method could probably use caching when it is being called by isSameAsDefault() */ protected Object createDefaultInstance() { try { return ConstructorUtils.invokeConstructor( wrappedObjectClass, null ); } catch ( NoSuchMethodException e ) { throw new ModelReflectionException( e ); } catch ( IllegalAccessException e ) { throw new ModelReflectionException( e ); } catch ( InvocationTargetException e ) { throw new ModelReflectionException( e ); } catch ( InstantiationException e ) { throw new ModelReflectionException( e ); } } public ObjectWrapper getParent() { return this.parent; } /** Returns the name of the field to which this object belongs. * * @return this object's field name */ public String getName() { return this.name; } /** Returns the fully qualified name of the wrapped object. * <p> * Objects are comprised of fields which have names. A fully qualified name * is the this name of the field this object belongs to, as well as that field's * owner and up.<br> * For example:<br> * If this objects name were "dependency", it could have a fullName of:<br> * project.dependencies.dependency * * @return the full name of the wrapped object */ public String getFullName() { if ( fullName == null ) { if ( parent == null ) { fullName = this.name; } else { fullName = parent.getFullName() + FIELD_PATH_SEPARATOR + this.name; } } return fullName; } /** Returns the {@link #toString()} representation of the value with * an appended annotation if this object has been modified. * * @return toString() with an annotation of modified */ public final String getValueLabel() { return toString(); } /** Returns the original wrapped object passed to or created in the constructor. * <p> * Subclasses should excercise caution an never modify this object directly. * * @return */ protected Object getInternalWrappedObject() { return wrappedObject; } /** Returns the original wrapped object with all modifications applied to it. * Note that this method will return null if the object is the same as the default constructor * for the object. * @return the wrapped object or null is the object isEmpty() */ public Object getWrappedObject() { if ( isEmpty() ) { return null; } for ( Iterator i = getFields().iterator(); i.hasNext(); ) { BeanField field = (BeanField) i.next(); if ( field.isWrappedValue() ) { ObjectWrapper value = (ObjectWrapper) getFieldValue( field, false ); if ( value != null ) { if ( createdValueSet.contains( field ) && value.isSameAsDefault() ) { // we can ignore this value because we created the value AND it // is the same as the default value = null; } else { setWrappedObjectValue( field, value.getWrappedObject() ); } } } } return wrappedObject; } /** Returns the internal {@link BeanFields} used to describe this object. */ public final BeanFields getFields() { return fields; } /** Iterates through each {@link BeanField} and determines if the value is empty. * <p> * If the value is an instance of <code>ObjectWrapper</code>, the <code>isEmpty()</code> * method is called on that object.<br> * If the value is a <code>String</code>, the {@link StringUtils#isNotEmpty(java.lang.String)} * method is used.<br> * The object is considered to be NOT empty if any field (other than String or ObjectWrapper) is non null. * */ public boolean isEmpty() { for ( Iterator i = getFields().iterator(); i.hasNext(); ) { BeanField field = (BeanField) i.next(); Object value = getFieldValue( field, false ); if ( value != null ) { if ( field.isWrappedValue() ) { if ( !( (ObjectWrapper) value ).isEmpty() ) { return false; } } else if ( !( value instanceof String ) || StringUtils.isNotEmpty( (String) value ) ) { return false; } } } return true; } /** Returns whether the wrapped object is in the same state as * it's default constructor. This is useful in maven to prevent * writing a section that is really only populated with the defaults. */ public boolean isSameAsDefault() { Object defaultInstance = createDefaultInstance(); for ( Iterator i = getFields().iterator(); i.hasNext(); ) { BeanField field = (BeanField) i.next(); Object myValue = getFieldValue( field, false ); Object defaultValue = getWrappedObjectValue( defaultInstance, field ); if ( !equals( myValue, defaultValue, field ) ) { return false; } } return true; } private boolean equals( Object myValue, Object defaultValue, BeanField field ) { if ( myValue == null ) { return defaultValue == null; } else if ( defaultValue == null ) { return false; } else { if ( field.isWrappedValue() ) { return ( (ObjectWrapper) myValue ).isSameAsDefault(); } else { return myValue.equals( defaultValue ); } } } public String toString() { return this.toStringBuilder.toString( this ); } private BeanField getField( String fieldName ) { BeanField field = getFields().get( fieldName ); if ( field == null ) { throw new IllegalArgumentException( "\"" + fieldName + "\" is not a valid field for " + getFullName() ); } return field; } /** Returns the value for the wrapped object for the specified fieldName. * * @param fieldName * @throws IllegalArgumentException if the field cannot be found. */ public Object getFieldValue( String fieldName ) { return getFieldValue( getField( fieldName ) ); } /** Returns the value for the wrapped object for the specified field. * This method creates a value if the specified field is null * * @param field * @return */ public Object getFieldValue( BeanField field ) { return getFieldValue( field, true ); } /** Returns the value for the wrapped object for the specified field. * Specifies whether to create the value if the underlying object's value * is null. Turning off object creation is useful in testing isEmpty and * equality in that it prevents having to do deep comparisons when the value is * actually null. * * @param field * @return */ public Object getFieldValue( BeanField field, boolean createIfNull ) { if ( field.isWrappedValue() ) { ObjectWrapper wrappedValue = (ObjectWrapper) wrappedValueMap.get( field ); // Create a wrapper if we don't already have a wrapper for this value if ( wrappedValue == null ) { Object objectToWrap = getWrappedObjectValue( field ); if ( objectToWrap != null || createIfNull ) { if ( objectToWrap == null ) { // add the value to our createdValueSet so we can identify // values created vs values that were existing. createdValueSet.add( field ); } wrappedValue = ( (FactoryBeanField) field ).createWrapperObject( this, objectToWrap ); wrappedValueMap.put( field, wrappedValue ); } } return wrappedValue; } else { return getWrappedObjectValue( field ); } } /** Sets the value for specified field of the underlying wrapped object * to the specified value. * * @throws IllegalArgumentException if the field cannot be found. */ public void setFieldValue( String fieldName, Object value ) { setFieldValue( getField( fieldName ), value ); } /** Sets the value for specified field of the underlying wrapped object * to the specified value. */ public void setFieldValue( BeanField field, Object value ) { if ( field.isWrappedValue() ) { ObjectWrapper obj = (ObjectWrapper) wrappedValueMap.get( field ); if ( obj == null ) { throw new IllegalStateException( "Attempted to set value for a wrapped object that did not exist." ); } obj.setFieldValue( field, value ); setModified( field ); } else { // Don't set the value if it didn't change. if ( !StringUtils.equals( String.valueOf( getFieldValue( field, false ) ), String.valueOf( value ) ) ) { setWrappedObjectValue( field, value ); setModified( field ); } } } public boolean isFieldModified( BeanField field ) { if ( modifiedFields.contains( field ) ) { return true; } if ( field.isWrappedValue() ) { ObjectWrapper wrapper = (ObjectWrapper) getFieldValue( field, false ); if ( wrapper != null ) { return wrapper.isModified(); } } return false; } private Object getWrappedObjectValue( BeanField field ) { return getWrappedObjectValue( wrappedObject, field ); } private Object getWrappedObjectValue( Object obj, BeanField field ) throws ModelReflectionException { String msg = "Unable to get value for " + getName() + "." + field.getFieldName(); try { return (Object) PropertyUtils.getProperty( obj, field.getFieldName() ); } catch ( IllegalAccessException e ) { throw new ModelReflectionException( msg, e ); } catch ( InvocationTargetException e ) { throw new ModelReflectionException( msg, e ); } catch ( NoSuchMethodException e ) { throw new ModelReflectionException( msg, e ); } } private void setWrappedObjectValue( BeanField field, Object value ) throws ModelReflectionException { String msg = "Unable to set value for " + getName() + "." + field.getFieldName(); try { BeanUtils.setProperty( wrappedObject, field.getFieldName(), value ); } catch ( IllegalAccessException e ) { throw new ModelReflectionException( msg, e ); } catch ( InvocationTargetException e ) { throw new ModelReflectionException( msg, e ); } } /** Sets the modified flag to true and adds the field to our modified fields.; * */ public void setModified( BeanField field ) { setModified(); modifiedFields.add( field ); } /** Clears our list of modified fields if setting to false; * */ public void setModified( boolean modified ) { super.setModified( modified ); if ( !modified ) { this.modifiedFields.clear(); } } }