// 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, WidgetProcessor
s 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 );
}
}