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 }