org.metawidget.inspector.composite.CompositeInspector Maven / Gradle / Ivy
// 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.composite;
import static org.metawidget.inspector.InspectionResultConstants.*;
import org.metawidget.inspector.iface.DomInspector;
import org.metawidget.inspector.iface.Inspector;
import org.metawidget.inspector.iface.InspectorException;
import org.metawidget.util.ArrayUtils;
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;
/**
* Delegates inspection to one or more sub-inspectors, then combines the resulting DOMs.
*
* The combining algorithm works as follows. For each element:
*
* - top-level elements with the same
type
attribute in both DOMs are combined
* - child elements with the same
name
attribute in both DOMs are combined
* - the ordering of elements in the existing DOM is preserved. New child elements are added
* either at the end or immediately after the last combined child
*
- element attributes from the new DOM override ones in the existing DOM
*
*
* This algorithm should be suitable for most use cases, but one benefit of having a separate
* CompositeInspector is that developers can replace it with their own version, with its own
* combining algorithm, if required.
*
* Note: the name CompositeInspector refers to the Composite design pattern.
*
* @author Richard Kennard
*/
public class CompositeInspector
implements DomInspector {
//
// Private statics
//
private static final Log LOG = LogUtils.getLog( CompositeInspector.class );
//
// Private members
//
/* package private */final Inspector[] mInspectors;
//
// Constructor
//
public CompositeInspector( CompositeInspectorConfig config ) {
Inspector[] inspectors = config.getInspectors();
// Must have at least one Inspector. At least two, really, but one can be useful
// if we want to validate what the sub-Inspector is returning (ie. using LOG.debug)
if ( inspectors == null || inspectors.length == 0 ) {
throw InspectorException.newException( "CompositeInspector needs at least one Inspector" );
}
// Defensive copy
mInspectors = new Inspector[inspectors.length];
for ( int loop = 0, length = inspectors.length; loop < length; loop++ ) {
Inspector inspector = inspectors[loop];
for ( int checkDuplicates = 0; checkDuplicates < loop; checkDuplicates++ ) {
if ( mInspectors[checkDuplicates].equals( inspector ) ) {
throw InspectorException.newException( "CompositeInspector's list of Inspectors contains two of the same " + inspector.getClass().getName() );
}
}
mInspectors[loop] = inspector;
}
}
//
// 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 ) {
return inspect( null, toInspect, type, names );
}
/**
* If your architecture is strongly separated, some metadata may only be available in one tier
* (eg. JPA annotations in the backend) and some only available in another tier (eg.
* struts-config.xml in the front-end).
*
* For this, CompositeInspector
supplies this overloaded method outside the normal
* Inspector
interface. It takes an additional XML string of inspection results,
* and merges forthcoming inspection results with it.
*
* 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( String master, Object toInspect, String type, String... names ) {
Element element = inspectAsDom( XmlUtils.documentFromString( master ), toInspect, type, names );
if ( element == null ) {
return null;
}
return XmlUtils.nodeToString( element, false );
}
/**
* This method is marked final
as it delegates directly to
* inspectAsDom( Document, Object, String, String... )
. Subclasses should override
* that method instead.
*/
public final Element inspectAsDom( Object toInspect, String type, String... names ) {
return inspectAsDom( null, toInspect, type, names );
}
/**
* If your architecture is strongly separated, some metadata may only be available in one tier
* (eg. JPA annotations in the backend) and some only available in another tier (eg.
* struts-config.xml in the front-end).
*
* For this, CompositeInspector
supplies this overloaded method outside the normal
* DomInspector
interface. It takes an additional DOM of inspection results, and
* merges forthcoming inspection results with it.
*/
public Element inspectAsDom( Document masterDocument, Object toInspect, String type, String... names ) {
try {
Document masterDocumentToUse = runInspectors( masterDocument, toInspect, type, names );
if ( masterDocumentToUse == null || !masterDocumentToUse.hasChildNodes() ) {
if ( toInspect != null && type != null && LOG.isWarnEnabled() ) {
LOG.warn( "No inspectors matched path == {0}{1}", type, ArrayUtils.toString( names, StringUtils.SEPARATOR_FORWARD_SLASH, true, false ) );
}
return null;
}
// (debug)
if ( LOG.isDebugEnabled() ) {
String formattedXml = XmlUtils.documentToString( masterDocumentToUse, true );
LOG.debug( "Inspected {0}{1}\r\n{2}", type, ArrayUtils.toString( names, StringUtils.SEPARATOR_FORWARD_SLASH, true, false ), formattedXml );
}
// (warn)
Element root = masterDocumentToUse.getDocumentElement();
if ( toInspect != null && type != null && LOG.isWarnEnabled() && !root.hasChildNodes() ) {
LOG.warn( "No inspectors matched path == {0}{1}", type, ArrayUtils.toString( names, StringUtils.SEPARATOR_FORWARD_SLASH, true, false ) );
return root;
}
return root;
} catch ( Exception e ) {
throw InspectorException.newException( e );
}
}
//
// Protected methods
//
/**
* Run the sub-Inspectors on the given toInspect and combine the result.
*
* Subclasses may override this method to, say, run some other Inspectors concurrently.
*/
protected Document runInspectors( Document masterDocument, Object toInspect, String type, String... names )
throws Exception {
Document masterDocumentToUse = masterDocument;
// Run each Inspector...
for ( Inspector inspector : mInspectors ) {
// ...parse the result...
Document inspectionDocument = runInspector( inspector, toInspect, type, names );
// ...combine them...
masterDocumentToUse = combineInspectionResult( masterDocumentToUse, inspectionDocument );
}
// ...and return them
return masterDocumentToUse;
}
protected Document runInspector( Inspector inspector, Object toInspect, String type, String... names )
throws Exception {
// DomInspector...
if ( inspector instanceof DomInspector> ) {
@SuppressWarnings( "unchecked" )
DomInspector domInspector = (DomInspector) inspector;
Element element = domInspector.inspectAsDom( toInspect, type, names );
if ( element == null ) {
return null;
}
if ( LOG.isTraceEnabled() ) {
String xml = XmlUtils.nodeToString( element, true );
LOG.trace( "{0} inspected {1}{2}\r\n{3}", inspector.getClass(), type, ArrayUtils.toString( names, StringUtils.SEPARATOR_FORWARD_SLASH, true, false ), xml );
}
validate( element.getOwnerDocument() );
return element.getOwnerDocument();
}
// ...or just regular Inspector
String xml = inspector.inspect( toInspect, type, names );
if ( xml == null ) {
return null;
}
LOG.trace( "{0} inspected {1}{2}\r\n{3}", inspector.getClass(), type, ArrayUtils.toString( names, StringUtils.SEPARATOR_FORWARD_SLASH, true, false ), xml );
Document document = XmlUtils.documentFromString( xml );
validate( document );
return document;
}
protected Document combineInspectionResult( Document masterDocument, Document inspectionDocument ) {
// Short circuit...
if ( inspectionDocument == null || !inspectionDocument.hasChildNodes() ) {
return masterDocument;
}
if ( masterDocument == null || !masterDocument.hasChildNodes() ) {
return inspectionDocument;
}
// ...or full combine
XmlUtils.combineElements( masterDocument.getDocumentElement(), inspectionDocument.getDocumentElement(), TYPE, NAME );
return masterDocument;
}
/**
* Does nothing by default.
*
* Subclasses can override this method to hook in validation.
*
* @param document
* the inspection result DOM
*/
protected void validate( Document document )
throws Exception {
// Do nothing
}
}