View Javadoc

1   package org.apache.commons.digester3.plugins;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.List;
23  
24  import org.apache.commons.digester3.Rule;
25  import org.apache.commons.logging.Log;
26  import org.xml.sax.Attributes;
27  
28  /**
29   * Allows the original rules for parsing the configuration file to define points at which plugins are allowed, by
30   * configuring a PluginCreateRule with the appropriate pattern.
31   * 
32   * @since 1.6
33   */
34  public class PluginCreateRule
35      extends Rule
36      implements InitializableRule
37  {
38  
39      // see setPluginClassAttribute
40      private String pluginClassAttrNs = null;
41  
42      private String pluginClassAttr = null;
43  
44      // see setPluginIdAttribute
45      private String pluginIdAttrNs = null;
46  
47      private String pluginIdAttr = null;
48  
49      /**
50       * In order to invoke the addRules method on the plugin class correctly, we need to know the pattern which this rule
51       * is matched by.
52       */
53      private String pattern;
54  
55      /** A base class that any plugin must derive from. */
56      private Class<?> baseClass = null;
57  
58      /**
59       * Info about optional default plugin to be used if no plugin-id is specified in the input data. This can simplify
60       * the syntax where one particular plugin is usually used.
61       */
62      private Declaration defaultPlugin;
63  
64      /**
65       * Currently, none of the Rules methods allow exceptions to be thrown. Therefore if this class cannot initialise
66       * itself properly, it cannot cause the digester to stop. Instead, we cache the exception and throw it the first
67       * time the begin() method is called.
68       */
69      private PluginConfigurationException initException;
70  
71      // -------------------- constructors -------------------------------------
72  
73      /**
74       * Create a plugin rule where the user <i>must</i> specify a plugin-class or plugin-id.
75       * 
76       * @param baseClass is the class which any specified plugin <i>must</i> be descended from.
77       */
78      public PluginCreateRule( Class<?> baseClass )
79      {
80          this.baseClass = baseClass;
81      }
82  
83      /**
84       * Create a plugin rule where the user <i>may</i> specify a plugin. If the user doesn't specify a plugin, then the
85       * default class specified in this constructor is used.
86       * 
87       * @param baseClass is the class which any specified plugin <i>must</i> be descended from.
88       * @param dfltPluginClass is the class which will be used if the user doesn't specify any plugin-class or plugin-id.
89       *            This class will have custom rules installed for it just like a declared plugin.
90       */
91      public PluginCreateRule( Class<?> baseClass, Class<?> dfltPluginClass )
92      {
93          this.baseClass = baseClass;
94          if ( dfltPluginClass != null )
95          {
96              defaultPlugin = new Declaration( dfltPluginClass );
97          }
98      }
99  
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 }