001    package 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    
022    import java.util.List;
023    
024    import org.apache.commons.digester3.Rule;
025    import org.apache.commons.logging.Log;
026    import 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     */
034    public 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    }