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 }