// 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.faces.component;
import static org.metawidget.inspector.InspectionResultConstants.*;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import javax.faces.application.Application;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.component.UIParameter;
import javax.faces.component.UIViewRoot;
import javax.faces.component.ValueHolder;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.context.PartialViewContext;
import javax.faces.el.ValueBinding;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.PreRenderViewEvent;
import javax.faces.event.SystemEvent;
import javax.faces.event.SystemEventListener;
import javax.faces.validator.Validator;
import org.metawidget.config.iface.ConfigReader;
import org.metawidget.config.impl.BaseConfigReader;
import org.metawidget.faces.FacesUtils;
import org.metawidget.iface.MetawidgetException;
import org.metawidget.inspectionresultprocessor.iface.InspectionResultProcessor;
import org.metawidget.inspector.iface.Inspector;
import org.metawidget.layout.iface.Layout;
import org.metawidget.pipeline.w3c.W3CPipeline;
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.WidgetBuilderUtils;
import org.metawidget.util.XmlUtils;
import org.metawidget.util.simple.StringUtils;
import org.metawidget.widgetbuilder.iface.WidgetBuilder;
import org.metawidget.widgetprocessor.iface.WidgetProcessor;
import org.w3c.dom.Element;
/**
* Base Metawidget for Java Server Faces environments.
*
* Its default RendererType is table
.
*
*
Resolving Directly To A Single Widget
*
* If the entire Metawidget resolves directly to a single widget, Metawidget allows you to attach
* converters, facets and validators to the dynamically chosen component. Tags placed inside the
* Metawidget tag will be moved on to the generated component. For example:
*
* <m:metawidget value="#{user.name}">
* <f:validator validatorId="myValidator">
* </m:metawidget>
*
* Conceptually, UIMetawidget
should only extend UIComponentBase
. This is
* because it is not:
*
*
* a UIInput
, though it may contain input widgets
* a UIOutput
, though it may contain output widgets
* a ValueHolder
, as it does not use a Converter
* an EditableValueHolder
, as it does not use a Validator
*
*
* However by extending UIInput
, we enable this useful 'attach facets to a single
* widget' capability.
*
* @author Richard Kennard
*/
@SuppressWarnings( "deprecation" )
public abstract class UIMetawidget
extends UIInput {
//
// Public statics
//
/**
* Component-level attribute used to store metadata.
*/
public static final String COMPONENT_ATTRIBUTE_METADATA = "metawidget-metadata";
/**
* Component-level attribute used to prevent recreation.
*
* By default, Metawidget destroys and recreates every component after
* processUpdates
and before encodeBegin
. This allows components to
* update to reflect changed state in underlying domain objects. For example, components may
* change from being UIOutput
labels to UIInput
text boxes after the
* user clicks Edit
.
*
* Most components work well with this approach. Some, however, maintain internal state that
* would get lost if the component was destroyed and recreated. For example, the ICEfaces
* SelectInputDate
component keeps its popup state internally. If it is destroyed
* and recreated, the popup never appears.
*
* Such components can be marked with COMPONENT_ATTRIBUTE_NOT_RECREATABLE
to
* prevent their destruction and recreation. Of course, this somewhat impacts their flexibility.
* For example, a SelectInputDate
could not change its date format in response to
* another component on the form.
*
* COMPONENT_ATTRIBUTE_NOT_RECREATABLE
is also used to mark components that
* override default component generation, such as a UIStub
used to suppress a
* field.
*
* This attribute must be used in conjunction with OverriddenWidgetBuilder
.
*/
public static final String COMPONENT_ATTRIBUTE_NOT_RECREATABLE = "metawidget-not-recreatable";
/**
* Component-level attribute used to store metadata.
*/
public static final String COMPONENT_ATTRIBUTE_SECTION_DECORATOR = "metawidget-section-decorator";
//
// Private statics
//
/**
* Application-level attribute used to cache ConfigReader. This can also be used to inject a
* different ConfigReader if needed (ie. for Grails)
*/
private static final String APPLICATION_ATTRIBUTE_CONFIG_READER = "metawidget-config-reader";
private static final String DEFAULT_USER_CONFIG = "metawidget.xml";
private static final String COMPONENT_ATTRIBUTE_PARAMETER_PREFIX = "metawidget-parameter-";
/* package private */static final Log LOG = LogUtils.getLog( UIMetawidget.class );
/* package private */static boolean LOGGED_MISSING_CONFIG;
private static Boolean USE_PRERENDER_VIEW_EVENT;
/**
* The standard component family for this component.
*/
@SuppressWarnings( "hiding" )
private static final String COMPONENT_FAMILY = "org.metawidget";
//
// Private members
//
/* package private */boolean mExplicitRendererType;
/* package private */boolean mBuildWidgetsOnAjaxRequest;
private boolean mInspectFromParent;
private boolean mReadOnly;
private Map mClientProperties;
/* package private */Pipeline mPipeline;
/* package private */Object mBuildWidgetsSupport;
//
// Constructor
//
public UIMetawidget() {
mPipeline = newPipeline();
// Default config
FacesContext context = FacesContext.getCurrentInstance();
ExternalContext externalContext = context.getExternalContext();
String configFile = externalContext.getInitParameter( COMPONENT_FAMILY + ".faces.component.CONFIG_FILE" );
if ( configFile == null ) {
setConfig( DEFAULT_USER_CONFIG );
} else {
setConfig( configFile );
}
FacesContext facesContext = UIMetawidget.this.getFacesContext();
Map applicationMap = facesContext.getExternalContext().getApplicationMap();
ConfigReader configReader = (ConfigReader) applicationMap.get( APPLICATION_ATTRIBUTE_CONFIG_READER );
if ( configReader == null ) {
configReader = new BaseConfigReader( new FacesResourceResolver() );
applicationMap.put( APPLICATION_ATTRIBUTE_CONFIG_READER, configReader );
}
mPipeline.setConfigReader( configReader );
// Default renderer (not set mExplicitRendererType yet)
super.setRendererType( "table" );
mExplicitRendererType = false;
registerBuildWidgetsSupport();
}
//
// Public methods
//
@Override
public String getFamily() {
return COMPONENT_FAMILY;
}
public boolean isReadOnly() {
// Dynamic read-only (takes precedence if set)
ValueBinding bindingReadOnly = getValueBinding( "readOnly" );
if ( bindingReadOnly != null ) {
return (Boolean) bindingReadOnly.getValue( getFacesContext() );
}
// Default to read-write
return mReadOnly;
}
public void setReadOnly( boolean readOnly ) {
mReadOnly = readOnly;
}
public void setConfig( String config ) {
mPipeline.setConfig( config );
}
public void setInspector( Inspector inspector ) {
mPipeline.setInspector( inspector );
}
/**
* Useful for WidgetBuilders to perform nested inspections (eg. for Collections).
*/
public String inspect( Object toInspect, String type, String... names ) {
return mPipeline.inspect( toInspect, type, names );
}
public void addInspectionResultProcessor( InspectionResultProcessor inspectionResultProcessor ) {
mPipeline.addInspectionResultProcessor( inspectionResultProcessor );
}
public void removeInspectionResultProcessor( InspectionResultProcessor inspectionResultProcessor ) {
mPipeline.removeInspectionResultProcessor( inspectionResultProcessor );
}
public void setInspectionResultProcessors( InspectionResultProcessor... inspectionResultProcessors ) {
mPipeline.setInspectionResultProcessors( inspectionResultProcessors );
}
public void setWidgetBuilder( WidgetBuilder widgetBuilder ) {
mPipeline.setWidgetBuilder( widgetBuilder );
}
/**
* Exposed mainly for those using UIComponent.setBinding
.
*/
public WidgetBuilder getWidgetBuilder() {
return mPipeline.getWidgetBuilder();
}
/**
* Exposed mainly for those using UIComponent.setBinding
.
*/
public void addWidgetProcessor( WidgetProcessor widgetProcessor ) {
mPipeline.addWidgetProcessor( widgetProcessor );
}
/**
* Exposed mainly for those using UIComponent.setBinding
.
*/
public void removeWidgetProcessor( WidgetProcessor widgetProcessor ) {
mPipeline.removeWidgetProcessor( widgetProcessor );
}
public void setWidgetProcessors( WidgetProcessor... widgetProcessors ) {
mPipeline.setWidgetProcessors( widgetProcessors );
}
public List> getWidgetProcessors() {
return mPipeline.getWidgetProcessors();
}
public T getWidgetProcessor( Class widgetProcessorClass ) {
return mPipeline.getWidgetProcessor( widgetProcessorClass );
}
public void setLayout( Layout layout ) {
mPipeline.setLayout( layout );
}
public Layout getLayout() {
return mPipeline.getLayout();
}
/**
* Instructs the Metawidget to inspect the value binding from the parent.
*
* If the value binding is of the form #{foo.bar}
, sometimes
* foo.getBar()
has useful metadata (such as UiLookup
). Metawidget
* inspects from parent anyway if #{foo.bar}
evaluates to null
, but on
* occasion it may be necessary to specify parent inspection explicitly.
*
* The disadvantage of inspecting from parent (and the reason it is not enabled by default) is
* that some Inspectors will not know the parent and so not be able to return anything. For
* example, HibernateInspector only knows the Hibernate XML mapping files of persistent classes,
* not the business class of a UI controller, so asking HibernateInspector to inspect
* #{controller.current}
from its parent will always return null
.
*/
public void setInspectFromParent( boolean inspectFromParent ) {
mInspectFromParent = inspectFromParent;
}
/**
* By default, UIMetawidget
does not rebuild widgets upon an AJAX request unless
* the Metawidget's Id
is explicitly included in the list of execute
* Ids. There are several reasons for this:
*
*
* Suppose a Metawidget X has children A, B and C. If B is executed by an AJAX request, this
* will trigger X with a PreRenderViewEvent
(because it is the parent). But if X
* rebuilds A and C, and they weren't part of the execute request, their values will be
* lost. This is similar to how UIMetawidget
doesn't rebuild upon a validation
* error
* Similarly, if the Metawidget's backing bean is request-scoped, rebuilding A and C will
* mean they fetch their values from a new (likely empty) backing bean instance. There will be
* no opportunity for A and C to postback their values first (because they are not executed)
* Some components (such as RichFaces' UIAutocomplete
) do not allow
* fine-grained control over what is executed and rendered. They just execute and render
* themselves
* AJAX is about performance, so typically clients are not wanting to rebuild large sections
* of the component tree
*
*
* Although this default behaviour is safer it does, however, result in less dynamic UIs.
* Clients can use setBuildWidgetsOnAjaxRequest
to override the default behaviour
* and instruct UIMetawidget
to always rebuild widgets upon an AJAX request.
* Mechanisms such as conversation-scoped backing beans can be used to avoid losing values.
*/
public void setBuildWidgetsOnAjaxRequest( boolean buildWidgetsOnAjaxRequest ) {
mBuildWidgetsOnAjaxRequest = buildWidgetsOnAjaxRequest;
}
/**
* Returns a label for the given set of attributes.
*
* The label is determined using the following algorithm:
*
*
* if attributes.get( "label" ) exists...
*
* attributes.get( "label" ) is camel-cased and used as a lookup into
* getLocalizedKey( camelCasedLabel ) . This means developers can initially build their
* UIs without worrying about localization, then turn it on later
* if no such lookup exists, return attributes.get( "label" )
*
*
* if attributes.get( "label" ) does not exist...
*
* attributes.get( "name" ) is used as a lookup into
* getLocalizedKey( name )
* if no such lookup exists, return attributes.get( "name" )
*
*
*
*
* @return the text of the label. This may itself contain a value expression, such as
* UiLabel( "#{foo.name}'s name" )
*/
public String getLabelString( Map attributes ) {
if ( attributes == null ) {
return "";
}
// Explicit label
String label = attributes.get( LABEL );
if ( label != null ) {
// (may be forced blank)
if ( "".equals( label ) ) {
return null;
}
// (localize if possible)
String localized = getLocalizedKey( StringUtils.camelCase( label ) );
if ( localized != null ) {
return localized.trim();
}
return label.trim();
}
// Default name
String name = attributes.get( NAME );
if ( name != null ) {
// (localize if possible)
String localized = getLocalizedKey( name );
if ( localized != null ) {
return localized.trim();
}
return StringUtils.uncamelCase( name );
}
return "";
}
/**
* @return null if no bundle, ???key??? if bundle is missing a key
*/
public String getLocalizedKey( String key ) {
String localizedKey = null;
FacesContext context = FacesContext.getCurrentInstance();
Application application = context.getApplication();
String appBundle = application.getMessageBundle();
// Component-specific bundle
ValueBinding bindingBundle = getValueBinding( "bundle" );
if ( bindingBundle != null ) {
// (watch out when localizing blank labels)
if ( key == null || key.trim().length() == 0 ) {
return "";
}
@SuppressWarnings( "unchecked" )
Map bundleMap = (Map) bindingBundle.getValue( context );
// (check for containsKey first, because BundleMap will return a dummy value otherwise)
if ( bundleMap.containsKey( key ) ) {
localizedKey = bundleMap.get( key );
}
} else if ( appBundle != null ) {
// App-specific bundle
try {
localizedKey = ResourceBundle.getBundle( appBundle, context.getViewRoot().getLocale() ).getString( key );
} catch ( MissingResourceException e ) {
// Fail gracefully: we seem to have problems locating, say,
// org.jboss.seam.core.SeamResourceBundle?
return null;
}
} else {
// No bundle
return null;
}
if ( localizedKey != null ) {
return localizedKey;
}
return StringUtils.RESOURCE_KEY_NOT_FOUND_PREFIX + key + StringUtils.RESOURCE_KEY_NOT_FOUND_SUFFIX;
}
/**
* Sets the parameter with the given name to the given value.
*
* This method will not override existing, manually specified <f:param />
.
* Rather it will store the given parameter in UIComponent.getAttributes
. This has
* several advantages:
*
*
* Creating child UIParameter components for every parameter seems very heavy
* Copying parameters to nested Metawidgets can be done using the getAttributes
* Map, without needing to create nested UIParameter components
* Nested UIParameter components complicate removeRecreatableChildren
because
* although they are not manually specified, they will not be automatically recreated either
*
*
* A disadvantage of this approach is that clients should always use
* UIMetawidget.getParameter
to retrieve parameters, rather than searching for
* UIParameter components directly.
*/
public void setParameter( String name, Object value ) {
getAttributes().put( COMPONENT_ATTRIBUTE_PARAMETER_PREFIX + name, value );
}
/**
* Gets the parameter with the given name.
*
* As discussed in the setParameter
JavaDoc, clients should use this method to
* retrieve parameters, rather than searching for UIParameter components directly.
*/
public String getParameter( String name ) {
// Try UIParameters first...
for ( UIComponent child : getChildren() ) {
if ( !( child instanceof UIParameter ) ) {
continue;
}
// ...with the name we're interested in
UIParameter parameter = (UIParameter) child;
if ( name.equals( parameter.getName() ) ) {
return (String) parameter.getValue();
}
}
// ...then our internal parameters
return (String) getAttributes().get( COMPONENT_ATTRIBUTE_PARAMETER_PREFIX + name );
}
/**
* As discussed in the setParameter
JavaDoc, clients should use this method to
* copy parameters, rather than creating UIParameter components directly.
*/
public void copyParameters( UIMetawidget copyFrom ) {
// Copy each child UIParameter
for ( UIComponent component : copyFrom.getChildren() ) {
if ( !( component instanceof UIParameter ) ) {
continue;
}
UIParameter parameter = (UIParameter) component;
setParameter( parameter.getName(), parameter.getValue() );
}
// Copy each internal parameter too
for ( Map.Entry entry : copyFrom.getAttributes().entrySet() ) {
if ( !entry.getKey().startsWith( COMPONENT_ATTRIBUTE_PARAMETER_PREFIX ) ) {
continue;
}
getAttributes().put( entry.getKey(), entry.getValue() );
}
}
/**
* Storage area for WidgetProcessors, Layouts, and other stateless clients.
*
* Unlike .setAttribute
, these values are not serialized by
* ResponseStateManagerImpl
.
*/
public void putClientProperty( Object key, Object value ) {
if ( mClientProperties == null ) {
mClientProperties = CollectionUtils.newHashMap();
}
mClientProperties.put( key, value );
}
/**
* Storage area for WidgetProcessors, Layouts, and other stateless clients.
*
* Unlike .getAttribute
, these values are not serialized by
* ResponseStateManagerImpl
.
*/
@SuppressWarnings( "unchecked" )
public T getClientProperty( Object key ) {
if ( mClientProperties == null ) {
return null;
}
return (T) mClientProperties.get( key );
}
@Override
public boolean isRendered() {
boolean rendered = super.isRendered();
if ( mBuildWidgetsSupport instanceof EncodeBeginSupport ) {
( (EncodeBeginSupport) mBuildWidgetsSupport ).isRendered( rendered );
}
return rendered;
}
/**
* Overridden to flag whether the rendererType has been set explicitly by the page.
*
* This stops global defaults, if defined in metawidget.xml
, from overriding the
* page-level renderType. This is because metawidget.xml
is not parsed until
* after setRendererType has been called.
*/
@Override
public void setRendererType( String rendererType ) {
mExplicitRendererType = true;
super.setRendererType( rendererType );
}
@Override
public void encodeBegin( FacesContext context )
throws IOException {
if ( mBuildWidgetsSupport instanceof EncodeBeginSupport ) {
( (EncodeBeginSupport) mBuildWidgetsSupport ).encodeBegin();
}
super.encodeBegin( context );
}
/**
* Get the component type used to create this Metawidget.
*
* Usually, clients will want to create a nested-Metawidget using the same subclass as
* themselves. To be 'proper' in JSF, though, we should go via
* application.createComponent
. Unfortunately by default a UIComponent does not
* know its own component type, so subclasses must override this method.
*
* This method is public for use by NestedLayoutDecorators.
*/
public abstract String getComponentType();
/**
* Useful for WidgetBuilders to setup nested Metawidgets (eg. for wrapping them in a
* h:column).
*/
public void initNestedMetawidget( UIMetawidget nestedMetawidget, Map attributes ) {
// Don't reconfigure...
nestedMetawidget.setConfig( null );
// ...instead, copy runtime values
mPipeline.initNestedPipeline( nestedMetawidget.mPipeline, attributes );
// Read-only
//
// Note: initNestedPipeline takes care of literal values. This is concerned with the value
// binding
if ( !WidgetBuilderUtils.isReadOnly( attributes ) ) {
ValueBinding bindingReadOnly = getValueBinding( "readOnly" );
if ( bindingReadOnly != null ) {
nestedMetawidget.setValueBinding( "readOnly", bindingReadOnly );
}
}
// Bundle
nestedMetawidget.setValueBinding( "bundle", getValueBinding( "bundle" ) );
// Renderer type
nestedMetawidget.setRendererType( getRendererType() );
// Parameters
nestedMetawidget.copyParameters( this );
// Note: it is very dangerous to do, say...
//
// to.getAttributes().putAll( from.getAttributes() );
//
// ...in order to copy all arbitary attributes, because some frameworks (eg. Facelets) use
// the attributes map as a storage area for special flags (eg.
// ComponentSupport.MARK_CREATED) that should not get copied down from component to
// component!
}
@Override
public Object saveState( FacesContext context ) {
Object[] values = new Object[6];
values[0] = super.saveState( context );
values[1] = mExplicitRendererType;
values[2] = mReadOnly;
values[3] = mPipeline.getConfig();
values[4] = mInspectFromParent;
values[5] = mBuildWidgetsOnAjaxRequest;
return values;
}
@Override
public void restoreState( FacesContext context, Object state ) {
Object[] values = (Object[]) state;
super.restoreState( context, values[0] );
mExplicitRendererType = (Boolean) values[1];
mReadOnly = (Boolean) values[2];
mPipeline.setConfig( values[3] );
mInspectFromParent = (Boolean) values[4];
mBuildWidgetsOnAjaxRequest = (Boolean) values[5];
}
//
// Protected methods
//
/**
* Instantiate the Pipeline used by this Metawidget.
*
* Subclasses wishing to use their own Pipeline should override this method to instantiate their
* version.
*/
protected Pipeline newPipeline() {
return new Pipeline();
}
/**
* Register the mechanism used to build widgets.
*
* Subclasses wishing to use their own mechanism should override this method to instantiate
* their version.
*/
protected void registerBuildWidgetsSupport() {
FacesContext context = FacesContext.getCurrentInstance();
ExternalContext externalContext = context.getExternalContext();
// PreRenderViewEvent support
//
// This is dependent on http://java.net/jira/browse/JAVASERVERFACES-1826
// and https://issues.apache.org/jira/browse/MYFACES-2935. It is decided once, statically,
// for the duration
if ( USE_PRERENDER_VIEW_EVENT == null ) {
// Use context.class. Using context.application.class returns 'JBoss Application Server
// Weld Integration EE Webtier services' under JBoss AS 6
Package contextPackage = context.getClass().getPackage();
String contextImplementationTitle = contextPackage.getImplementationTitle();
String contextImplementationVersion = contextPackage.getImplementationVersion();
if ( TRUE.equals( externalContext.getInitParameter( COMPONENT_FAMILY + ".faces.component.DONT_USE_PRERENDER_VIEW_EVENT" ) ) ) {
if ( isBadMojarra2( contextImplementationTitle, contextImplementationVersion ) && !FacesUtils.isPartialStateSavingDisabled() ) {
throw MetawidgetException.newException( contextImplementationTitle + " " + contextImplementationVersion + " requires setting 'javax.faces.PARTIAL_STATE_SAVING' to 'false'. Or upgrade Mojarra to a version that includes this fix: http://java.net/jira/browse/JAVASERVERFACES-1826" );
} else if ( isBadMyFaces2( contextImplementationTitle, contextImplementationVersion ) && !FacesUtils.isPartialStateSavingDisabled() ) {
throw MetawidgetException.newException( contextImplementationTitle + " " + contextImplementationVersion + " requires setting 'javax.faces.PARTIAL_STATE_SAVING' to 'false'. Or upgrade MyFaces to a version that includes this fix: https://issues.apache.org/jira/browse/MYFACES-2935" );
}
// Forcibly disabled
USE_PRERENDER_VIEW_EVENT = Boolean.FALSE;
} else if ( FacesUtils.isJsf2() ) {
if ( isBadMojarra2( contextImplementationTitle, contextImplementationVersion ) ) {
throw MetawidgetException.newException( contextImplementationTitle + " " + contextImplementationVersion + " requires setting 'org.metawidget.faces.component.DONT_USE_PRERENDER_VIEW_EVENT' to 'true'. Or upgrade Mojarra to a version that includes this fix: http://java.net/jira/browse/JAVASERVERFACES-1826" );
} else if ( isBadMyFaces2( contextImplementationTitle, contextImplementationVersion ) ) {
throw MetawidgetException.newException( contextImplementationTitle + " " + contextImplementationVersion + " requires setting 'org.metawidget.faces.component.DONT_USE_PRERENDER_VIEW_EVENT' to 'true'. Or upgrade MyFaces to a version that includes this fix: https://issues.apache.org/jira/browse/MYFACES-2935" );
}
if ( context.getViewRoot() == null ) {
// No ViewRoot? Maybe PARTIAL_STATE_SAVING disabled under MyFaces?
USE_PRERENDER_VIEW_EVENT = Boolean.FALSE;
} else {
// Supported
USE_PRERENDER_VIEW_EVENT = Boolean.TRUE;
}
} else {
// JSF 1.x
USE_PRERENDER_VIEW_EVENT = Boolean.FALSE;
}
}
if ( Boolean.TRUE.equals( USE_PRERENDER_VIEW_EVENT ) ) {
mBuildWidgetsSupport = new PreRenderViewEventSupport( this );
} else {
mBuildWidgetsSupport = new EncodeBeginSupport( this );
}
}
/**
* Build widgets for the given value binding.
*
* Subclasses can override this method as a common entry point regardless of whether
* PreRenderViewEventSupport (JSF2) or EncodeBeginSupport (JSF1) is being used.
*/
protected void buildWidgets()
throws Exception {
// Inspect from the value binding...
ValueBinding valueBinding = getValueBinding( "value" );
if ( valueBinding != null ) {
mPipeline.buildWidgets( inspect( valueBinding, mInspectFromParent ) );
return;
}
// ...or from a raw value (for jBPM)...
Object value = getValue();
if ( value instanceof String ) {
mPipeline.buildWidgets( mPipeline.inspectAsDom( null, (String) value ) );
return;
}
// ...or a Class (for 'binding' attribute)...
if ( value instanceof Class> ) {
mPipeline.buildWidgets( mPipeline.inspectAsDom( null, ( (Class>) value ).getName() ) );
return;
}
// ...or a direct Object (for 'binding' attribute)...
if ( value != null ) {
mPipeline.buildWidgets( mPipeline.inspectAsDom( value, value.getClass().getName() ) );
return;
}
// ...or run without inspection (using the Metawidget purely for layout)
mPipeline.buildWidgets( null );
}
protected abstract String getDefaultConfiguration();
/**
* Build child widgets.
*/
protected void startBuild() {
LOG.trace( "startBuild" );
// Metawidget has no valueBinding? Won't be destroying/recreating any components, then.
//
// This is an optimisation, but is also important for cases like RichFacesLayout, which
// use nested Metawidgets (without a value binding) purely for layout. They populate a new
// Metawidget with previously inspected components, and we don't want them destroyed
// here and/or unnecessarily re-inspected in endBuild
//
// Check getValue() is null too, in case the Metawidget is being used with direct objects
// or direct classes (through the 'binding' attribute)
if ( getValueBinding( "value" ) == null && getValue() == null ) {
return;
}
// Remove any components we created previously (this is
// important for polymorphic controls, which may change from
// refresh to refresh)
List children = getChildren();
for ( Iterator i = children.iterator(); i.hasNext(); ) {
UIComponent componentChild = i.next();
Map attributes = componentChild.getAttributes();
// The first time in, children will have no metadata attached. Use this opportunity
// to tag the initial children so that we never recreate them
if ( !attributes.containsKey( COMPONENT_ATTRIBUTE_METADATA ) ) {
attributes.put( COMPONENT_ATTRIBUTE_NOT_RECREATABLE, true );
continue;
}
// Remove recreatable components
if ( removeRecreatableChildren( componentChild ) ) {
i.remove();
}
}
}
/**
* @param elementName
* XML node name of the business field. Typically 'entity', 'property' or 'action'.
* Never null
*/
protected void layoutWidget( UIComponent component, String elementName, Map attributes ) {
Map componentAttributes = component.getAttributes();
componentAttributes.put( COMPONENT_ATTRIBUTE_METADATA, attributes );
// If this component already exists in the list, remove it and re-add it. This
// enables us to sort existing, manually created components in the correct order
//
// Doing the remove here, rather than in SimpleLayout, ensures we always remove and
// add for cases like moving a Stub from outside a TabPanel to inside it
getChildren().remove( component );
// Look up any additional attributes
Map additionalAttributes = mPipeline.getAdditionalAttributes( component );
if ( additionalAttributes != null ) {
attributes.putAll( additionalAttributes );
}
// BasePipeline will call .layoutWidget
}
protected void endBuild() {
List children = getChildren();
// Inspect any remaining components, and sort them to the bottom
for ( int loop = 0, index = 0, length = children.size(); loop < length; loop++ ) {
UIComponent component = children.get( index );
// If this component has already been processed by the inspection (ie. contains
// metadata), is not rendered, or is a UIParameter, skip it
//
// This is also handy for RichFacesLayout, which uses a nested Metawidget purely as a
// layout tool: it populates a new Metawidget with some previously inspected components.
// This check makes sure they aren't unnecessarily re-inspected here
Map miscAttributes = component.getAttributes();
if ( miscAttributes.containsKey( COMPONENT_ATTRIBUTE_METADATA ) || !component.isRendered() || component instanceof UIParameter ) {
index++;
continue;
}
// Try and determine some metadata for the component by inspecting its binding. This
// helps our layout display proper labels, required stars etc. - even for components
// whose binding is not a descendant of our parent binding
Map childAttributes = CollectionUtils.newHashMap();
miscAttributes.put( COMPONENT_ATTRIBUTE_METADATA, childAttributes );
ValueBinding binding = component.getValueBinding( "value" );
if ( binding != null ) {
Element inspectionResult = inspect( binding, true );
if ( inspectionResult != null ) {
childAttributes.putAll( XmlUtils.getAttributesAsMap( inspectionResult.getFirstChild() ) );
}
} else {
// If no found metadata, default to no section.
//
// This is so if a user puts, say, a in the component tree, it doesn't
// appear inside an existing section
childAttributes.put( SECTION, "" );
}
mPipeline.layoutWidget( component, PROPERTY, childAttributes );
}
LOG.trace( "endBuild" );
}
//
// Private methods
//
/**
* Removes all recreatable children (i.e. not marked COMPONENT_ATTRIBUTE_NOT_RECREATABLE). Does
* not remove top-level UIComponent
s if any of their
* children are COMPONENT_ATTRIBUTE_NOT_RECREATABLE, but does remove as many of their
* children as it can. This allows their siblings to still behave dynamically even if some
* components are locked (e.g. SelectInputDate
).
*
* @return true if all children were removed (i.e. none were marked not-recreatable).
*/
private boolean removeRecreatableChildren( UIComponent component ) {
// Do not remove locked or overridden components...
Map attributes = component.getAttributes();
if ( attributes.containsKey( COMPONENT_ATTRIBUTE_NOT_RECREATABLE ) ) {
// ...but always remove their metadata, otherwise
// they will not be removed/re-added (and therefore re-ordered) upon POSTback
attributes.remove( COMPONENT_ATTRIBUTE_METADATA );
return false;
}
// Recurse into children. We may have an auto-generated 'not recreatable' (e.g.
// SelectInputDate) or a manually added 'not recreatable', and we don't want to remove the
// top-level for it. This includes children that are nested Metawidgets, and children that
// are LayoutDecorators
List children = component.getChildren();
for ( Iterator i = children.iterator(); i.hasNext(); ) {
UIComponent componentChild = i.next();
if ( removeRecreatableChildren( componentChild ) ) {
i.remove();
}
}
return children.isEmpty();
}
/**
* Inspect the value binding.
*
* A value binding is optional. UIMetawidget can be used purely to lay out manually-specified
* components
*/
private Element inspect( ValueBinding valueBinding, boolean inspectFromParent ) {
if ( valueBinding == null ) {
return null;
}
// Inspect the object directly
FacesContext context = getFacesContext();
String valueBindingString = valueBinding.getExpressionString();
if ( !inspectFromParent || !FacesUtils.isExpression( valueBindingString ) ) {
Object toInspect = valueBinding.getValue( context );
if ( toInspect != null && !ClassUtils.isPrimitiveWrapper( toInspect.getClass() ) ) {
return mPipeline.inspectAsDom( toInspect, toInspect.getClass().getName() );
}
}
// In the event the direct object is 'null' or a primitive...
String binding = FacesUtils.unwrapExpression( valueBindingString );
// ...and the EL expression is such that we can chop it off to get to the parent...
//
// Note: using EL functions in generated ValueExpressions only works in JSF 2.0,
// see https://javaserverfaces.dev.java.net/issues/show_bug.cgi?id=813. A workaround is
// to assign the function's return value to a temporary, request-scoped variable using c:set
if ( binding.indexOf( ' ' ) == -1 && binding.indexOf( StringUtils.SEPARATOR_COLON_CHAR ) == -1 && binding.indexOf( '(' ) == -1 ) {
int lastIndexOf = binding.lastIndexOf( StringUtils.SEPARATOR_DOT_CHAR );
if ( lastIndexOf != -1 ) {
// ...traverse from the parent as there may be useful metadata there (such as 'name'
// and 'type')
Application application = context.getApplication();
ValueBinding bindingParent = application.createValueBinding( FacesUtils.wrapExpression( binding.substring( 0, lastIndexOf ) ) );
Object toInspect = bindingParent.getValue( context );
if ( toInspect != null ) {
return mPipeline.inspectAsDom( toInspect, toInspect.getClass().getName(), binding.substring( lastIndexOf + 1 ) );
}
}
}
LOG.debug( "No inspectors matched {0} (evaluated to null)", valueBindingString );
return null;
}
/**
* Mojarra 2.x requires a fix for http://java.net/jira/browse/JAVASERVERFACES-1826.
*/
private boolean isBadMojarra2( String contextImplementationTitle, String contextImplementationVersion ) {
if ( contextImplementationTitle == null ) {
return false;
}
if ( !contextImplementationTitle.contains( "Mojarra" ) ) {
return false;
}
if ( contextImplementationVersion.endsWith( "2.1.0" ) || contextImplementationVersion.endsWith( "2.1.1" ) || contextImplementationVersion.endsWith( "2.1.2" ) ) {
return true;
}
if ( contextImplementationVersion.endsWith( "2.1.3" ) || contextImplementationVersion.endsWith( "2.1.4" ) || contextImplementationVersion.endsWith( "2.1.5" ) ) {
return true;
}
if ( contextImplementationVersion.endsWith( "2.1.6" ) ) {
return true;
}
return contextImplementationVersion.contains( "2.0." );
}
/**
* MyFaces 2.x requires a fix for https://issues.apache.org/jira/browse/MYFACES-2935 (and
* ideally https://issues.apache.org/jira/browse/MYFACES-3010 too).
*/
private boolean isBadMyFaces2( String contextImplementationTitle, String contextImplementationVersion ) {
if ( contextImplementationTitle == null ) {
return false;
}
if ( !contextImplementationTitle.contains( "MyFaces" ) ) {
return false;
}
return contextImplementationVersion.endsWith( "2.0.0" ) || contextImplementationVersion.endsWith( "2.0.1" ) || contextImplementationVersion.endsWith( "2.0.2" );
}
//
// Inner class
//
protected class Pipeline
extends W3CPipeline {
//
// Public methods
//
@Override
protected void configure() {
boolean wasExplicitRendererType = mExplicitRendererType;
String rendererType = getRendererType();
try {
super.configure();
} catch ( MetawidgetException e ) {
if ( !DEFAULT_USER_CONFIG.equals( getConfig() ) || !( e.getCause() instanceof FileNotFoundException ) ) {
throw e;
}
// Log a warning. Still log the Exception message, in case the FileNotFoundException
// is from inside metawidget.xml, for example 'Unable to locate checkout.jpdl.xml on
// CLASSPATH'
if ( !LOGGED_MISSING_CONFIG ) {
LOGGED_MISSING_CONFIG = true;
LOG.info( "Could not locate " + DEFAULT_USER_CONFIG + ". This file is optional, but if you HAVE created one then Metawidget isn''t finding it: {0}", e.getMessage() );
}
super.configureDefaults();
}
// Preserve rendererType if was set explicitly
if ( wasExplicitRendererType ) {
setRendererType( rendererType );
}
}
@Override
protected String getDefaultConfiguration() {
return UIMetawidget.this.getDefaultConfiguration();
}
/**
* Overridden to just-in-time evaluate EL binding.
*/
@Override
public boolean isReadOnly() {
return UIMetawidget.this.isReadOnly();
}
@Override
public void setReadOnly( boolean readOnly ) {
UIMetawidget.this.setReadOnly( readOnly );
}
//
// Protected methods
//
@Override
protected void startBuild() {
super.startBuild();
UIMetawidget.this.startBuild();
}
@Override
protected UIComponent buildWidget( String elementName, Map attributes ) {
UIComponent entityLevelWidget = super.buildWidget( elementName, attributes );
// If we manage to build an entity-level widget, move our children *inside* it
//
// It's pretty rare we'll manage to create an entity-level widget, and even rarer that
// we'll create one when we ourselves have children, but if we do this allows us to
// attach, say, f:validator or f:ajax handlers to a dynamically chosen widget
if ( entityLevelWidget != null && ENTITY.equals( elementName ) ) {
// Move converters
if ( entityLevelWidget instanceof ValueHolder ) {
ValueHolder valueHolder = (ValueHolder) entityLevelWidget;
valueHolder.setConverter( UIMetawidget.this.getConverter() );
// Do not UIMetawidget.this.setConverter( null ), else it will get lost for
// subsequent POSTbacks
}
// Move facets
Map metawidgetFacets = UIMetawidget.this.getFacets();
Map entityLevelWidgetFacets = entityLevelWidget.getFacets();
for ( Map.Entry entry : metawidgetFacets.entrySet() ) {
UIComponent facet = entry.getValue();
entityLevelWidgetFacets.put( entry.getKey(), facet );
if ( mBuildWidgetsSupport instanceof EncodeBeginSupport ) {
( (EncodeBeginSupport) mBuildWidgetsSupport ).reassignFacet( facet );
}
}
metawidgetFacets.clear();
// Move validators
if ( entityLevelWidget instanceof EditableValueHolder ) {
EditableValueHolder entityLevelEditableValueHolder = (EditableValueHolder) entityLevelWidget;
for ( Validator metawidgetValidator : UIMetawidget.this.getValidators() ) {
entityLevelEditableValueHolder.addValidator( metawidgetValidator );
// Do not UIMetawidget.this.removeValidator( metawidgetValidator ), else
// they will get lost for subsequent POSTbacks
}
}
// It's not clear whether we should move .getChildren() too. Err on the side of
// caution and don't for now
}
return entityLevelWidget;
}
@Override
protected Map getAdditionalAttributes( UIComponent widget ) {
if ( widget instanceof UIStub ) {
return ( (UIStub) widget ).getStubAttributesAsMap();
}
return null;
}
@Override
protected UIMetawidget buildNestedMetawidget( Map attributes )
throws Exception {
FacesContext context = FacesContext.getCurrentInstance();
UIMetawidget metawidget = (UIMetawidget) context.getApplication().createComponent( UIMetawidget.this.getComponentType() );
UIMetawidget.this.initNestedMetawidget( metawidget, attributes );
return metawidget;
}
@Override
protected void layoutWidget( UIComponent component, String elementName, Map attributes ) {
UIMetawidget.this.layoutWidget( component, elementName, attributes );
super.layoutWidget( component, elementName, attributes );
}
@Override
protected void endBuild() {
super.endBuild();
UIMetawidget.this.endBuild();
}
@Override
protected UIMetawidget getPipelineOwner() {
return UIMetawidget.this;
}
}
/**
* Dynamically modify the component tree using the JSF1 API.
*
*
Background
*
* JSF1 did not have very good support for dynamically modifying the component tree. See
* http://osdir.com/ml/java.facelets.user/2008-06/msg00050.html:
*
* Jacob Hookum: "What's actually needed in [JSF 1.2] is post component tree creation or post
* component creation hooks, providing the ability to then modify the component tree"
* Ken Paulsen: "This hasn't been resolved in the 2.0 EG yet"
*
* We tried various workarounds:
*
*
* Triggering buildWidgets on getChildCount/getChildren. This does not work because those
* methods get called at all sorts of other times
* Doing it in super.encodeBegin for a GET, in processUpdates for a POST. This does not work
* because the GET still records the bad components
* A PhaseListener before PhaseId.RENDER_RESPONSE to trigger buildWidgets. This does not
* work because UIViewRoot has no children at that stage in the lifecycle
*
* JSF2 introduced SystemEvents
to address this exact problem. See
* PreRenderViewEventSupport
below.
*
*
Why It's A Problem
*
* JSF actually has (sort of) two component trees: the one in the ViewState, and the
* components in the original JSP page. The latter is re-merged with the former, then the whole
* lot is serialized. This happens after processUpdates
but before
* encodeBegin
, which is a bit of a 'dead zone' for hooking into under JSF1.
*
* It can cause an Exception if the original JSP contains a manually coded control (such as an
* h:inputHidden) that subsequently gets moved into a Metawidget-generated sub-container (such
* as a rich:simpleTogglePanel). Now there are two versions of the component: one in the
* original JSP and one in a different place in the ViewState.
*
* This hack removes that duplicate.
*/
private static class EncodeBeginSupport
extends BuildWidgetsSupport {
//
// Constructor
//
public EncodeBeginSupport( UIMetawidget metawidget ) {
super( metawidget );
}
//
// Public methods
//
/**
* If the component is never going to be rendered, then encodeBegin
will never
* get called. Therefore our 'remove duplicates' code will never get called either.
*/
public void isRendered( boolean rendered ) {
// Note: we explored not doing this until after the processUpdates phase, in case
// the value of 'rendered' changes, but it didn't seem to make any difference to our
// unit tests?
if ( !rendered ) {
getMetawidget().getChildren().clear();
}
}
/**
* Modify the component tree during encodeBegin
. This is not the cleanest, but
* is the best we can do under JSF 1.x
*/
public void encodeBegin()
throws IOException {
try {
// Remove duplicate children
//
// Remove the top-level version of each duplicate, not the nested-level version,
// because the top-level is the 'original' whereas the nested-level is the
// 'moved' (i.e. at its final destination).
for ( Iterator i = getMetawidget().getChildren().iterator(); i.hasNext(); ) {
UIComponent component = i.next();
if ( findDuplicateChild( getMetawidget(), component ) != null ) {
i.remove();
}
}
buildWidgets();
} catch ( Exception e ) {
// IOException does not take a Throwable 'cause' argument until Java 6, so
// as we need to stay 1.5 compatible we output the trace here
LogUtils.getLog( getClass() ).error( "Unable to encodeBegin", e );
// At this level, it is more 'proper' to throw an IOException than
// a MetawidgetException, as that is what the layers above are expecting
throw new IOException( e.getMessage() );
}
}
/**
* Under JSF 1.x, facets will get duplicated just like children do. We can clean them up on
* encodeBegin
, but in the case of an AJAX POST-back we are not given that
* chance. Instead, give them a new unique id.
*/
public void reassignFacet( UIComponent facet ) {
facet.setId( FacesUtils.createUniqueId() );
}
//
// Private methods
//
private UIComponent findDuplicateChild( UIComponent componentWithChildren, UIComponent originalComponent ) {
String id = originalComponent.getId();
if ( id == null ) {
return null;
}
for ( UIComponent child : componentWithChildren.getChildren() ) {
if ( child == originalComponent ) {
continue;
}
if ( id.equals( child.getId() ) ) {
return child;
}
UIComponent found = findDuplicateChild( child, originalComponent );
if ( found != null ) {
return found;
}
}
return null;
}
}
/**
* Dynamically modify the component tree using the JSF2 API.
*
* JSF2 introduced PreRenderViewEvent
, which we can use to avoid
* RemoveDuplicatesSupport
.
*/
private static class PreRenderViewEventSupport
extends BuildWidgetsSupport
implements SystemEventListener {
//
// Constructor
//
public PreRenderViewEventSupport( UIMetawidget metawidget ) {
super( metawidget );
FacesContext context = FacesContext.getCurrentInstance();
UIViewRoot root = context.getViewRoot();
if ( root == null ) {
throw MetawidgetException.newException( "UIViewRoot is null. Is faces-config.xml set to version 2.0?" );
}
root.subscribeToViewEvent( PreRenderViewEvent.class, this );
}
//
// Public methods
//
public boolean isListenerForSource( Object source ) {
return source instanceof UIViewRoot;
}
public void processEvent( SystemEvent event ) {
// Don't do unnecessary work if none of our child components are to be rendered anyway
// (work around for https://issues.apache.org/jira/browse/MYFACES-3293)
if ( !getMetawidget().isRendered() ) {
return;
}
// Don't run if we are not actually part of the View (HtmlWidgetBuilder uses us as a
// dummy Metawidget)
if ( getMetawidget().getParent() == null ) {
return;
}
// PartialViewContext (JSF 2-specific)
if ( !getMetawidget().mBuildWidgetsOnAjaxRequest ) {
PartialViewContext partialViewContext = FacesContext.getCurrentInstance().getPartialViewContext();
if ( partialViewContext.isAjaxRequest() ) {
Collection executeIds = partialViewContext.getExecuteIds();
if ( !executeIds.contains( getMetawidget().getClientId() ) ) {
return;
}
}
}
try {
buildWidgets();
} catch ( Exception e ) {
// At this level, it is more 'proper' to throw an AbortProcessingException than
// a MetawidgetException, as that is what the layers above are expecting
throw new AbortProcessingException( e );
}
}
}
/**
* Base implementation shared by EncodeBeginSupport
and
* PreRenderViewEventSupport
.
*/
private static class BuildWidgetsSupport {
//
// Private members
//
private UIMetawidget mMetawidget;
//
// Constructor
//
public BuildWidgetsSupport( UIMetawidget metawidget ) {
mMetawidget = metawidget;
}
//
// Protected methods
//
protected UIMetawidget getMetawidget() {
return mMetawidget;
}
protected void buildWidgets()
throws Exception {
// Validation error? Do not rebuild, as we will lose the invalid values in the
// components
if ( FacesUtils.isValidationFailed() ) {
return;
}
// Build the widgets
mMetawidget.buildWidgets();
}
}
}