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 file is dual licensed under both the LGPL
// (http://www.gnu.org/licenses/lgpl-2.1.html) and the EPL
// (http://www.eclipse.org/org/documents/epl-v10.php). As a
// recipient of Metawidget, you may choose to receive it under either
// the LGPL or the EPL.
//
// Commercial licenses are also available. See http://metawidget.org
// for details.

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 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.inspector.impl.propertystyle.ValueAndDeclaredType;
import org.metawidget.util.ArrayUtils;
import org.metawidget.util.ClassUtils;
import org.metawidget.util.LogUtils;
import org.metawidget.util.LogUtils.Log;
import org.metawidget.util.XmlUtils;
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. *

*

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 are special concessions: *

*

    *
  1. 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.
  2. *
  3. Because object traversal is managed by the PropertyStyle, some support class * traversal (eg. StaticPropertyStyle)
  4. *
* * @author Richard Kennard */ public abstract class BaseObjectInspector implements DomInspector { // // Protected members // protected final Log mLog = LogUtils.getLog( getClass() ); // // Private members // private final PropertyStyle mPropertyStyle; private final ActionStyle mActionStyle; // // 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; String childName; String declaredChildType; Map parentAttributes; boolean abortTraversingPastNull = false; if ( toInspect != null ) { ClassUtils.registerAlienClassLoader( toInspect.getClass().getClassLoader() ); } // If the path has a parent... if ( names != null && names.length > 0 ) { // ...inspect its property for useful annotations ValueAndDeclaredType valueAndDeclaredType = mPropertyStyle.traverse( toInspect, type, true, names ); String parentType = valueAndDeclaredType.getDeclaredType(); // If parentType is null, the mPropertyStyle does not want us to continue if ( parentType == null ) { return null; } // If possible use the actual class rather than the declared class, in case // the declared class is an interface or superclass. // // Parent can be null if we are just traversing Classes (i.e. StaticPropertyStyle) Object parent = valueAndDeclaredType.getValue(); if ( parent != null ) { parentType = parent.getClass().getName(); } childName = names[names.length - 1]; Property propertyInParent = mPropertyStyle.getProperties( parentType ).get( childName ); // If the parent does not define such a property, something is wrong if ( propertyInParent == null ) { throw InspectorException.newException( "Parent of " + type + ArrayUtils.toString( names, StringUtils.SEPARATOR_FORWARD_SLASH, true, false ) + " does not define a property '" + childName + "'" ); } declaredChildType = propertyInParent.getType(); parentAttributes = inspectParent( parent, propertyInParent ); // Now step forward to the usual end of the path if ( parent == null || !propertyInParent.isReadable() ) { childToInspect = null; } else { childToInspect = propertyInParent.read( parent ); // Stop if childToInspect==null, given we know names.length > 0 // // If we are inspecting Objects, we never want to traverse past a null. If we // are just inspecting Classes (i.e. StaticPropertyStyle) we will never come in // here because parent==null if ( childToInspect == null ) { abortTraversingPastNull = true; } else { ClassUtils.registerAlienClassLoader( childToInspect.getClass().getClassLoader() ); } } } else { // ...otherwise, just start at the end point childToInspect = toInspect; childName = null; declaredChildType = type; parentAttributes = null; // Proceed even if childToInspect==null, given we know names.length==0 // // If pointed directly at a type, we return properties even if the toInspect 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 } Document document = XmlUtils.newDocument(); Element entity = document.createElementNS( NAMESPACE, ENTITY ); // Inspect child properties String actualChildType; if ( childToInspect == null || ClassUtils.isPrimitive( declaredChildType ) ) { actualChildType = declaredChildType; } else { actualChildType = childToInspect.getClass().getName(); } XmlUtils.setMapAsAttributes( entity, inspectEntity( declaredChildType, actualChildType ) ); if ( !abortTraversingPastNull ) { inspectTraits( 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 will stop XML and Object-based Inspectors merging back together properly entity.setAttribute( TYPE, declaredChildType ); // 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. * * @param parentToInspect * the parent to inspect. Can be null (eg. when using static inspection). May be * useful to clients in determining the context of the subsequent * inspectTrait and inspectProperty calls (eg. * they relate to the parent, not the normal toInspect) * @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. 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 inspectTraits( Object toInspect, String type, Element toAddTo ) throws Exception { Document document = toAddTo.getOwnerDocument(); // Inspect properties for ( Property property : getProperties( type ).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( type ).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. *

* Does nothing by default. For example usage, see PropertyTypeInspector. * * @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( String declaredClass, String 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. *

* Does nothing by default. * * @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. *

* Does nothing by default. * * @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. *

* Does nothing by default. * * @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. *

* Returns false by default. For example usage, see * PropertyTypeInspector. * * @param property * the property to inspect */ protected boolean shouldInspectPropertyAsEntity( Property property ) { return false; } // // Protected final methods // protected final Map getProperties( String type ) { if ( mPropertyStyle == null ) { return Collections.emptyMap(); } return mPropertyStyle.getProperties( type ); } protected final Map getActions( String type ) { if ( mActionStyle == null ) { return Collections.emptyMap(); } return mActionStyle.getActions( type ); } // // 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; } String actualType = 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 ( toInspect != null ) { Class actualClass = ClassUtils.niceForName( actualType ); if ( property.isReadable() && ( actualClass == null || !Modifier.isFinal( actualClass.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 ) { actualType = propertyValue.getClass().getName(); } } } // Delegate to inspectEntity return inspectEntity( property.getType(), actualType ); } /** * 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy