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

org.metawidget.inspector.impl.BaseXmlInspector 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.io.InputStream;
import java.util.Map;

import org.metawidget.config.iface.ResourceResolver;
import org.metawidget.inspector.iface.DomInspector;
import org.metawidget.inspector.iface.InspectorException;
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 XML files.
 * 

* Handles taking a 'flat' XML file (eg. only one <entity> node deep) and * traversing nested paths, such as foo/bar, from... *

* * <entity type="foo">
*    <property name="myBar" type="bar">
* </entity> *
*

* ...to top level... *

* * <entity type="bar"> * *

* ...and constructing... *

* <entity name="myBar" type="bar"> *

* ...as output. *

*

Schema Validation

*

* This class does not support schema validation - it is not that useful in practice for two * reasons. First, Inspectors like HibernateInspector cannot use it because they can be * pointed at different kinds of files (eg. hibernate.cfg.xml or hibernate-mapping.hbm.xml). Second, * Inspectors that are intended for Android environments (eg. XmlInspector) cannot use * it because Android's Dalvik preprocessor balks at the unsupported schema classes (even if they're * wrapped in a ClassNotFoundException). *

*

Mixing XML and Object-based Inspectors

*

* Several pieces of functionality apply to mixing XML-based Inspectors (e.g. * XmlInspector) and Object-based Inspectors (e.g. * PropertyTypeInspector) in the same application (i.e. via * CompositeInspector). *

* First, you may encounter a problem whereby the Object-based Inspectors will always * stop at null or recursive references, whereas the XML Inspectors (which * have no knowledge of Object values) will continue. This can lead to the * WidgetBuilders constructing a UI for a null Object, which may upset * some WidgetProcessors (e.g. BeansBindingProcessor). To resolve this, * you can set BaseXmlInspectorConfig.setRestrictAgainstObject, whereby the XML-based * Inspector will do a check for null or recursive references, and not * return any XML. *

* In addition, setting restrictAgainstObject allows the XML Inspector to * traverse child relationships and infer their types using the Object. This saves having to * explicitly specify those types and relationships in the XML. *

* Second, by default you need to explicitly specify any inheritance relationships between types in * the XML, because the XML has no knowledge of your Java classes. This includes the names of any * proxied classes. If this becomes laborious, you can set * BaseXmlInspectorConfig.setInferInheritanceHierarchy to infer the relationships * automatically from your Java classes. If you are using setRestrictAgainstObject, * setInferInheritanceHierarchy is implied. *

* Third, it is important the properties defined by the XML and the ones defined by the Java classes * stay in sync. To enforce this, you can set * BaseXmlInspectorConfig.setValidateAgainstClasses. * * @author Richard Kennard */ // TODO: rework BaseXmlInspector to support arbitrary nesting for XML Schemas public abstract class BaseXmlInspector implements DomInspector { // // Protected members // protected Log mLog = LogUtils.getLog( getClass() ); // // Private members // /** * Shared DOM to store this Inspector's source XML. *

* This member is private because, as pointed out here: "There's no * requirement that a DOM be thread safe, so applications need to make sure that threads are * properly synchronized for concurrent access to [a shared] DOM. This is true even if you're * just invoking read operations". */ private Element mRoot; private final PropertyStyle mRestrictAgainstObject; private final boolean mInferInheritanceHierarchy; // // Constructor // /** * Config-based constructor. *

* All BaseXmlInspector inspectors must be configurable, to allow specifying an XML file. */ protected BaseXmlInspector( BaseXmlInspectorConfig config ) { try { // Look up the XML file InputStream[] inputStreams = config.getInputStreams(); if ( inputStreams != null && inputStreams.length > 0 ) { mRoot = getDocumentElement( config.getResourceResolver(), config.getInputStreams() ); } else { // REFACTOR: support both at once Document[] documents = config.getDocuments(); if ( documents != null && documents.length > 0 ) { mRoot = getDocumentElement( documents ); } } if ( mRoot == null ) { throw InspectorException.newException( "No XML input file specified" ); } // Debug if ( mLog.isTraceEnabled() ) { mLog.trace( XmlUtils.documentToString( mRoot.getOwnerDocument(), false ) ); } // restrictAgainstObject mRestrictAgainstObject = config.getRestrictAgainstObject(); // inferInheritanceHierarchy mInferInheritanceHierarchy = config.isInferInheritanceHierarchy(); if ( mRestrictAgainstObject != null && mInferInheritanceHierarchy ) { throw InspectorException.newException( "When using restrictAgainstObject, inferInheritanceHierarchy is implied" ); } // validateAgainstClasses PropertyStyle validateAgainstClasses = config.getValidateAgainstClasses(); if ( validateAgainstClasses != null ) { String topLevelTypeAttribute = getTopLevelTypeAttribute(); String extendsAttribute = getExtendsAttribute(); String nameAttribute = getNameAttribute(); String typeAttribute = getTypeAttribute(); // For each entity... Element entity = XmlUtils.getChildWithAttribute( mRoot, topLevelTypeAttribute ); while ( entity != null ) { // ...the maps to a Java class... String topLevelType = entity.getAttribute( topLevelTypeAttribute ); Class actualClass = ClassUtils.niceForName( topLevelType ); if ( actualClass != null ) { // ...check its extends... String extendz = entity.getAttribute( extendsAttribute ); Class actualSuperclass = actualClass.getSuperclass(); if ( !"".equals( extendz ) && !extendz.equals( actualSuperclass.getName() ) ) { throw InspectorException.newException( actualClass + " extends " + actualSuperclass + ", not '" + extendz + "'" ); } // ...then for each property... Map actualProperties = validateAgainstClasses.getProperties( topLevelType ); Element property = XmlUtils.getChildWithAttribute( entity, nameAttribute ); while ( property != null ) { // ...check it exists String propertyName = property.getAttribute( nameAttribute ); Property actualProperty = actualProperties.get( propertyName ); if ( actualProperty == null ) { throw InspectorException.newException( actualClass + " does not define a property '" + propertyName + "'" ); } String propertyType = property.getAttribute( typeAttribute ); String actualType = actualProperty.getType(); if ( !"".equals( propertyType ) && !propertyType.equals( actualType ) ) { throw InspectorException.newException( actualClass + " defines property '" + propertyName + "' to be " + actualType + ", not '" + propertyType + "'" ); } property = XmlUtils.getSiblingWithAttribute( property, nameAttribute ); } } entity = XmlUtils.getSiblingWithAttribute( entity, topLevelTypeAttribute ); } } } catch ( Exception e ) { throw InspectorException.newException( e ); } } // // 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. */ 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 { Document document; Element entity; ValueAndDeclaredType valueAndDeclaredType; Map parentAttributes = null; // "There's no requirement that a DOM be thread safe, so applications need to make sure // that threads are properly synchronized for concurrent access to [a shared] DOM. This // is true even if you're just invoking read operations" // // https://issues.apache.org/jira/browse/XERCESJ-727 synchronized ( mRoot ) { // If the path has a parent... if ( names != null && names.length > 0 ) { // ...inspect its property for useful attributes... Element propertyInParent = (Element) traverse( toInspect, type, true, names ).getValue(); if ( propertyInParent != null ) { parentAttributes = inspectProperty( propertyInParent ); } } // ...otherwise, just start at the end point valueAndDeclaredType = traverse( toInspect, type, false, names ); if ( valueAndDeclaredType.getValue() == null ) { if ( parentAttributes == null || parentAttributes.isEmpty() ) { return null; } document = XmlUtils.newDocument(); entity = document.createElementNS( NAMESPACE, ENTITY ); } else { // Inspect traits document = XmlUtils.newDocument(); entity = document.createElementNS( NAMESPACE, ENTITY ); inspectTraits( (Element) valueAndDeclaredType.getValue(), entity ); // Nothing of consequence to return? if ( !entity.hasChildNodes() && entity.getAttributes().getLength() == 0 && parentAttributes == null ) { return null; } } } Element root = document.createElementNS( NAMESPACE, ROOT ); root.setAttribute( VERSION, "1.0" ); document.appendChild( root ); root.appendChild( entity ); // Add parent attributes (if any) XmlUtils.setMapAsAttributes( entity, parentAttributes ); // Use the declared type so as to align with other Inspectors if ( valueAndDeclaredType.getDeclaredType() != null ) { entity.setAttribute( TYPE, valueAndDeclaredType.getDeclaredType() ); } // Return the root return root; } catch ( Exception e ) { throw InspectorException.newException( e ); } } // // Protected methods // /** * Parse the given InputStreams into a single DOM Document, and return its root. * * @param resolver * helper in case getDocumentElement needs to resolve references defined * in the InputStream. */ protected Element getDocumentElement( ResourceResolver resolver, InputStream... files ) throws Exception { int length = files.length; Document[] documents = new Document[length]; for ( int loop = 0; loop < length; loop++ ) { documents[loop] = XmlUtils.parse( files[loop] ); } return getDocumentElement( documents ); } /** * Merge the given DOM Documents into a single Document, and return its root. */ protected Element getDocumentElement( Document... documents ) throws Exception { Document documentMaster = null; for ( Document document : documents ) { if ( !document.hasChildNodes() ) { continue; } preprocessDocument( document ); if ( documentMaster == null || !documentMaster.hasChildNodes() ) { documentMaster = document; continue; } XmlUtils.combineElements( documentMaster.getDocumentElement(), document.getDocumentElement(), getTopLevelTypeAttribute(), getNameAttribute() ); } if ( documentMaster == null ) { return null; } return documentMaster.getDocumentElement(); } /** * Hook for subclasses to preprocess the document after the Inspector is initialized. *

* For example, HibernateInspector preprocesses the class names in Hibernate * mapping files to make them fully qualified. * * @param document * DOM of XML being processed */ protected void preprocessDocument( Document document ) { // Do nothing by default } /** * 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. */ protected void inspectTraits( Element toInspect, Element toAddTo ) { if ( toInspect == null ) { return; } Document document = toAddTo.getOwnerDocument(); // Do 'extends' attribute first String extendsAttribute = getExtendsAttribute(); if ( extendsAttribute != null ) { if ( toInspect.hasAttribute( extendsAttribute ) ) { inspectTraits( (Element) traverse( null, toInspect.getAttribute( extendsAttribute ), false ).getValue(), toAddTo ); } } // Next, inspect each child... Element element = document.createElementNS( NAMESPACE, ENTITY ); inspectTraitSiblings( element, XmlUtils.getFirstChildElement( toInspect ) ); // ...and combine them all. Note the element may already exist from the superclass, // and its attributes will get overridden by the subclass XmlUtils.combineElements( toAddTo, element, NAME, NAME ); } /** * Inspect the given trait, looping over its siblings. *

* 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. */ protected void inspectTraitSiblings( Element toAddTo, Element toInspect ) { Element toInspectToUse = toInspect; while ( toInspectToUse != null ) { Element inspectedTrait = inspectTrait( toAddTo.getOwnerDocument(), toInspectToUse ); if ( inspectedTrait != null ) { toAddTo.appendChild( inspectedTrait ); } toInspectToUse = XmlUtils.getNextSiblingElement( toInspectToUse ); } } /** * Inspect the given Element and return a Map of attributes if it is a trait. *

* It is this method's responsibility to decide whether the given Element does, in fact, qualify * as a 'trait' - based on its own rules. * * @param toInspect * DOM element to inspect */ protected Element inspectTrait( Document toAddTo, Element toInspect ) { // Properties Map propertyAttributes = inspectProperty( toInspect ); if ( propertyAttributes != null && !propertyAttributes.isEmpty() ) { Element child = toAddTo.createElementNS( NAMESPACE, PROPERTY ); XmlUtils.setMapAsAttributes( child, propertyAttributes ); return child; } // Actions Map actionAttributes = inspectAction( toInspect ); if ( actionAttributes != null && !actionAttributes.isEmpty() ) { // Sanity check if ( propertyAttributes != null ) { throw InspectorException.newException( "Ambigious match: " + toInspect.getNodeName() + " matches as both a property and an action" ); } Element child = toAddTo.createElementNS( NAMESPACE, ACTION ); XmlUtils.setMapAsAttributes( child, actionAttributes ); return child; } return null; } /** * Inspect the given Element and return a Map of attributes if it is a property. *

* It is this method's responsibility to decide whether the given Element does, in fact, qualify * as a 'property' - based on its own rules. Does nothing by default. * * @param toInspect * DOM element to inspect * @return a Map of the property's attributes, or null if this Element is not a property */ protected Map inspectProperty( Element toInspect ) { return null; } /** * Inspect the given Element and return a Map of attributes if it is an action. *

* It is this method's responsibility to decide whether the given Element does, in fact, qualify * as an 'action' - based on its own rules. Does nothing by default. * * @param toInspect * DOM element to inspect * @return a Map of the property's attributes, or null if this Element is not an action */ protected Map inspectAction( Element toInspect ) { return null; } /** * @return the Element (may be null) and its declared type (not actual type). Never null. * If the declared type within the ValueAndDeclaredType is null, inspection will be * aborted */ protected ValueAndDeclaredType traverse( Object toTraverse, String type, boolean onlyToParent, String... names ) { // If given a non-null Object, use it to restrictAgainstObject String typeToInspect = type; String[] namesToInspect = names; Object traverseAgainstObject = null; String declaredType = null; if ( toTraverse != null && mRestrictAgainstObject != null ) { ValueAndDeclaredType valueAndDeclaredType = mRestrictAgainstObject.traverse( toTraverse, typeToInspect, onlyToParent, namesToInspect ); traverseAgainstObject = valueAndDeclaredType.getValue(); if ( valueAndDeclaredType.getDeclaredType() != null ) { declaredType = valueAndDeclaredType.getDeclaredType(); } if ( traverseAgainstObject == null ) { return new ValueAndDeclaredType( null, declaredType ); } if ( onlyToParent ) { namesToInspect = new String[] { namesToInspect[namesToInspect.length - 1] }; } else { namesToInspect = null; } typeToInspect = traverseAgainstObject.getClass().getName(); } if ( declaredType == null ) { declaredType = typeToInspect; } // Validate type String topLevelTypeAttribute = getTopLevelTypeAttribute(); Element topLevelElement = XmlUtils.getChildWithAttributeValue( mRoot, topLevelTypeAttribute, typeToInspect ); if ( topLevelElement == null ) { if ( traverseAgainstObject == null && !mInferInheritanceHierarchy ) { return new ValueAndDeclaredType( null, declaredType ); } // If using mRestrictAgainstObject or mInferInheritanceHierarchy, attempt to match // superclasses by checking against the Java class heirarchy Class actualClass; if ( traverseAgainstObject != null ) { actualClass = traverseAgainstObject.getClass(); } else { actualClass = ClassUtils.niceForName( typeToInspect ); if ( actualClass == null ) { return new ValueAndDeclaredType( null, typeToInspect ); } } while ( topLevelElement == null ) { actualClass = actualClass.getSuperclass(); if ( actualClass == null ) { break; } topLevelElement = XmlUtils.getChildWithAttributeValue( mRoot, topLevelTypeAttribute, actualClass.getName() ); } if ( topLevelElement == null ) { return new ValueAndDeclaredType( null, declaredType ); } } Element elementWithNamedChildren = traverseFromTopLevelTypeToNamedChildren( topLevelElement ); if ( namesToInspect == null || elementWithNamedChildren == null ) { return new ValueAndDeclaredType( elementWithNamedChildren, declaredType ); } int length = namesToInspect.length; if ( length == 0 ) { return new ValueAndDeclaredType( elementWithNamedChildren, declaredType ); } // Traverse names String extendsAttribute = getExtendsAttribute(); String nameAttribute = getNameAttribute(); String typeAttribute = getTypeAttribute(); String referenceAttribute = getReferenceAttribute(); // For each name... for ( int loop = 0; loop < length; loop++ ) { String name = namesToInspect[loop]; declaredType = null; // ...find the property with that name Element property = XmlUtils.getChildWithAttributeValue( elementWithNamedChildren, nameAttribute, name ); // If none, XML structure may support 'extends', so jump across to the extended element // and search for named properties there if ( property == null && extendsAttribute != null ) { while ( true ) { // ('extends' may be several levels deep) if ( !elementWithNamedChildren.hasAttribute( extendsAttribute ) ) { break; } String childExtends = elementWithNamedChildren.getAttribute( extendsAttribute ); elementWithNamedChildren = XmlUtils.getChildWithAttributeValue( mRoot, topLevelTypeAttribute, childExtends ); if ( elementWithNamedChildren == null ) { break; } property = XmlUtils.getChildWithAttributeValue( elementWithNamedChildren, nameAttribute, name ); if ( property != null ) { break; } } } // If still none, XML structure may support 'reference', so search for referenced // properties if ( property == null && referenceAttribute != null ) { property = XmlUtils.getChildWithAttributeValue( elementWithNamedChildren, referenceAttribute, name ); if ( property == null ) { return new ValueAndDeclaredType( null, null ); } // Traverse to new top-level element of the given declaredType declaredType = name; } // If still none, give up if ( property == null ) { return new ValueAndDeclaredType( null, null ); } if ( onlyToParent && loop >= ( length - 1 ) ) { return new ValueAndDeclaredType( property, declaredType ); } if ( declaredType == null ) { // Fetch typeAttribute (if any) if ( property.hasAttribute( typeAttribute ) ) { declaredType = property.getAttribute( typeAttribute ); } // Support nested elements with named children (with or without a typeAttribute) elementWithNamedChildren = traverseFromTopLevelTypeToNamedChildren( property ); if ( XmlUtils.getChildWithAttribute( elementWithNamedChildren, nameAttribute ) != null ) { continue; } // If no typeAttribute, support referenceAttribute (though typeAttribute takes // precedence) if ( !property.hasAttribute( typeAttribute ) ) { if ( referenceAttribute == null || XmlUtils.getChildWithAttribute( elementWithNamedChildren, referenceAttribute ) == null ) { throw InspectorException.newException( "Property " + name + " in entity " + topLevelElement.getAttribute( typeAttribute ) + " has no @" + typeAttribute + " attribute in the XML, so cannot navigate to " + type + ArrayUtils.toString( namesToInspect, StringUtils.SEPARATOR_FORWARD_SLASH, true, false ) ); } continue; } } // Traverse to new top-level element of the given declaredType topLevelElement = XmlUtils.getChildWithAttributeValue( mRoot, topLevelTypeAttribute, declaredType ); if ( topLevelElement == null ) { return new ValueAndDeclaredType( null, declaredType ); } // For ref lookups, topLevelElement may have an additional typeAttribute that is // different from topLevelTypeAttribute if ( topLevelElement.hasAttribute( typeAttribute ) ) { declaredType = topLevelElement.getAttribute( typeAttribute ); } elementWithNamedChildren = traverseFromTopLevelTypeToNamedChildren( topLevelElement ); if ( elementWithNamedChildren == null ) { return new ValueAndDeclaredType( null, declaredType ); } } return new ValueAndDeclaredType( elementWithNamedChildren, declaredType ); } /** * The attribute on top-level elements that uniquely identifies them. */ protected String getTopLevelTypeAttribute() { return TYPE; } /** * The attribute on child elements that uniquely identifies them. */ protected String getNameAttribute() { return NAME; } /** * The attribute on child elements that identifies another top-level element. *

* This is necessary for path traversal. If an XML format does not specify a way to traverse * from a child to another top-level element, the Inspector cannot find information along paths * (eg. foo/bar/baz). There is a way around this but, on balance, we * decided against it (see http://blog.kennardconsulting.com/2008/01/ask-your-father.html). */ protected String getTypeAttribute() { return TYPE; } /** * The attribute on top-level elements that identifies a superclass relationship (if any). */ protected String getExtendsAttribute() { return null; } /** * The attribute on child elements that identifies a reference to another element (if any). * Note that typeAttribute will always take precedence over * referenceAttribute. */ protected String getReferenceAttribute() { return null; } /** * Traverse from the given top-level element (as per getTopLevelTypeAttribute) to * the element which contains named children (as per getNameAttribute). In many * cases this is one and the same, so by default this method simply returns the given element. *

* Subclasses can override this method if they need to do some intermediate traversal. * * @return the element containing named children, or null if no such element */ protected Element traverseFromTopLevelTypeToNamedChildren( Element topLevel ) { return topLevel; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy