blob: 599943d4897f9808009b62152ed3fe6b4991cf82 [file] [log] [blame]
package org.apache.commons.digester3.plugins;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.util.List;
import org.apache.commons.digester3.Rule;
import org.apache.commons.logging.Log;
import org.xml.sax.Attributes;
/**
* Allows the original rules for parsing the configuration file to define points at which plugins are allowed, by
* configuring a PluginCreateRule with the appropriate pattern.
*
* @since 1.6
*/
public class PluginCreateRule
extends Rule
implements InitializableRule
{
// see setPluginClassAttribute
private String pluginClassAttrNs = null;
private String pluginClassAttr = null;
// see setPluginIdAttribute
private String pluginIdAttrNs = null;
private String pluginIdAttr = null;
/**
* In order to invoke the addRules method on the plugin class correctly, we need to know the pattern which this rule
* is matched by.
*/
private String pattern;
/** A base class that any plugin must derive from. */
private Class<?> baseClass = null;
/**
* Info about optional default plugin to be used if no plugin-id is specified in the input data. This can simplify
* the syntax where one particular plugin is usually used.
*/
private Declaration defaultPlugin;
/**
* Currently, none of the Rules methods allow exceptions to be thrown. Therefore if this class cannot initialize
* itself properly, it cannot cause the digester to stop. Instead, we cache the exception and throw it the first
* time the begin() method is called.
*/
private PluginConfigurationException initException;
// -------------------- constructors -------------------------------------
/**
* Create a plugin rule where the user <i>must</i> specify a plugin-class or plugin-id.
*
* @param baseClass is the class which any specified plugin <i>must</i> be descended from.
*/
public PluginCreateRule( final Class<?> baseClass )
{
this.baseClass = baseClass;
}
/**
* Create a plugin rule where the user <i>may</i> specify a plugin. If the user doesn't specify a plugin, then the
* default class specified in this constructor is used.
*
* @param baseClass is the class which any specified plugin <i>must</i> be descended from.
* @param dfltPluginClass is the class which will be used if the user doesn't specify any plugin-class or plugin-id.
* This class will have custom rules installed for it just like a declared plugin.
*/
public PluginCreateRule( final Class<?> baseClass, final Class<?> dfltPluginClass )
{
this.baseClass = baseClass;
if ( dfltPluginClass != null )
{
defaultPlugin = new Declaration( dfltPluginClass );
}
}
/**
* Create a plugin rule where the user <i>may</i> specify a plugin. If the user doesn't specify a plugin, then the
* default class specified in this constructor is used.
*
* @param baseClass is the class which any specified plugin <i>must</i> be descended from.
* @param dfltPluginClass is the class which will be used if the user doesn't specify any plugin-class or plugin-id.
* This class will have custom rules installed for it just like a declared plugin.
* @param dfltPluginRuleLoader is a RuleLoader instance which knows how to load the custom rules associated with
* this default plugin.
*/
public PluginCreateRule( final Class<?> baseClass, final Class<?> dfltPluginClass, final RuleLoader dfltPluginRuleLoader )
{
this.baseClass = baseClass;
if ( dfltPluginClass != null )
{
defaultPlugin = new Declaration( dfltPluginClass, dfltPluginRuleLoader );
}
}
// ------------------- properties ---------------------------------------
/**
* Sets the xml attribute which the input xml uses to indicate to a PluginCreateRule which class should be
* instantiated.
* <p>
* See {@link PluginRules#setPluginClassAttribute} for more info.
*
* @param namespaceUri is the namespace uri that the specified attribute is in. If the attribute is in no namespace,
* then this should be null. Note that if a namespace is used, the attrName value should <i>not</i>
* contain any kind of namespace-prefix. Note also that if you are using a non-namespace-aware parser,
* this parameter <i>must</i> be null.
* @param attrName is the attribute whose value contains the name of the class to be instantiated.
*/
public void setPluginClassAttribute( final String namespaceUri, final String attrName )
{
pluginClassAttrNs = namespaceUri;
pluginClassAttr = attrName;
}
/**
* Sets the xml attribute which the input xml uses to indicate to a PluginCreateRule which plugin declaration is
* being referenced.
* <p>
* See {@link PluginRules#setPluginIdAttribute} for more info.
*
* @param namespaceUri is the namespace uri that the specified attribute is in. If the attribute is in no namespace,
* then this should be null. Note that if a namespace is used, the attrName value should <i>not</i>
* contain any kind of namespace-prefix. Note also that if you are using a non-namespace-aware parser,
* this parameter <i>must</i> be null.
* @param attrName is the attribute whose value contains the id of the plugin declaration to be used when
* instantiating an object.
*/
public void setPluginIdAttribute( final String namespaceUri, final String attrName )
{
pluginIdAttrNs = namespaceUri;
pluginIdAttr = attrName;
}
// ------------------- methods --------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public void postRegisterInit( final String matchPattern )
{
final Log log = LogUtils.getLogger( getDigester() );
final boolean debug = log.isDebugEnabled();
if ( debug )
{
log.debug( "PluginCreateRule.postRegisterInit" + ": rule registered for pattern [" + matchPattern + "]" );
}
if ( getDigester() == null )
{
// We require setDigester to be called before this method.
// Note that this means that PluginCreateRule cannot be added
// to a Rules object which has not yet been added to a
// Digester object.
initException =
new PluginConfigurationException( "Invalid invocation of postRegisterInit" + ": digester not set." );
throw initException;
}
if ( pattern != null )
{
// We have been called twice, ie a single instance has been
// associated with multiple patterns.
//
// Generally, Digester Rule instances can be associated with
// multiple patterns. However for plugins, this creates some
// complications. Some day this may be supported; however for
// now we just reject this situation.
initException =
new PluginConfigurationException( "A single PluginCreateRule instance has been mapped to"
+ " multiple patterns; this is not supported." );
throw initException;
}
if ( matchPattern.indexOf( '*' ) != -1 )
{
// having wildcards in patterns is extremely difficult to
// deal with. For now, we refuse to allow this.
//
// TODO: check for any chars not valid in xml element name
// rather than just *.
//
// Reasons include:
// (a) handling recursive plugins, and
// (b) determining whether one pattern is "below" another,
// as done by PluginRules. Without wildcards, "below"
// just means startsWith, which is easy to check.
initException =
new PluginConfigurationException( "A PluginCreateRule instance has been mapped to" + " pattern ["
+ matchPattern + "]." + " This pattern includes a wildcard character."
+ " This is not supported by the plugin architecture." );
throw initException;
}
if ( baseClass == null )
{
baseClass = Object.class;
}
final PluginRules rules = (PluginRules) getDigester().getRules();
final PluginManager pm = rules.getPluginManager();
// check default class is valid
if ( defaultPlugin != null )
{
if ( !baseClass.isAssignableFrom( defaultPlugin.getPluginClass() ) )
{
initException =
new PluginConfigurationException( "Default class [" + defaultPlugin.getPluginClass().getName()
+ "] does not inherit from [" + baseClass.getName() + "]." );
throw initException;
}
try
{
defaultPlugin.init( getDigester(), pm );
}
catch ( final PluginException pwe )
{
throw new PluginConfigurationException( pwe.getMessage(), pwe.getCause() );
}
}
// remember the pattern for later
pattern = matchPattern;
if ( pluginClassAttr == null )
{
// the user hasn't set explicit xml attr names on this rule,
// so fetch the default values
pluginClassAttrNs = rules.getPluginClassAttrNs();
pluginClassAttr = rules.getPluginClassAttr();
if ( debug )
{
log.debug( "init: pluginClassAttr set to per-digester values [" + "ns=" + pluginClassAttrNs + ", name="
+ pluginClassAttr + "]" );
}
}
else
{
if ( debug )
{
log.debug( "init: pluginClassAttr set to rule-specific values [" + "ns=" + pluginClassAttrNs
+ ", name=" + pluginClassAttr + "]" );
}
}
if ( pluginIdAttr == null )
{
// the user hasn't set explicit xml attr names on this rule,
// so fetch the default values
pluginIdAttrNs = rules.getPluginIdAttrNs();
pluginIdAttr = rules.getPluginIdAttr();
if ( debug )
{
log.debug( "init: pluginIdAttr set to per-digester values [" + "ns=" + pluginIdAttrNs + ", name="
+ pluginIdAttr + "]" );
}
}
else
{
if ( debug )
{
log.debug( "init: pluginIdAttr set to rule-specific values [" + "ns=" + pluginIdAttrNs + ", name="
+ pluginIdAttr + "]" );
}
}
}
/**
* Invoked when the Digester matches this rule against an xml element.
* <p>
* A new instance of the target class is created, and pushed onto the stack. A new "private" PluginRules object is
* then created and set as the digester's default Rules object. Any custom rules associated with the plugin class
* are then loaded into that new Rules object. Finally, any custom rules that are associated with the current
* pattern (such as SetPropertiesRules) have their begin methods executed.
*
* @param namespace the namespace URI of the matching element, or an empty string if the parser is not namespace
* aware or the element has no namespace
* @param name the local name if the parser is namespace aware, or just the element name otherwise
* @param attributes The attribute list of this element
* @throws Exception if any error occurs
*/
@Override
public void begin( final String namespace, final String name, final org.xml.sax.Attributes attributes )
throws Exception
{
final Log log = getDigester().getLogger();
final boolean debug = log.isDebugEnabled();
if ( debug )
{
log.debug( "PluginCreateRule.begin" + ": pattern=[" + pattern + "]" + " match=[" + getDigester().getMatch()
+ "]" );
}
if ( initException != null )
{
// we had a problem during initialisation that we could
// not report then; report it now.
throw initException;
}
// load any custom rules associated with the plugin
final PluginRules oldRules = (PluginRules) getDigester().getRules();
final PluginManager pluginManager = oldRules.getPluginManager();
Declaration currDeclaration = null;
String pluginClassName;
if ( pluginClassAttrNs == null )
{
// Yep, this is ugly.
//
// In a namespace-aware parser, the one-param version will
// return attributes with no namespace.
//
// In a non-namespace-aware parser, the two-param version will
// never return any attributes, ever.
pluginClassName = attributes.getValue( pluginClassAttr );
}
else
{
pluginClassName = attributes.getValue( pluginClassAttrNs, pluginClassAttr );
}
String pluginId;
if ( pluginIdAttrNs == null )
{
pluginId = attributes.getValue( pluginIdAttr );
}
else
{
pluginId = attributes.getValue( pluginIdAttrNs, pluginIdAttr );
}
if ( pluginClassName != null )
{
// The user is using a plugin "inline", ie without a previous
// explicit declaration. If they have used the same plugin class
// before, we have already gone to the effort of creating a
// Declaration object, so retrieve it. If there is no existing
// declaration object for this class, then create one.
currDeclaration = pluginManager.getDeclarationByClass( pluginClassName );
if ( currDeclaration == null )
{
currDeclaration = new Declaration( pluginClassName );
try
{
currDeclaration.init( getDigester(), pluginManager );
}
catch ( final PluginException pwe )
{
throw new PluginInvalidInputException( pwe.getMessage(), pwe.getCause() );
}
pluginManager.addDeclaration( currDeclaration );
}
}
else if ( pluginId != null )
{
currDeclaration = pluginManager.getDeclarationById( pluginId );
if ( currDeclaration == null )
{
throw new PluginInvalidInputException( "Plugin id [" + pluginId + "] is not defined." );
}
}
else if ( defaultPlugin != null )
{
currDeclaration = defaultPlugin;
}
else
{
throw new PluginInvalidInputException( "No plugin class specified for element " + pattern );
}
// get the class of the user plugged-in type
final Class<?> pluginClass = currDeclaration.getPluginClass();
final String path = getDigester().getMatch();
// create a new Rules object and effectively push it onto a stack of
// rules objects. The stack is actually a linked list; using the
// PluginRules constructor below causes the new instance to link
// to the previous head-of-stack, then the Digester.setRules() makes
// the new instance the new head-of-stack.
final PluginRules newRules = new PluginRules( getDigester(), path, oldRules, pluginClass );
getDigester().setRules( newRules );
if ( debug )
{
log.debug( "PluginCreateRule.begin: installing new plugin: " + "oldrules=" + oldRules.toString()
+ ", newrules=" + newRules.toString() );
}
// load up the custom rules
currDeclaration.configure( getDigester(), pattern );
// create an instance of the plugin class
final Object instance = pluginClass.newInstance();
getDigester().push( instance );
if ( debug )
{
log.debug( "PluginCreateRule.begin" + ": pattern=[" + pattern + "]" + " match=[" + getDigester().getMatch()
+ "]" + " pushed instance of plugin [" + pluginClass.getName() + "]" );
}
// and now we have to fire any custom rules which would have
// been matched by the same path that matched this rule, had
// they been loaded at that time.
final List<Rule> rules = newRules.getDecoratedRules().match( namespace, path, name, attributes );
fireBeginMethods( rules, namespace, name, attributes );
}
/**
* {@inheritDoc}
*/
@Override
public void body( final String namespace, final String name, final String text )
throws Exception
{
// While this class itself has no work to do in the body method,
// we do need to fire the body methods of all dynamically-added
// rules matching the same path as this rule. During begin, we had
// to manually execute the dynamic rules' begin methods because they
// didn't exist in the digester's Rules object when the match begin.
// So in order to ensure consistent ordering of rule execution, the
// PluginRules class deliberately avoids returning any such rules
// in later calls to the match method, instead relying on this
// object to execute them at the appropriate time.
//
// Note that this applies only to rules matching exactly the path
// which is also matched by this PluginCreateRule.
final String path = getDigester().getMatch();
final PluginRules newRules = (PluginRules) getDigester().getRules();
final List<Rule> rules = newRules.getDecoratedRules().match( namespace, path, name, null );
fireBodyMethods( rules, namespace, name, text );
}
/**
* {@inheritDoc}
*/
@Override
public void end( final String namespace, final String name )
throws Exception
{
// see body method for more info
final String path = getDigester().getMatch();
final PluginRules newRules = (PluginRules) getDigester().getRules();
final List<Rule> rules = newRules.getDecoratedRules().match( namespace, path, name, null );
fireEndMethods( rules, namespace, name );
// pop the stack of PluginRules instances, which
// discards all custom rules associated with this plugin
getDigester().setRules( newRules.getParent() );
// and get rid of the instance of the plugin class from the
// digester object stack.
getDigester().pop();
}
/**
* Return the pattern that this Rule is associated with.
* <p>
* In general, Rule instances <i>can</i> be associated with multiple patterns. A PluginCreateRule, however, will
* only function correctly when associated with a single pattern. It is possible to fix this, but I can't be
* bothered just now because this feature is unlikely to be used.
* </p>
*
* @return The pattern value
*/
public String getPattern()
{
return pattern;
}
/**
* Duplicate the processing that the Digester does when firing the begin methods of rules. It would be really nice
* if the Digester class provided a way for this functionality to just be invoked directly.
*
* @param rules The rules which {@link Rule#begin(String, String, Attributes)} method has to be fired
* @param namespace the namespace URI of the matching element, or an empty string if the parser is not namespace
* aware or the element has no namespace
* @param name the local name if the parser is namespace aware, or just the element name otherwise
* @param list The attribute list of this element
* @throws Exception if any error occurs
*/
public void fireBeginMethods( final List<Rule> rules, final String namespace, final String name, final Attributes list )
throws Exception
{
if ( ( rules != null ) && ( !rules.isEmpty() ) )
{
final Log log = getDigester().getLogger();
final boolean debug = log.isDebugEnabled();
for ( final Rule rule : rules )
{
if ( debug )
{
log.debug( " Fire begin() for " + rule );
}
try
{
rule.begin( namespace, name, list );
}
catch ( final Exception e )
{
throw getDigester().createSAXException( e );
}
catch ( final Error e )
{
throw e;
}
}
}
}
/**
* Duplicate the processing that the Digester does when firing the {@link Rule#body(String, String, String)} methods
* of rules.
*
* It would be really nice if the Digester class provided a way for this functionality to just be invoked directly.
*
* @param rules The rules which {@link Rule#body(String, String, String)} method has to be fired
* @param namespace the namespace URI of the matching element, or an empty string if the parser is not namespace
* aware or the element has no namespace
* @param name the local name if the parser is namespace aware, or just the element name otherwise
* @param text The text of the body of this element
* @throws Exception if any error occurs
*/
private void fireBodyMethods( final List<Rule> rules, final String namespaceURI, final String name, final String text )
throws Exception
{
if ( ( rules != null ) && ( !rules.isEmpty() ) )
{
final Log log = getDigester().getLogger();
final boolean debug = log.isDebugEnabled();
for ( final Rule rule : rules )
{
if ( debug )
{
log.debug( " Fire body() for " + rule );
}
try
{
rule.body( namespaceURI, name, text );
}
catch ( final Exception e )
{
throw getDigester().createSAXException( e );
}
catch ( final Error e )
{
throw e;
}
}
}
}
/**
* Duplicate the processing that the Digester does when firing the end methods of rules.
*
* It would be really nice if the Digester class provided a way for this functionality to just be invoked directly.
*
* @param rules The rules which {@link Rule#end(String, String)} method has to be fired
* @param namespaceURI the namespace URI of the matching element, or an empty string if the parser is not namespace
* aware or the element has no namespace
* @param name the local name if the parser is namespace aware, or just the element name otherwise
* @throws Exception if any error occurs
*/
public void fireEndMethods( final List<Rule> rules, final String namespaceURI, final String name )
throws Exception
{
// Fire "end" events for all relevant rules in reverse order
if ( rules != null )
{
final Log log = getDigester().getLogger();
final boolean debug = log.isDebugEnabled();
for ( int i = 0; i < rules.size(); i++ )
{
final int j = ( rules.size() - i ) - 1;
final Rule rule = rules.get( j );
if ( debug )
{
log.debug( " Fire end() for " + rule );
}
try
{
rule.end( namespaceURI, name );
}
catch ( final Exception e )
{
throw getDigester().createSAXException( e );
}
catch ( final Error e )
{
throw e;
}
}
}
}
}