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