org.metawidget.pipeline.base.BasePipeline Maven / Gradle / Ivy
// 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.pipeline.base;
import static org.metawidget.inspector.InspectionResultConstants.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.metawidget.inspectionresultprocessor.iface.DomInspectionResultProcessor;
import org.metawidget.inspectionresultprocessor.iface.InspectionResultProcessor;
import org.metawidget.inspectionresultprocessor.iface.InspectionResultProcessorException;
import org.metawidget.inspector.iface.DomInspector;
import org.metawidget.inspector.iface.Inspector;
import org.metawidget.layout.iface.AdvancedLayout;
import org.metawidget.layout.iface.Layout;
import org.metawidget.widgetbuilder.iface.AdvancedWidgetBuilder;
import org.metawidget.widgetbuilder.iface.WidgetBuilder;
import org.metawidget.widgetprocessor.iface.AdvancedWidgetProcessor;
import org.metawidget.widgetprocessor.iface.WidgetProcessor;
import org.metawidget.widgetprocessor.iface.WidgetProcessorException;
/**
* Convenience implementation for implementing pipelines (see
* http://metawidget.org/doc/reference/en/html/ch02.html)
*
* Use of BasePipeline when developing Metawidgets is entirely optional. However, it provides a
* level of functionality and structure to the code which most Metawidgets will benefit from.
*
* Specifically, BasePipeline provides support for:
*
* - Inspectors, InspectionResultProcessors, WidgetBuilders, WidgetProcessors and Layouts
* - single/compound widgets
* - stubs/stub attributes
* - read-only/active widgets
* - maximum inspection depth
*
* This base class abstracts the pipeline without enforcing which XML libraries to use. Most
* subclasses will choose org.metawidget.pipeline.w3c.W3CPipeline
, which uses
* org.w3c.dom
.
*
* BasePipeline is not Thread-safe.
*
* Note: this class is located in org.metawidget.pipeline.base
, as opposed to just
* org.metawidget.pipeline
, to make it easier to integrate GWT (which is bad at
* ignoring sub-packages such as org.metawidget.pipeline.w3c
).
*
* @author Richard Kennard
*/
public abstract class BasePipeline {
//
// Private statics
//
private static final int DEFAULT_MAXIMUM_INSPECTION_DEPTH = 10;
//
// Private members
//
private boolean mReadOnly;
private int mMaximumInspectionDepth = DEFAULT_MAXIMUM_INSPECTION_DEPTH;
private boolean mNeedsConfiguring = true;
private Inspector mInspector;
private List> mInspectionResultProcessors;
private WidgetBuilder mWidgetBuilder;
private List> mWidgetProcessors;
private Layout mLayout;
//
// Public methods
//
public void setReadOnly( boolean readOnly ) {
mReadOnly = readOnly;
}
public boolean isReadOnly() {
return mReadOnly;
}
public int getMaximumInspectionDepth() {
return mMaximumInspectionDepth;
}
/**
* Sets the maximum depth of inspection.
*
* Metawidget renders most non-primitve types by using nested Metawidgets. This value limits the
* number of nestings.
*
* This can be useful in detecing cyclic references. Although BaseObjectInspector
* -derived Inspectors are capable of detecting cyclic references, other Inspectors may not be.
* For example, BaseXmlInspector
-derived Inspectors cannot because they only test
* types, not actual objects.
*
* @param maximumInspectionDepth
* 0 for top-level only, 1 for 1 level deep etc.
*/
public void setMaximumInspectionDepth( int maximumInspectionDepth ) {
mMaximumInspectionDepth = maximumInspectionDepth;
}
public void setNeedsConfiguring() {
mNeedsConfiguring = true;
}
/**
* Configures the Metawidget, then sets a flag so that subsequent calls to
* configureOnce
do nothing.
*
* The flag can be reset by calling setNeedsConfiguring
.
*/
public void configureOnce() {
if ( !mNeedsConfiguring ) {
return;
}
mNeedsConfiguring = false;
configure();
}
public void setInspector( Inspector inspector ) {
mInspector = inspector;
}
public Inspector getInspector() {
configureOnce();
return mInspector;
}
/**
* Gets the List of InspectionResultProcessors.
*
* This pipeline only references a single Inspector and single WidgetBuilder. It relies on
* CompositeInspector and CompositeWidgetBuilder to support multiples, which allows the
* combination algorithm itself to be pluggable.
*
* We use a List of InspectionResultProcessors, however, so as to be consistent with
* WidgetProcessors. Note ordering of InspectionResultProcessors is significant.
*/
public List> getInspectionResultProcessors() {
configureOnce();
return mInspectionResultProcessors;
}
public void setInspectionResultProcessors( InspectionResultProcessor... inspectionResultProcessors ) {
if ( inspectionResultProcessors == null ) {
mInspectionResultProcessors = null;
} else {
mInspectionResultProcessors = new ArrayList>( Arrays.asList( inspectionResultProcessors ) );
}
}
public void addInspectionResultProcessor( InspectionResultProcessor inspectionResultProcessor ) {
configureOnce();
if ( mInspectionResultProcessors == null ) {
mInspectionResultProcessors = new ArrayList>();
} else if ( mInspectionResultProcessors.contains( inspectionResultProcessor ) ) {
throw InspectionResultProcessorException.newException( "List of InspectionResultProcessors already contains " + inspectionResultProcessor.getClass() );
}
mInspectionResultProcessors.add( inspectionResultProcessor );
}
public void removeInspectionResultProcessor(
InspectionResultProcessor inspectionResultProcessors ) {
configureOnce();
if ( mInspectionResultProcessors == null ) {
return;
}
mInspectionResultProcessors.remove( inspectionResultProcessors );
}
public void setWidgetBuilder( WidgetBuilder widgetBuilder ) {
mWidgetBuilder = widgetBuilder;
}
public WidgetBuilder getWidgetBuilder() {
configureOnce();
return mWidgetBuilder;
}
/**
* Gets the List of WidgetProcessors.
*
* This pipeline only references a single Inspector and single WidgetBuilder. It relies on
* CompositeInspector and CompositeWidgetBuilder to support multiples, which allows the
* combination algorithm itself to be pluggable.
*
* We cannot use this same approach for WidgetProcessors, however, because we want to support
* event handling. We want to allow, for example:
*
*
* metawidget.addWidgetProcessor( new WidgetProcessor() {
* ...handle event...
* }
*
*
* This mechanism cannot be delegated to a CompositeWidgetProcessor, because WidgetProcessors
* must be immutable, and we want to allow event handlers that are non-static anonymous inner
* classes.
*
* There, we use a List of WidgetProcessors. Note ordering of WidgetProcessors is significant.
*/
public List> getWidgetProcessors() {
configureOnce();
return mWidgetProcessors;
}
public void setWidgetProcessors( WidgetProcessor... widgetProcessors ) {
if ( widgetProcessors == null ) {
mWidgetProcessors = null;
} else {
mWidgetProcessors = new ArrayList>( Arrays.asList( widgetProcessors ) );
}
}
public void addWidgetProcessor( WidgetProcessor widgetProcessor ) {
configureOnce();
if ( mWidgetProcessors == null ) {
mWidgetProcessors = new ArrayList>();
} else if ( mWidgetProcessors.contains( widgetProcessor ) ) {
throw WidgetProcessorException.newException( "List of WidgetProcessors already contains " + widgetProcessor.getClass() );
}
mWidgetProcessors.add( widgetProcessor );
}
public void removeWidgetProcessor( WidgetProcessor widgetProcessor ) {
configureOnce();
if ( mWidgetProcessors == null ) {
return;
}
mWidgetProcessors.remove( widgetProcessor );
}
public Layout getLayout() {
configureOnce();
return mLayout;
}
/**
* Set the Layout to use for the Metawidget.
*/
public void setLayout( Layout layout ) {
mLayout = layout;
}
/**
* 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 mirrors the Inspector
interface. Internally it looks up the
* Inspector to use. It is a useful hook for subclasses wishing to inspect different Objects
* using our same Inspector
.
*
* In addition, this method runs the InspectionResultProcessors
.
*/
public String inspect( Object toInspect, String type, String... names ) {
E element = inspectAsDom( toInspect, type, names );
return elementToString( element );
}
/**
* 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 mirrors the DomInspector
interface. Internally it looks up the
* Inspector to use. It is a useful hook for subclasses wishing to inspect different Objects
* using our same Inspector
.
*
* In addition, this method runs the InspectionResultProcessors
.
*/
public E inspectAsDom( Object toInspect, String type, String... names ) {
configureOnce();
if ( mInspector == null ) {
throw new NullPointerException( "No inspector configured" );
}
Object inspectionResult;
if ( mInspector instanceof DomInspector> ) {
inspectionResult = ( (DomInspector>) mInspector ).inspectAsDom( toInspect, type, names );
} else {
inspectionResult = mInspector.inspect( toInspect, type, names );
}
if ( inspectionResult == null ) {
return null;
}
return processInspectionResult( inspectionResult, toInspect, type, names );
}
/**
* Build widgets from the given XML inspection result.
*
* Note: the BasePipeline
expects the XML to be passed in externally, rather than
* fetching it itself, because some XML inspections may be asynchronous.
*/
public void buildWidgets( E inspectionResult )
throws Exception {
configureOnce();
startBuild();
if ( inspectionResult != null ) {
// Build simple widget (from the top-level entity)
E entity = getFirstChildElement( inspectionResult );
// Sanity check
String elementName = getElementName( entity );
if ( !ENTITY.equals( elementName ) ) {
throw new Exception( "Top-level element name should be " + ENTITY + ", not " + elementName );
}
E nextSiblingElement = getNextSiblingElement( entity );
if ( nextSiblingElement != null ) {
throw new Exception( "Top-level " + ENTITY + " element has a sibling " + getElementName( nextSiblingElement ) + " element" );
}
// Metawidget-wide read-only
Map attributes = getAttributesAsMap( entity );
if ( isReadOnly() ) {
attributes.put( READ_ONLY, TRUE );
}
// Build top-level widget.
//
// This includes invoking all WidgetBuilders, such as OverriddenWidgetBuilder. It is
// a little counter-intuitive that there can ever be an override of the top-level
// element. However, if we go down the path that builds a single widget (eg. doesn't
// invoke buildCompoundWidget), then our child is at the same top-level as us, and there
// are some scenarios (like Java Server Faces POST backs) where we need to re-identify
// that
W widget = buildWidget( ENTITY, attributes );
// If mWidgetBuilder.buildWidget returns null, try buildCompoundWidget (from our child
// elements)
if ( widget == null ) {
buildCompoundWidget( entity );
} else {
widget = processWidget( widget, ENTITY, attributes );
if ( widget != null ) {
layoutWidget( widget, ENTITY, attributes );
}
}
}
// Even if no inspectors match, we still call startBuild()/endBuild()
// because:
//
// 1. you can use a Metawidget purely for layout, with no inspection
// 2. it makes us behave better in visual builder tools when dropping
// child widgets in
endBuild();
}
/**
* Copies this pipeline's values into another pipeline. Useful for when a
* Metawidget creates a nested Metawidget.
*
* Special behaviour is:
*
* - the given pipeline has setReadOnly if the current pipeline has setReadOnly or if
* the attributes map contains
READ_ONLY
* - the given pipeline is initialised with a maximumInspectionDepth of 1 less than the
* current maximumInspectionDepth. This is so that, as nesting continues, eventually the
* maximumInspectionDepth reaches zero
* - the given pipeline is initialised with the same Inspectors, InspectionResultProcessors,
* WidgetBuilders, WidgetProcessors and Layouts as the current pipeline. This is safe because
* they are all immutable
*
*
* @param attributes
* may be null
*/
public void initNestedPipeline( BasePipeline nestedPipeline, Map attributes ) {
nestedPipeline.setReadOnly( isReadOnly() || ( attributes != null && TRUE.equals( attributes.get( READ_ONLY ) ) ) );
nestedPipeline.setMaximumInspectionDepth( getMaximumInspectionDepth() - 1 );
// Inspectors, InspectionResultProcessors, WidgetBuilders,
// WidgetProcessors and Layouts can be shared because they are immutable. However note that
// the InspectionResultProcessor and WidgetProcessor Lists are defensively copied
nestedPipeline.setInspector( getInspector() );
nestedPipeline.setWidgetBuilder( getWidgetBuilder() );
nestedPipeline.setLayout( getLayout() );
if ( mInspectionResultProcessors == null ) {
nestedPipeline.mInspectionResultProcessors = null;
} else {
nestedPipeline.mInspectionResultProcessors = new ArrayList>( mInspectionResultProcessors );
}
if ( mWidgetProcessors == null ) {
nestedPipeline.mWidgetProcessors = null;
} else {
nestedPipeline.mWidgetProcessors = new ArrayList>( mWidgetProcessors );
}
}
//
// Protected methods
//
/**
* Build a compound widget by iterating through children of the given
* element, calling buildWidget
and addWidget
on
* each.
*/
protected void buildCompoundWidget( E entity )
throws Exception {
E child = getFirstChildElement( entity );
int loop = 0;
while ( child != null ) {
loop++;
// Sanity check
String elementName = getElementName( child );
if ( !PROPERTY.equals( elementName ) && !ACTION.equals( elementName ) ) {
throw new Exception( "Child element #" + loop + " should be " + PROPERTY + " or " + ACTION + ", not " + elementName );
}
Map attributes = getAttributesAsMap( child );
String childName = attributes.get( NAME );
if ( childName == null || "".equals( childName ) ) {
throw new Exception( "Child element #" + loop + " has no @" + NAME );
}
// Metawidget as a whole may have had setReadOnly( true )
//
// Note: we cannot do this in WidgetBuilderUtils.isReadOnly because:
//
// 1) There is not a common Metawidget class that we can pass to
// WidgetBuilderUtils in order for it to call isReadOnly
// 2) This way WidgetBuilders/Layouts etc don't have to worry about
// checking 2 places for readOnly-ness
//
// In addition, we are trying to keep the exact nature of the 'readOnly' mechanism (i.e.
// set on attribute, or set on overall Metawidget) out of the
// WidgetBuilders/WidgetProcessors/Layouts. This is because not everybody will need/want
// a Metawidget-level 'setReadOnly'
boolean forcedReadOnly = false;
if ( !TRUE.equals( attributes.get( READ_ONLY ) ) && isReadOnly() ) {
attributes.put( READ_ONLY, TRUE );
forcedReadOnly = true;
}
try {
W widget = buildWidget( elementName, attributes );
if ( widget == null ) {
if ( mMaximumInspectionDepth <= 0 ) {
continue;
}
// If setReadOnly( true ), remove our forced attribute so the nestedMetawidget
// can differentiate whether it was forced or in the inspector XML
if ( forcedReadOnly ) {
attributes.remove( READ_ONLY );
}
widget = buildNestedMetawidget( attributes );
}
Map additionalAttributes = getAdditionalAttributes( widget );
if ( additionalAttributes != null ) {
attributes.putAll( additionalAttributes );
}
widget = processWidget( widget, elementName, attributes );
// A WidgetProcessor could return null to cancel the widget
if ( widget == null ) {
continue;
}
layoutWidget( widget, elementName, attributes );
} finally {
child = getNextSiblingElement( child );
}
}
}
//
// Protected abstract methods
//
protected abstract E stringToElement( String xml );
/**
* Serialize the given element to an XML String.
*
* @param element
* the element to serialize. May be null.
*/
protected abstract String elementToString( E element );
protected abstract E getFirstChildElement( E parent );
protected abstract E getNextSiblingElement( E element );
protected abstract String getElementName( E element );
protected abstract Map getAttributesAsMap( E element );
protected abstract void configure();
protected void startBuild() {
M pipelineOwner = getPipelineOwner();
if ( mWidgetBuilder instanceof AdvancedWidgetBuilder, ?> ) {
( (AdvancedWidgetBuilder) mWidgetBuilder ).onStartBuild( pipelineOwner );
}
if ( mWidgetProcessors != null ) {
for ( WidgetProcessor widgetProcessor : mWidgetProcessors ) {
if ( widgetProcessor instanceof AdvancedWidgetProcessor, ?> ) {
( (AdvancedWidgetProcessor) widgetProcessor ).onStartBuild( pipelineOwner );
}
}
}
// (layout can be null if no path, in an IDE visual builder)
if ( mLayout instanceof AdvancedLayout, ?, ?> ) {
AdvancedLayout advancedLayout = (AdvancedLayout) mLayout;
advancedLayout.onStartBuild( pipelineOwner );
advancedLayout.startContainerLayout( pipelineOwner, pipelineOwner );
}
}
/**
* @param inspectionResult
* may be a String of XML, or an E, depending on whether the
* Inspector was a DomInspector
*/
@SuppressWarnings( "unchecked" )
protected E processInspectionResult( Object inspectionResult, Object toInspect, String type, String... names ) {
Object inspectionResultToProcess = inspectionResult;
if ( mInspectionResultProcessors != null ) {
M pipelineOwner = getPipelineOwner();
for ( InspectionResultProcessor inspectionResultProcessor : mInspectionResultProcessors ) {
if ( inspectionResultProcessor instanceof DomInspectionResultProcessor, ?> ) {
if ( inspectionResultToProcess instanceof String ) {
inspectionResultToProcess = stringToElement( (String) inspectionResultToProcess );
}
DomInspectionResultProcessor domInspectionResultProcessor = (DomInspectionResultProcessor) inspectionResultProcessor;
E inspectionResultToProcessElement = (E) inspectionResultToProcess;
inspectionResultToProcess = domInspectionResultProcessor.processInspectionResultAsDom( inspectionResultToProcessElement, pipelineOwner, toInspect, type, names );
} else {
if ( !( inspectionResultToProcess instanceof String ) ) {
E inspectionResultToProcessElement = (E) inspectionResultToProcess;
inspectionResultToProcess = elementToString( inspectionResultToProcessElement );
}
inspectionResultToProcess = inspectionResultProcessor.processInspectionResult( (String) inspectionResultToProcess, pipelineOwner, toInspect, type, names );
}
// An InspectionResultProcessor could return null to cancel the
// inspection
if ( inspectionResultToProcess == null ) {
return null;
}
}
}
if ( inspectionResultToProcess instanceof String ) {
return stringToElement( (String) inspectionResultToProcess );
}
return (E) inspectionResultToProcess;
}
/**
* Returns additional attributes associated with the widget.
*
* At the very least, this method should be implemented to support returning additional
* attributes from stubs.
*
* @return the additional attributes. May be null
*/
protected abstract Map getAdditionalAttributes( W widget );
protected W buildWidget( String elementName, Map attributes ) {
if ( mWidgetBuilder == null ) {
return null;
}
return mWidgetBuilder.buildWidget( elementName, attributes,
getPipelineOwner() );
}
/**
* Process the built widget.
*/
protected W processWidget( W widget, String elementName, Map attributes ) {
W processedWidget = widget;
if ( mWidgetProcessors != null ) {
M pipelineOwner = getPipelineOwner();
for ( WidgetProcessor widgetProcessor : mWidgetProcessors ) {
processedWidget = widgetProcessor.processWidget( processedWidget, elementName, attributes, pipelineOwner );
// A WidgetProcessor could return null to cancel the widget
if ( processedWidget == null ) {
return null;
}
}
}
return processedWidget;
}
protected abstract M buildNestedMetawidget( Map attributes )
throws Exception;
protected abstract M getPipelineOwner();
/**
* Lays out the built and processed widget.
*/
protected void layoutWidget( W widget, String elementName, Map attributes ) {
M pipelineOwner = getPipelineOwner();
mLayout.layoutWidget( widget, elementName, attributes, pipelineOwner, pipelineOwner );
}
protected void endBuild() {
M pipelineOwner = getPipelineOwner();
// (layout can be null if no path, in an IDE visual builder)
if ( mLayout instanceof AdvancedLayout, ?, ?> ) {
AdvancedLayout advancedLayout = (AdvancedLayout) mLayout;
advancedLayout.endContainerLayout( pipelineOwner, pipelineOwner );
advancedLayout.onEndBuild( pipelineOwner );
}
if ( mWidgetProcessors != null ) {
for ( WidgetProcessor widgetProcessor : mWidgetProcessors ) {
if ( widgetProcessor instanceof AdvancedWidgetProcessor, ?> ) {
( (AdvancedWidgetProcessor) widgetProcessor ).onEndBuild( pipelineOwner );
}
}
}
if ( mWidgetBuilder instanceof AdvancedWidgetBuilder, ?> ) {
( (AdvancedWidgetBuilder) mWidgetBuilder ).onEndBuild( pipelineOwner );
}
}
}