001package org.apache.commons.digester3.plugins;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.util.List;
023
024import org.apache.commons.digester3.Rule;
025import org.apache.commons.logging.Log;
026import org.xml.sax.Attributes;
027
028/**
029 * Allows the original rules for parsing the configuration file to define points at which plugins are allowed, by
030 * configuring a PluginCreateRule with the appropriate pattern.
031 * 
032 * @since 1.6
033 */
034public class PluginCreateRule
035    extends Rule
036    implements InitializableRule
037{
038
039    // see setPluginClassAttribute
040    private String pluginClassAttrNs = null;
041
042    private String pluginClassAttr = null;
043
044    // see setPluginIdAttribute
045    private String pluginIdAttrNs = null;
046
047    private String pluginIdAttr = null;
048
049    /**
050     * In order to invoke the addRules method on the plugin class correctly, we need to know the pattern which this rule
051     * is matched by.
052     */
053    private String pattern;
054
055    /** A base class that any plugin must derive from. */
056    private Class<?> baseClass = null;
057
058    /**
059     * Info about optional default plugin to be used if no plugin-id is specified in the input data. This can simplify
060     * the syntax where one particular plugin is usually used.
061     */
062    private Declaration defaultPlugin;
063
064    /**
065     * Currently, none of the Rules methods allow exceptions to be thrown. Therefore if this class cannot initialise
066     * itself properly, it cannot cause the digester to stop. Instead, we cache the exception and throw it the first
067     * time the begin() method is called.
068     */
069    private PluginConfigurationException initException;
070
071    // -------------------- constructors -------------------------------------
072
073    /**
074     * Create a plugin rule where the user <i>must</i> specify a plugin-class or plugin-id.
075     * 
076     * @param baseClass is the class which any specified plugin <i>must</i> be descended from.
077     */
078    public PluginCreateRule( Class<?> baseClass )
079    {
080        this.baseClass = baseClass;
081    }
082
083    /**
084     * Create a plugin rule where the user <i>may</i> specify a plugin. If the user doesn't specify a plugin, then the
085     * default class specified in this constructor is used.
086     * 
087     * @param baseClass is the class which any specified plugin <i>must</i> be descended from.
088     * @param dfltPluginClass is the class which will be used if the user doesn't specify any plugin-class or plugin-id.
089     *            This class will have custom rules installed for it just like a declared plugin.
090     */
091    public PluginCreateRule( Class<?> baseClass, Class<?> dfltPluginClass )
092    {
093        this.baseClass = baseClass;
094        if ( dfltPluginClass != null )
095        {
096            defaultPlugin = new Declaration( dfltPluginClass );
097        }
098    }
099
100    /**
101     * Create a plugin rule where the user <i>may</i> specify a plugin. If the user doesn't specify a plugin, then the
102     * default class specified in this constructor is used.
103     * 
104     * @param baseClass is the class which any specified plugin <i>must</i> be descended from.
105     * @param dfltPluginClass is the class which will be used if the user doesn't specify any plugin-class or plugin-id.
106     *            This class will have custom rules installed for it just like a declared plugin.
107     * @param dfltPluginRuleLoader is a RuleLoader instance which knows how to load the custom rules associated with
108     *            this default plugin.
109     */
110    public PluginCreateRule( Class<?> baseClass, Class<?> dfltPluginClass, RuleLoader dfltPluginRuleLoader )
111    {
112        this.baseClass = baseClass;
113        if ( dfltPluginClass != null )
114        {
115            defaultPlugin = new Declaration( dfltPluginClass, dfltPluginRuleLoader );
116        }
117    }
118
119    // ------------------- properties ---------------------------------------
120
121    /**
122     * Sets the xml attribute which the input xml uses to indicate to a PluginCreateRule which class should be
123     * instantiated.
124     * <p>
125     * See {@link PluginRules#setPluginClassAttribute} for more info.
126     *
127     * @param namespaceUri is the namespace uri that the specified attribute is in. If the attribute is in no namespace,
128     *            then this should be null. Note that if a namespace is used, the attrName value should <i>not</i>
129     *            contain any kind of namespace-prefix. Note also that if you are using a non-namespace-aware parser,
130     *            this parameter <i>must</i> be null.
131     * @param attrName is the attribute whose value contains the name of the class to be instantiated.
132     */
133    public void setPluginClassAttribute( String namespaceUri, String attrName )
134    {
135        pluginClassAttrNs = namespaceUri;
136        pluginClassAttr = attrName;
137    }
138
139    /**
140     * Sets the xml attribute which the input xml uses to indicate to a PluginCreateRule which plugin declaration is
141     * being referenced.
142     * <p>
143     * See {@link PluginRules#setPluginIdAttribute} for more info.
144     * 
145     * @param namespaceUri is the namespace uri that the specified attribute is in. If the attribute is in no namespace,
146     *            then this should be null. Note that if a namespace is used, the attrName value should <i>not</i>
147     *            contain any kind of namespace-prefix. Note also that if you are using a non-namespace-aware parser,
148     *            this parameter <i>must</i> be null.
149     * @param attrName is the attribute whose value contains the id of the plugin declaration to be used when
150     *            instantiating an object.
151     */
152    public void setPluginIdAttribute( String namespaceUri, String attrName )
153    {
154        pluginIdAttrNs = namespaceUri;
155        pluginIdAttr = attrName;
156    }
157
158    // ------------------- methods --------------------------------------------
159
160    /**
161     * {@inheritDoc}
162     */
163    public void postRegisterInit( String matchPattern )
164    {
165        Log log = LogUtils.getLogger( getDigester() );
166        boolean debug = log.isDebugEnabled();
167        if ( debug )
168        {
169            log.debug( "PluginCreateRule.postRegisterInit" + ": rule registered for pattern [" + matchPattern + "]" );
170        }
171
172        if ( getDigester() == null )
173        {
174            // We require setDigester to be called before this method.
175            // Note that this means that PluginCreateRule cannot be added
176            // to a Rules object which has not yet been added to a
177            // Digester object.
178            initException =
179                new PluginConfigurationException( "Invalid invocation of postRegisterInit" + ": digester not set." );
180            throw initException;
181        }
182
183        if ( pattern != null )
184        {
185            // We have been called twice, ie a single instance has been
186            // associated with multiple patterns.
187            //
188            // Generally, Digester Rule instances can be associated with
189            // multiple patterns. However for plugins, this creates some
190            // complications. Some day this may be supported; however for
191            // now we just reject this situation.
192            initException =
193                new PluginConfigurationException( "A single PluginCreateRule instance has been mapped to"
194                    + " multiple patterns; this is not supported." );
195            throw initException;
196        }
197
198        if ( matchPattern.indexOf( '*' ) != -1 )
199        {
200            // having wildcards in patterns is extremely difficult to
201            // deal with. For now, we refuse to allow this.
202            //
203            // TODO: check for any chars not valid in xml element name
204            // rather than just *.
205            //
206            // Reasons include:
207            // (a) handling recursive plugins, and
208            // (b) determining whether one pattern is "below" another,
209            // as done by PluginRules. Without wildcards, "below"
210            // just means startsWith, which is easy to check.
211            initException =
212                new PluginConfigurationException( "A PluginCreateRule instance has been mapped to" + " pattern ["
213                    + matchPattern + "]." + " This pattern includes a wildcard character."
214                    + " This is not supported by the plugin architecture." );
215            throw initException;
216        }
217
218        if ( baseClass == null )
219        {
220            baseClass = Object.class;
221        }
222
223        PluginRules rules = (PluginRules) getDigester().getRules();
224        PluginManager pm = rules.getPluginManager();
225
226        // check default class is valid
227        if ( defaultPlugin != null )
228        {
229            if ( !baseClass.isAssignableFrom( defaultPlugin.getPluginClass() ) )
230            {
231                initException =
232                    new PluginConfigurationException( "Default class [" + defaultPlugin.getPluginClass().getName()
233                        + "] does not inherit from [" + baseClass.getName() + "]." );
234                throw initException;
235            }
236
237            try
238            {
239                defaultPlugin.init( getDigester(), pm );
240
241            }
242            catch ( PluginException pwe )
243            {
244
245                throw new PluginConfigurationException( pwe.getMessage(), pwe.getCause() );
246            }
247        }
248
249        // remember the pattern for later
250        pattern = matchPattern;
251
252        if ( pluginClassAttr == null )
253        {
254            // the user hasn't set explicit xml attr names on this rule,
255            // so fetch the default values
256            pluginClassAttrNs = rules.getPluginClassAttrNs();
257            pluginClassAttr = rules.getPluginClassAttr();
258
259            if ( debug )
260            {
261                log.debug( "init: pluginClassAttr set to per-digester values [" + "ns=" + pluginClassAttrNs + ", name="
262                    + pluginClassAttr + "]" );
263            }
264        }
265        else
266        {
267            if ( debug )
268            {
269                log.debug( "init: pluginClassAttr set to rule-specific values [" + "ns=" + pluginClassAttrNs
270                    + ", name=" + pluginClassAttr + "]" );
271            }
272        }
273
274        if ( pluginIdAttr == null )
275        {
276            // the user hasn't set explicit xml attr names on this rule,
277            // so fetch the default values
278            pluginIdAttrNs = rules.getPluginIdAttrNs();
279            pluginIdAttr = rules.getPluginIdAttr();
280
281            if ( debug )
282            {
283                log.debug( "init: pluginIdAttr set to per-digester values [" + "ns=" + pluginIdAttrNs + ", name="
284                    + pluginIdAttr + "]" );
285            }
286        }
287        else
288        {
289            if ( debug )
290            {
291                log.debug( "init: pluginIdAttr set to rule-specific values [" + "ns=" + pluginIdAttrNs + ", name="
292                    + pluginIdAttr + "]" );
293            }
294        }
295    }
296
297    /**
298     * Invoked when the Digester matches this rule against an xml element.
299     * <p>
300     * A new instance of the target class is created, and pushed onto the stack. A new "private" PluginRules object is
301     * then created and set as the digester's default Rules object. Any custom rules associated with the plugin class
302     * are then loaded into that new Rules object. Finally, any custom rules that are associated with the current
303     * pattern (such as SetPropertiesRules) have their begin methods executed.
304     * 
305     * @param namespace the namespace URI of the matching element, or an empty string if the parser is not namespace
306     *            aware or the element has no namespace
307     * @param name the local name if the parser is namespace aware, or just the element name otherwise
308     * @param attributes The attribute list of this element
309     * @throws Exception if any error occurs
310     */
311    @Override
312    public void begin( String namespace, String name, org.xml.sax.Attributes attributes )
313        throws Exception
314    {
315        Log log = getDigester().getLogger();
316        boolean debug = log.isDebugEnabled();
317        if ( debug )
318        {
319            log.debug( "PluginCreateRule.begin" + ": pattern=[" + pattern + "]" + " match=[" + getDigester().getMatch()
320                + "]" );
321        }
322
323        if ( initException != null )
324        {
325            // we had a problem during initialisation that we could
326            // not report then; report it now.
327            throw initException;
328        }
329
330        // load any custom rules associated with the plugin
331        PluginRules oldRules = (PluginRules) getDigester().getRules();
332        PluginManager pluginManager = oldRules.getPluginManager();
333        Declaration currDeclaration = null;
334
335        String pluginClassName;
336        if ( pluginClassAttrNs == null )
337        {
338            // Yep, this is ugly.
339            //
340            // In a namespace-aware parser, the one-param version will
341            // return attributes with no namespace.
342            //
343            // In a non-namespace-aware parser, the two-param version will
344            // never return any attributes, ever.
345            pluginClassName = attributes.getValue( pluginClassAttr );
346        }
347        else
348        {
349            pluginClassName = attributes.getValue( pluginClassAttrNs, pluginClassAttr );
350        }
351
352        String pluginId;
353        if ( pluginIdAttrNs == null )
354        {
355            pluginId = attributes.getValue( pluginIdAttr );
356        }
357        else
358        {
359            pluginId = attributes.getValue( pluginIdAttrNs, pluginIdAttr );
360        }
361
362        if ( pluginClassName != null )
363        {
364            // The user is using a plugin "inline", ie without a previous
365            // explicit declaration. If they have used the same plugin class
366            // before, we have already gone to the effort of creating a
367            // Declaration object, so retrieve it. If there is no existing
368            // declaration object for this class, then create one.
369
370            currDeclaration = pluginManager.getDeclarationByClass( pluginClassName );
371
372            if ( currDeclaration == null )
373            {
374                currDeclaration = new Declaration( pluginClassName );
375                try
376                {
377                    currDeclaration.init( getDigester(), pluginManager );
378                }
379                catch ( PluginException pwe )
380                {
381                    throw new PluginInvalidInputException( pwe.getMessage(), pwe.getCause() );
382                }
383                pluginManager.addDeclaration( currDeclaration );
384            }
385        }
386        else if ( pluginId != null )
387        {
388            currDeclaration = pluginManager.getDeclarationById( pluginId );
389
390            if ( currDeclaration == null )
391            {
392                throw new PluginInvalidInputException( "Plugin id [" + pluginId + "] is not defined." );
393            }
394        }
395        else if ( defaultPlugin != null )
396        {
397            currDeclaration = defaultPlugin;
398        }
399        else
400        {
401            throw new PluginInvalidInputException( "No plugin class specified for element " + pattern );
402        }
403
404        // get the class of the user plugged-in type
405        Class<?> pluginClass = currDeclaration.getPluginClass();
406
407        String path = getDigester().getMatch();
408
409        // create a new Rules object and effectively push it onto a stack of
410        // rules objects. The stack is actually a linked list; using the
411        // PluginRules constructor below causes the new instance to link
412        // to the previous head-of-stack, then the Digester.setRules() makes
413        // the new instance the new head-of-stack.
414        PluginRules newRules = new PluginRules( getDigester(), path, oldRules, pluginClass );
415        getDigester().setRules( newRules );
416
417        if ( debug )
418        {
419            log.debug( "PluginCreateRule.begin: installing new plugin: " + "oldrules=" + oldRules.toString()
420                + ", newrules=" + newRules.toString() );
421        }
422
423        // load up the custom rules
424        currDeclaration.configure( getDigester(), pattern );
425
426        // create an instance of the plugin class
427        Object instance = pluginClass.newInstance();
428        getDigester().push( instance );
429        if ( debug )
430        {
431            log.debug( "PluginCreateRule.begin" + ": pattern=[" + pattern + "]" + " match=[" + getDigester().getMatch()
432                + "]" + " pushed instance of plugin [" + pluginClass.getName() + "]" );
433        }
434
435        // and now we have to fire any custom rules which would have
436        // been matched by the same path that matched this rule, had
437        // they been loaded at that time.
438        List<Rule> rules = newRules.getDecoratedRules().match( namespace, path, name, attributes );
439        fireBeginMethods( rules, namespace, name, attributes );
440    }
441
442    /**
443     * {@inheritDoc}
444     */
445    @Override
446    public void body( String namespace, String name, String text )
447        throws Exception
448    {
449
450        // While this class itself has no work to do in the body method,
451        // we do need to fire the body methods of all dynamically-added
452        // rules matching the same path as this rule. During begin, we had
453        // to manually execute the dynamic rules' begin methods because they
454        // didn't exist in the digester's Rules object when the match begin.
455        // So in order to ensure consistent ordering of rule execution, the
456        // PluginRules class deliberately avoids returning any such rules
457        // in later calls to the match method, instead relying on this
458        // object to execute them at the appropriate time.
459        //
460        // Note that this applies only to rules matching exactly the path
461        // which is also matched by this PluginCreateRule.
462
463        String path = getDigester().getMatch();
464        PluginRules newRules = (PluginRules) getDigester().getRules();
465        List<Rule> rules = newRules.getDecoratedRules().match( namespace, path, name, null );
466        fireBodyMethods( rules, namespace, name, text );
467    }
468
469    /**
470     * {@inheritDoc}
471     */
472    @Override
473    public void end( String namespace, String name )
474        throws Exception
475    {
476        // see body method for more info
477        String path = getDigester().getMatch();
478        PluginRules newRules = (PluginRules) getDigester().getRules();
479        List<Rule> rules = newRules.getDecoratedRules().match( namespace, path, name, null );
480        fireEndMethods( rules, namespace, name );
481
482        // pop the stack of PluginRules instances, which
483        // discards all custom rules associated with this plugin
484        getDigester().setRules( newRules.getParent() );
485
486        // and get rid of the instance of the plugin class from the
487        // digester object stack.
488        getDigester().pop();
489    }
490
491    /**
492     * Return the pattern that this Rule is associated with.
493     * <p>
494     * In general, Rule instances <i>can</i> be associated with multiple patterns. A PluginCreateRule, however, will
495     * only function correctly when associated with a single pattern. It is possible to fix this, but I can't be
496     * bothered just now because this feature is unlikely to be used.
497     * </p>
498     * 
499     * @return The pattern value
500     */
501    public String getPattern()
502    {
503        return pattern;
504    }
505
506    /**
507     * Duplicate the processing that the Digester does when firing the begin methods of rules. It would be really nice
508     * if the Digester class provided a way for this functionality to just be invoked directly.
509     *
510     * @param rules The rules which {@link Rule#begin(String, String, Attributes)} method has to be fired
511     * @param namespace the namespace URI of the matching element, or an empty string if the parser is not namespace
512     *            aware or the element has no namespace
513     * @param name the local name if the parser is namespace aware, or just the element name otherwise
514     * @param list The attribute list of this element
515     * @throws Exception if any error occurs
516     */
517    public void fireBeginMethods( List<Rule> rules, String namespace, String name, Attributes list )
518        throws Exception
519    {
520
521        if ( ( rules != null ) && ( !rules.isEmpty() ) )
522        {
523            Log log = getDigester().getLogger();
524            boolean debug = log.isDebugEnabled();
525            for ( Rule rule : rules )
526            {
527                if ( debug )
528                {
529                    log.debug( "  Fire begin() for " + rule );
530                }
531                try
532                {
533                    rule.begin( namespace, name, list );
534                }
535                catch ( Exception e )
536                {
537                    throw getDigester().createSAXException( e );
538                }
539                catch ( Error e )
540                {
541                    throw e;
542                }
543            }
544        }
545    }
546
547    /**
548     * Duplicate the processing that the Digester does when firing the {@link Rule#body(String, String, String)} methods
549     * of rules.
550     *
551     * It would be really nice if the Digester class provided a way for this functionality to just be invoked directly.
552     *
553     * @param rules The rules which {@link Rule#body(String, String, String)} method has to be fired
554     * @param namespace the namespace URI of the matching element, or an empty string if the parser is not namespace
555     *            aware or the element has no namespace
556     * @param name the local name if the parser is namespace aware, or just the element name otherwise
557     * @param text The text of the body of this element
558     * @throws Exception if any error occurs
559     */
560    private void fireBodyMethods( List<Rule> rules, String namespaceURI, String name, String text )
561        throws Exception
562    {
563        if ( ( rules != null ) && ( !rules.isEmpty() ) )
564        {
565            Log log = getDigester().getLogger();
566            boolean debug = log.isDebugEnabled();
567            for ( Rule rule : rules )
568            {
569                if ( debug )
570                {
571                    log.debug( "  Fire body() for " + rule );
572                }
573                try
574                {
575                    rule.body( namespaceURI, name, text );
576                }
577                catch ( Exception e )
578                {
579                    throw getDigester().createSAXException( e );
580                }
581                catch ( Error e )
582                {
583                    throw e;
584                }
585            }
586        }
587    }
588
589    /**
590     * Duplicate the processing that the Digester does when firing the end methods of rules.
591     *
592     * It would be really nice if the Digester class provided a way for this functionality to just be invoked directly.
593     *
594     * @param rules The rules which {@link Rule#end(String, String)} method has to be fired
595     * @param namespaceURI the namespace URI of the matching element, or an empty string if the parser is not namespace
596     *            aware or the element has no namespace
597     * @param name the local name if the parser is namespace aware, or just the element name otherwise
598     * @throws Exception if any error occurs
599     */
600    public void fireEndMethods( List<Rule> rules, String namespaceURI, String name )
601        throws Exception
602    {
603        // Fire "end" events for all relevant rules in reverse order
604        if ( rules != null )
605        {
606            Log log = getDigester().getLogger();
607            boolean debug = log.isDebugEnabled();
608            for ( int i = 0; i < rules.size(); i++ )
609            {
610                int j = ( rules.size() - i ) - 1;
611                Rule rule = rules.get( j );
612                if ( debug )
613                {
614                    log.debug( "  Fire end() for " + rule );
615                }
616                try
617                {
618                    rule.end( namespaceURI, name );
619                }
620                catch ( Exception e )
621                {
622                    throw getDigester().createSAXException( e );
623                }
624                catch ( Error e )
625                {
626                    throw e;
627                }
628            }
629        }
630    }
631
632}