All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.metawidget.inspector.impl.BaseObjectInspector Maven / Gradle / Ivy

There is a newer version: 4.2
Show newest version
// Metawidget
//
// This library 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 2.1 of the License, or (at your option) any later version.
//
// This library 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 copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package org.metawidget.inspector.impl;

import static org.metawidget.inspector.InspectionResultConstants.*;

import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import org.metawidget.inspector.iface.DomInspector;
import org.metawidget.inspector.iface.InspectorException;
import org.metawidget.inspector.impl.actionstyle.Action;
import org.metawidget.inspector.impl.actionstyle.ActionStyle;
import org.metawidget.inspector.impl.propertystyle.Property;
import org.metawidget.inspector.impl.propertystyle.PropertyStyle;
import org.metawidget.util.ArrayUtils;
import org.metawidget.util.ClassUtils;
import org.metawidget.util.CollectionUtils;
import org.metawidget.util.LogUtils;
import org.metawidget.util.LogUtils.Log;
import org.metawidget.util.XmlUtils;
import org.metawidget.util.simple.Pair;
import org.metawidget.util.simple.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Convenience implementation for Inspectors that inspect Objects.
 * 

* Handles iterating over an Object for properties and actions, and supporting pluggable property * and action conventions. Also handles unwrapping an Object wrapped by a proxy library (such as * CGLIB or Javassist). *

*

Inspecting classes

*

* In general BaseObjectInspector inspects objects, not classes. It will * return null if the object value is null, rather than returning the properties of its class. This * is generally what is expected. In particular, WidgetProcessors such as binding * implementations would not expect to be given a list of properties and asked to bind to a null * object. *

* However, there is a special concession. If BaseObjectInspector is pointed * directly at a type (ie. names == null), it will return properties even if the actual * value is null. This is important so we can inspect parameterized types of Collections without * having to iterate over and grab the first element in that Collection. * * @author Richard Kennard */ public abstract class BaseObjectInspector implements DomInspector { // // Private members // private final PropertyStyle mPropertyStyle; private final ActionStyle mActionStyle; // // Protected members // protected final Log mLog = LogUtils.getLog( getClass() ); // // Constructors // protected BaseObjectInspector() { this( new BaseObjectInspectorConfig() ); } /** * Config-based constructor. *

* BaseObjectInspector-derived inspectors should generally support configuration, to allow * configuring property styles and action styles. */ protected BaseObjectInspector( BaseObjectInspectorConfig config ) { mPropertyStyle = config.getPropertyStyle(); mActionStyle = config.getActionStyle(); } // // Public methods // /** * Inspect the given Object according to the given path, and return the result as a String * conforming to inspection-result-1.0.xsd. *

* This method is marked final because most Metawidget implementations will call * inspectAsDom directly instead. So subclasses need to override * inspectAsDom, not inspect. */ public final String inspect( Object toInspect, String type, String... names ) { Element element = inspectAsDom( toInspect, type, names ); if ( element == null ) { return null; } return XmlUtils.nodeToString( element, false ); } public Element inspectAsDom( Object toInspect, String type, String... names ) { // If no type, return nothing if ( type == null ) { return null; } try { Object childToInspect = null; String childName = null; Class declaredChildType; Map parentAttributes = null; // If the path has a parent... if ( names != null && names.length > 0 ) { // ...inspect its property for useful annotations... Pair> pair = traverse( toInspect, type, true, names ); if ( pair == null ) { return null; } childName = names[names.length - 1]; // Use the actual, runtime class (tuple[0].getClass()) not the declared class // (tuple[1]), in case the declared class is an interface or subclass Object parent = pair.getLeft(); Class parentType = parent.getClass(); Property propertyInParent = mPropertyStyle.getProperties( parentType ).get( childName ); if ( propertyInParent == null ) { return null; } declaredChildType = propertyInParent.getType(); // ...provided it has a getter if ( propertyInParent.isReadable() ) { parentAttributes = inspectParent( parent, propertyInParent ); childToInspect = propertyInParent.read( parent ); } } // ...otherwise, just start at the end point else { Pair> pair = traverse( toInspect, type, false ); if ( pair == null ) { return null; } childToInspect = pair.getLeft(); declaredChildType = pair.getRight(); } Document document = XmlUtils.newDocument(); Element entity = document.createElementNS( NAMESPACE, ENTITY ); // Inspect child properties if ( childToInspect == null || declaredChildType.isPrimitive() ) { XmlUtils.setMapAsAttributes( entity, inspectEntity( declaredChildType, declaredChildType ) ); // If pointed directly at a type, return properties even // if the actual value is null. This is a special concession so // we can inspect parameterized types of Collections without having // to iterate over and grab the first element in that Collection if ( names == null || names.length == 0 ) { inspect( childToInspect, declaredChildType, entity ); } } else { Class actualChildType = childToInspect.getClass(); XmlUtils.setMapAsAttributes( entity, inspectEntity( declaredChildType, actualChildType ) ); inspect( childToInspect, actualChildType, entity ); } // Add parent attributes (if any) XmlUtils.setMapAsAttributes( entity, parentAttributes ); // Nothing of consequence to return? if ( isInspectionEmpty( entity ) ) { return null; } // Start a new DOM Document Element root = document.createElementNS( NAMESPACE, ROOT ); root.setAttribute( VERSION, "1.0" ); document.appendChild( root ); root.appendChild( entity ); // If there were parent attributes, we may have a useful child name if ( childName != null ) { entity.setAttribute( NAME, childName ); } // Every Inspector needs to attach a type to the entity, so that CompositeInspector can // merge it. The type should be the *declared* type, not the *actual* type, as otherwise // subtypes (and proxied types) will stop XML and Object-based Inspectors merging back // together properly entity.setAttribute( TYPE, declaredChildType.getName() ); // Return the document return root; } catch ( Exception e ) { throw InspectorException.newException( e ); } } // // Protected methods // /** * Inspect the parent property leading to the toInspect. Often the parent property * contains useful annotations, such as UiLookup. *

* This method can be overridden by clients wishing to modify the parent inspection process (eg. * JexlInspector) * * @param parentToInspect * the parent to inspect. Never null * @param propertyInParent * the property in the parent that points to the original toInspect */ protected Map inspectParent( Object parentToInspect, Property propertyInParent ) throws Exception { Map traitAttributes = inspectTrait( propertyInParent ); Map propertyAttributes = inspectProperty( propertyInParent ); if ( traitAttributes == null ) { return propertyAttributes; } if ( propertyAttributes == null ) { return traitAttributes; } traitAttributes.putAll( propertyAttributes ); return traitAttributes; } /** * Inspect the toInspect for properties and actions. *

* This method can be overridden by clients wishing to modify the inspection process (eg. * JexlInspector). Most clients will find it easier to override one of the * sub-methods, such as inspectTrait or inspectProperty. * * @param toInspect * the object to inspect. May be null * @param clazz * the class to inspect. If toInspect is not null, will be the actual class of * toInspect. If toInspect is null, will be the class to lookup */ protected void inspect( Object toInspect, Class classToInspect, Element toAddTo ) throws Exception { Document document = toAddTo.getOwnerDocument(); // Inspect properties for ( Property property : getProperties( classToInspect ).values() ) { Map traitAttributes = inspectTrait( property ); Map propertyAttributes = inspectProperty( property ); Map entityAttributes = inspectPropertyAsEntity( property, toInspect ); if ( ( traitAttributes == null || traitAttributes.isEmpty() ) && ( propertyAttributes == null || propertyAttributes.isEmpty() ) && ( entityAttributes == null || entityAttributes.isEmpty() ) ) { continue; } Element element = document.createElementNS( NAMESPACE, PROPERTY ); element.setAttribute( NAME, property.getName() ); XmlUtils.setMapAsAttributes( element, traitAttributes ); XmlUtils.setMapAsAttributes( element, propertyAttributes ); XmlUtils.setMapAsAttributes( element, entityAttributes ); toAddTo.appendChild( element ); } // Inspect actions for ( Action action : getActions( classToInspect ).values() ) { Map traitAttributes = inspectTrait( action ); Map actionAttributes = inspectAction( action ); if ( ( traitAttributes == null || traitAttributes.isEmpty() ) && ( actionAttributes == null || actionAttributes.isEmpty() ) ) { continue; } Element element = document.createElementNS( NAMESPACE, ACTION ); element.setAttribute( NAME, action.getName() ); XmlUtils.setMapAsAttributes( element, traitAttributes ); XmlUtils.setMapAsAttributes( element, actionAttributes ); toAddTo.appendChild( element ); } } /** * Inspect the given entity's class (not its child properties/actions) and return a Map * of attributes. *

* Note: for convenience, this method does not expect subclasses to deal with DOMs and Elements. * Those subclasses wanting more control over these features should override methods higher in * the call stack instead. *

* For example usage, see PropertyTypeInspector and Java5Inspector. * * @param declaredClass * the class passed to inspect, or the class declared by the Object's * parent (ie. its getter method) * @param actualClass * the actual class of the Object. If you are searching for annotations, generally * you should inspect actualClass rather than declaredClass */ protected Map inspectEntity( Class declaredClass, Class actualClass ) throws Exception { return null; } /** * Inspect the given trait and return a Map of attributes. *

* A 'trait' is an interface common to both Property and Action, so * you can override this single method if your annotation is applicable to both. For example, * UiLabel. *

* In the event of an overlap between the attributes set by inspectTrait and those * set by inspectProperty/inspectAction, the latter will receive * precedence. *

* Note: for convenience, this method does not expect subclasses to deal with DOMs and Elements. * Those subclasses wanting more control over these features should override methods higher in * the call stack instead. * * @param trait * the trait to inspect */ protected Map inspectTrait( Trait trait ) throws Exception { return null; } /** * Inspect the given property and return a Map of attributes. *

* Note: for convenience, this method does not expect subclasses to deal with DOMs and Elements. * Those subclasses wanting more control over these features should override methods higher in * the call stack instead. * * @param property * the property to inspect */ protected Map inspectProperty( Property property ) throws Exception { return null; } /** * Inspect the given action and return a Map of attributes. *

* Note: for convenience, this method does not expect subclasses to deal with DOMs and Elements. * Those subclasses wanting more control over these features should override methods higher in * the call stack instead. * * @param action * the action to inspect */ protected Map inspectAction( Action action ) throws Exception { return null; } /** * Whether to additionally inspect each child property using inspectEntity from its * object level. *

* This can be useful if the property's value defines useful class-level semantics (eg. * TYPE), but it is expensive (as it requires invoking the property's * getter to retrieve the value) so is false by default. *

* For example usage, see PropertyTypeInspector and Java5Inspector. * * @param property * the property to inspect */ protected boolean shouldInspectPropertyAsEntity( Property property ) { return false; } // // Protected final methods // protected final Map getProperties( Class clazz ) { if ( mPropertyStyle == null ) { // (use Collections.EMPTY_MAP, not Collections.emptyMap, so that we're 1.4 compatible) @SuppressWarnings( "unchecked" ) Map map = Collections.EMPTY_MAP; return map; } return mPropertyStyle.getProperties( clazz ); } protected final Map getActions( Class clazz ) { if ( mActionStyle == null ) { // (use Collections.EMPTY_MAP, not Collections.emptyMap, so that we're 1.4 compatible) @SuppressWarnings( "unchecked" ) Map map = Collections.EMPTY_MAP; return map; } return mActionStyle.getActions( clazz ); } // // Private methods // /** * Inspect the given property 'as an entity'. *

* This method delegates to inspectEntity. *

* If the property is readable and its type is not final, this method first invokes the * property's getter so that it can pass the runtime type of the object to * inspectEntity. */ private Map inspectPropertyAsEntity( Property property, Object toInspect ) throws Exception { if ( !shouldInspectPropertyAsEntity( property ) ) { return null; } Class entityClass = property.getType(); // Inspect the runtime type // // Note: it is tempting to provide a less-expensive version of // inspectPropertyAsEntity that inspects the property's type without invoking // the getter. However that places a burden on the individual Inspector, // because what if the field is declared to be of type Object but its // actual value is a Boolean? // // Note: If the type is final (which includes Java primitives) there is no // need to call the getter because there cannot be a subtype if ( property.isReadable() && !Modifier.isFinal( entityClass.getModifiers() ) ) { Object propertyValue = null; try { propertyValue = property.read( toInspect ); } catch ( Exception e ) { // By definition, a 'getter' method should not affect the state // of the object, so it should not fail. However, sometimes a getter's // implementation may rely on another object being in a certain state (eg. // JSF's DataModel.getRowData) - in which case it will not be readable. // We therefore treat value as 'null', so that at least we inspect the type } if ( propertyValue != null ) { entityClass = propertyValue.getClass(); } } // Delegate to inspectEntity return inspectEntity( property.getType(), entityClass ); } /** * Returns true if the inspection returned nothing of consequence. This is an optimization that * allows our Inspector to return null overall, rather than creating * and serializing an XML document, which CompositeInspector then deserializes and * merges, all for no meaningful content. * * @return true if the inspection is 'empty' */ private boolean isInspectionEmpty( Element elementEntity ) { if ( elementEntity.hasAttributes() ) { return false; } if ( elementEntity.hasChildNodes() ) { return false; } return true; } /** * Traverses the given Object using properties of the given names. *

* Note: traversal involves calling Property.read, which invokes getter methods and can * therefore have side effects. For example, a JSF controller 'ResourceController' may have a * method 'getLoggedIn' which has to check the HttpSession, maybe even hit some EJBs or access * the database. * * @return If found, a tuple of Object and declared type (not actual type). If not found, * returns null. */ private Pair> traverse( Object toTraverse, String type, boolean onlyToParent, String... names ) { // Special support for direct class lookup if ( toTraverse == null ) { // If there are names, return null if ( onlyToParent ) { return null; } // If no such class, return null Class clazz = ClassUtils.niceForName( type ); if ( clazz == null ) { return null; } return new Pair>( null, clazz ); } // Use the toTraverse's ClassLoader, to support Groovy dynamic classes // // (note: for Groovy dynamic classes, this needs the applet to be signed - I think this is // still better than 'relaxing' this sanity check, as that would lead to differing behaviour // when deployed as an unsigned applet versus a signed applet) Class traverseDeclaredType = ClassUtils.niceForName( type, toTraverse.getClass().getClassLoader() ); if ( traverseDeclaredType == null || !traverseDeclaredType.isAssignableFrom( toTraverse.getClass() ) ) { return null; } // Traverse through names (if any) Object traverse = toTraverse; if ( names != null && names.length > 0 ) { Set traversed = CollectionUtils.newHashSet(); traversed.add( traverse ); int length = names.length; for ( int loop = 0; loop < length; loop++ ) { String name = names[loop]; Property property = mPropertyStyle.getProperties( traverse.getClass() ).get( name ); if ( property == null || !property.isReadable() ) { return null; } Object parentTraverse = traverse; traverse = property.read( traverse ); // Unlike BaseXmlInspector (which can never be certain it has detected a // cyclic reference because it only looks at types, not objects), // BaseObjectInspector can detect cycles and nip them in the bud if ( !traversed.add( traverse ) ) { // Trace, rather than do a debug log, because it makes for a nicer 'out // of the box' experience mLog.trace( "{0} prevented infinite recursion on {1}{2}. Consider annotating {3} as @UiHidden", ClassUtils.getSimpleName( getClass() ), type, ArrayUtils.toString( names, StringUtils.SEPARATOR_FORWARD_SLASH, true, false ), name ); return null; } // Always come in this loop once, even if onlyToParent, because we // want to do the recursion check if ( onlyToParent && loop >= length - 1 ) { return new Pair>( parentTraverse, traverseDeclaredType ); } if ( traverse == null ) { return null; } traverseDeclaredType = property.getType(); } } return new Pair>( traverse, traverseDeclaredType ); } }