1. /*
  2. * Copyright 2001-2004 The Apache Software Foundation.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package org.apache.commons.betwixt.io;
  17. import java.util.HashMap;
  18. import java.util.List;
  19. import java.util.Map;
  20. import org.apache.commons.betwixt.AttributeDescriptor;
  21. import org.apache.commons.betwixt.ElementDescriptor;
  22. import org.apache.commons.betwixt.XMLBeanInfo;
  23. import org.apache.commons.betwixt.XMLIntrospector;
  24. import org.apache.commons.betwixt.digester.XMLIntrospectorHelper;
  25. import org.apache.commons.betwixt.expression.Context;
  26. import org.apache.commons.betwixt.expression.MethodUpdater;
  27. import org.apache.commons.betwixt.expression.Updater;
  28. import org.apache.commons.digester.Rule;
  29. import org.apache.commons.digester.Rules;
  30. import org.apache.commons.logging.Log;
  31. import org.apache.commons.logging.LogFactory;
  32. import org.xml.sax.Attributes;
  33. /** <p><code>BeanCreateRule</code> is a Digester Rule for creating beans
  34. * from the betwixt XML metadata.</p>
  35. *
  36. * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
  37. * @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a>
  38. * @deprecated 0.5 this Rule does not allowed good integration with other Rules -
  39. * use {@link BeanRuleSet} instead.
  40. */
  41. public class BeanCreateRule extends Rule {
  42. /** Logger */
  43. private static Log log = LogFactory.getLog( BeanCreateRule.class );
  44. /**
  45. * Set log to be used by <code>BeanCreateRule</code> instances
  46. * @param aLog the <code>Log</code> implementation for this class to log to
  47. */
  48. public static void setLog(Log aLog) {
  49. log = aLog;
  50. }
  51. /** The descriptor of this element */
  52. private ElementDescriptor descriptor;
  53. /** The Context used when evaluating Updaters */
  54. private Context context;
  55. /** Have we added our child rules to the digester? */
  56. private boolean addedChildren;
  57. /** In this begin-end loop did we actually create a new bean */
  58. private boolean createdBean;
  59. /** The type of the bean to create */
  60. private Class beanClass;
  61. /** The prefix added to digester rules */
  62. private String pathPrefix;
  63. /** Use id's to match beans? */
  64. private boolean matchIDs = true;
  65. /** allows an attribute to be specified to overload the types of beans used */
  66. private String classNameAttribute = "className";
  67. /**
  68. * Convenience constructor which uses <code>ID's</code> for matching.
  69. *
  70. * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
  71. * @param beanClass the <code>Class</code> to be created
  72. * @param pathPrefix the digester style path
  73. */
  74. public BeanCreateRule(
  75. ElementDescriptor descriptor,
  76. Class beanClass,
  77. String pathPrefix ) {
  78. this( descriptor, beanClass, pathPrefix, true );
  79. }
  80. /**
  81. * Constructor taking a class.
  82. *
  83. * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
  84. * @param beanClass the <code>Class</code> to be created
  85. * @param pathPrefix the digester style path
  86. * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
  87. */
  88. public BeanCreateRule(
  89. ElementDescriptor descriptor,
  90. Class beanClass,
  91. String pathPrefix,
  92. boolean matchIDs ) {
  93. this(
  94. descriptor,
  95. beanClass,
  96. new Context(),
  97. pathPrefix,
  98. matchIDs);
  99. }
  100. /**
  101. * Convenience constructor which uses <code>ID's</code> for matching.
  102. *
  103. * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
  104. * @param beanClass the <code>Class</code> to be created
  105. */
  106. public BeanCreateRule( ElementDescriptor descriptor, Class beanClass ) {
  107. this( descriptor, beanClass, true );
  108. }
  109. /**
  110. * Constructor uses standard qualified name.
  111. *
  112. * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
  113. * @param beanClass the <code>Class</code> to be created
  114. * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
  115. */
  116. public BeanCreateRule( ElementDescriptor descriptor, Class beanClass, boolean matchIDs ) {
  117. this( descriptor, beanClass, descriptor.getQualifiedName() + "/" , matchIDs );
  118. }
  119. /**
  120. * Convenience constructor which uses <code>ID's</code> for match.
  121. *
  122. * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
  123. * @param context the <code>Context</code> to be used to evaluate expressions
  124. * @param pathPrefix the digester path prefix
  125. */
  126. public BeanCreateRule(
  127. ElementDescriptor descriptor,
  128. Context context,
  129. String pathPrefix ) {
  130. this( descriptor, context, pathPrefix, true );
  131. }
  132. /**
  133. * Constructor taking a context.
  134. *
  135. * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
  136. * @param context the <code>Context</code> to be used to evaluate expressions
  137. * @param pathPrefix the digester path prefix
  138. * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
  139. */
  140. public BeanCreateRule(
  141. ElementDescriptor descriptor,
  142. Context context,
  143. String pathPrefix,
  144. boolean matchIDs ) {
  145. this(
  146. descriptor,
  147. descriptor.getSingularPropertyType(),
  148. context,
  149. pathPrefix,
  150. matchIDs );
  151. }
  152. /**
  153. * Base constructor (used by other constructors).
  154. *
  155. * @param descriptor the <code>ElementDescriptor</code> describing the element mapped
  156. * @param beanClass the <code>Class</code> of the bean to be created
  157. * @param context the <code>Context</code> to be used to evaluate expressions
  158. * @param pathPrefix the digester path prefix
  159. * @param matchIDs should <code>ID</code>/<code>IDREF</code>'s be used for matching
  160. */
  161. private BeanCreateRule(
  162. ElementDescriptor descriptor,
  163. Class beanClass,
  164. Context context,
  165. String pathPrefix,
  166. boolean matchIDs ) {
  167. this.descriptor = descriptor;
  168. this.context = context;
  169. this.beanClass = beanClass;
  170. this.pathPrefix = pathPrefix;
  171. this.matchIDs = matchIDs;
  172. if (log.isTraceEnabled()) {
  173. log.trace("Created bean create rule");
  174. log.trace("Descriptor=" + descriptor);
  175. log.trace("Class=" + beanClass);
  176. log.trace("Path prefix=" + pathPrefix);
  177. }
  178. }
  179. // Rule interface
  180. //-------------------------------------------------------------------------
  181. /**
  182. * Process the beginning of this element.
  183. *
  184. * @param attributes The attribute list of this element
  185. */
  186. public void begin(Attributes attributes) {
  187. log.debug( "Called with descriptor: " + descriptor
  188. + " propertyType: " + descriptor.getPropertyType() );
  189. if (log.isTraceEnabled()) {
  190. int attributesLength = attributes.getLength();
  191. if (attributesLength > 0) {
  192. log.trace("Attributes:");
  193. }
  194. for (int i=0, size=attributesLength; i<size; i++) {
  195. log.trace("Local:" + attributes.getLocalName(i));
  196. log.trace("URI:" + attributes.getURI(i));
  197. log.trace("QName:" + attributes.getQName(i));
  198. }
  199. }
  200. // XXX: if a single rule instance gets reused and nesting occurs
  201. // XXX: we should probably use a stack of booleans to test if we created a bean
  202. // XXX: or let digester take nulls, which would be easier for us ;-)
  203. createdBean = false;
  204. Object instance = null;
  205. if ( beanClass != null ) {
  206. instance = createBean(attributes);
  207. if ( instance != null ) {
  208. createdBean = true;
  209. context.setBean( instance );
  210. digester.push(instance);
  211. // if we are a reference to a type we should lookup the original
  212. // as this ElementDescriptor will be 'hollow' and have no child attributes/elements.
  213. // XXX: this should probably be done by the NodeDescriptors...
  214. ElementDescriptor typeDescriptor = getElementDescriptor( descriptor );
  215. //ElementDescriptor typeDescriptor = descriptor;
  216. // iterate through all attributes
  217. AttributeDescriptor[] attributeDescriptors
  218. = typeDescriptor.getAttributeDescriptors();
  219. if ( attributeDescriptors != null ) {
  220. for ( int i = 0, size = attributeDescriptors.length; i < size; i++ ) {
  221. AttributeDescriptor attributeDescriptor = attributeDescriptors[i];
  222. // The following isn't really the right way to find the attribute
  223. // but it's quite robust.
  224. // The idea is that you try both namespace and local name first
  225. // and if this returns null try the qName.
  226. String value = attributes.getValue(
  227. attributeDescriptor.getURI(),
  228. attributeDescriptor.getLocalName()
  229. );
  230. if (value == null) {
  231. value = attributes.getValue(attributeDescriptor.getQualifiedName());
  232. }
  233. if (log.isTraceEnabled()) {
  234. log.trace("Attr URL:" + attributeDescriptor.getURI());
  235. log.trace("Attr LocalName:" + attributeDescriptor.getLocalName() );
  236. log.trace(value);
  237. }
  238. Updater updater = attributeDescriptor.getUpdater();
  239. log.trace(updater);
  240. if ( updater != null && value != null ) {
  241. updater.update( context, value );
  242. }
  243. }
  244. }
  245. addChildRules();
  246. // add bean for ID matching
  247. if ( matchIDs ) {
  248. // XXX need to support custom ID attribute names
  249. // XXX i have a feeling that the current mechanism might need to change
  250. // XXX so i'm leaving this till later
  251. String id = attributes.getValue( "id" );
  252. if ( id != null ) {
  253. getBeansById().put( id, instance );
  254. }
  255. }
  256. }
  257. }
  258. }
  259. /**
  260. * Process the end of this element.
  261. */
  262. public void end() {
  263. if ( createdBean ) {
  264. // force any setters of the parent bean to be called for this new bean instance
  265. Updater updater = descriptor.getUpdater();
  266. Object instance = context.getBean();
  267. Object top = digester.pop();
  268. if (digester.getCount() == 0) {
  269. context.setBean(null);
  270. }else{
  271. context.setBean( digester.peek() );
  272. }
  273. if ( updater != null ) {
  274. if ( log.isDebugEnabled() ) {
  275. log.debug( "Calling updater for: " + descriptor + " with: "
  276. + instance + " on bean: " + context.getBean() );
  277. }
  278. updater.update( context, instance );
  279. } else {
  280. if ( log.isDebugEnabled() ) {
  281. log.debug( "No updater for: " + descriptor + " with: "
  282. + instance + " on bean: " + context.getBean() );
  283. }
  284. }
  285. }
  286. }
  287. /**
  288. * Tidy up.
  289. */
  290. public void finish() {}
  291. // Properties
  292. //-------------------------------------------------------------------------
  293. /**
  294. * The name of the attribute which can be specified in the XML to override the
  295. * type of a bean used at a certain point in the schema.
  296. *
  297. * <p>The default value is 'className'.</p>
  298. *
  299. * @return The name of the attribute used to overload the class name of a bean
  300. */
  301. public String getClassNameAttribute() {
  302. return classNameAttribute;
  303. }
  304. /**
  305. * Sets the name of the attribute which can be specified in
  306. * the XML to override the type of a bean used at a certain
  307. * point in the schema.
  308. *
  309. * <p>The default value is 'className'.</p>
  310. *
  311. * @param classNameAttribute The name of the attribute used to overload the class name of a bean
  312. */
  313. public void setClassNameAttribute(String classNameAttribute) {
  314. this.classNameAttribute = classNameAttribute;
  315. }
  316. // Implementation methods
  317. //-------------------------------------------------------------------------
  318. /**
  319. * Factory method to create new bean instances
  320. *
  321. * @param attributes the <code>Attributes</code> used to match <code>ID/IDREF</code>
  322. * @return the created bean
  323. */
  324. protected Object createBean(Attributes attributes) {
  325. //
  326. // See if we've got an IDREF
  327. //
  328. // XXX This should be customizable but i'm not really convinced by the existing system
  329. // XXX maybe it's going to have to change so i'll use 'idref' for nows
  330. //
  331. if ( matchIDs ) {
  332. String idref = attributes.getValue( "idref" );
  333. if ( idref != null ) {
  334. // XXX need to check up about ordering
  335. // XXX this is a very simple system that assumes that id occurs before idrefs
  336. // XXX would need some thought about how to implement a fuller system
  337. log.trace( "Found IDREF" );
  338. Object bean = getBeansById().get( idref );
  339. if ( bean != null ) {
  340. if (log.isTraceEnabled()) {
  341. log.trace( "Matched bean " + bean );
  342. }
  343. return bean;
  344. }
  345. log.trace( "No match found" );
  346. }
  347. }
  348. Class theClass = beanClass;
  349. try {
  350. String className = attributes.getValue(classNameAttribute);
  351. if (className != null) {
  352. // load the class we should instantiate
  353. theClass = getDigester().getClassLoader().loadClass(className);
  354. }
  355. if (log.isTraceEnabled()) {
  356. log.trace( "Creating instance of " + theClass );
  357. }
  358. return theClass.newInstance();
  359. } catch (Exception e) {
  360. log.warn( "Could not create instance of type: " + theClass.getName() );
  361. return null;
  362. }
  363. }
  364. /** Adds the rules to the digester for all child elements */
  365. protected void addChildRules() {
  366. if ( ! addedChildren ) {
  367. addedChildren = true;
  368. addChildRules( pathPrefix, descriptor );
  369. }
  370. }
  371. /**
  372. * Add child rules for given descriptor at given prefix
  373. *
  374. * @param prefix add child rules at this (digester) path prefix
  375. * @param currentDescriptor add child rules for this descriptor
  376. */
  377. protected void addChildRules(String prefix, ElementDescriptor currentDescriptor ) {
  378. if (log.isTraceEnabled()) {
  379. log.trace("Adding child rules for " + currentDescriptor + "@" + prefix);
  380. }
  381. // if we are a reference to a type we should lookup the original
  382. // as this ElementDescriptor will be 'hollow' and have no child attributes/elements.
  383. // XXX: this should probably be done by the NodeDescriptors...
  384. ElementDescriptor typeDescriptor = getElementDescriptor( currentDescriptor );
  385. //ElementDescriptor typeDescriptor = descriptor;
  386. ElementDescriptor[] childDescriptors = typeDescriptor.getElementDescriptors();
  387. if ( childDescriptors != null ) {
  388. for ( int i = 0, size = childDescriptors.length; i < size; i++ ) {
  389. final ElementDescriptor childDescriptor = childDescriptors[i];
  390. if (log.isTraceEnabled()) {
  391. log.trace("Processing child " + childDescriptor);
  392. }
  393. String qualifiedName = childDescriptor.getQualifiedName();
  394. if ( qualifiedName == null ) {
  395. log.trace( "Ignoring" );
  396. continue;
  397. }
  398. String path = prefix + qualifiedName;
  399. // this code is for making sure that recursive elements
  400. // can also be used..
  401. if ( qualifiedName.equals( currentDescriptor.getQualifiedName() )
  402. && currentDescriptor.getPropertyName() != null ) {
  403. log.trace("Creating generic rule for recursive elements");
  404. int index = -1;
  405. if (childDescriptor.isWrapCollectionsInElement()) {
  406. index = prefix.indexOf(qualifiedName);
  407. if (index == -1) {
  408. // shouldn't happen..
  409. log.debug( "Oops - this shouldn't happen" );
  410. continue;
  411. }
  412. int removeSlash = prefix.endsWith("/")?1:0;
  413. path = "*/" + prefix.substring(index, prefix.length()-removeSlash);
  414. }else{
  415. // we have a element/element type of thing..
  416. ElementDescriptor[] desc = currentDescriptor.getElementDescriptors();
  417. if (desc.length == 1) {
  418. path = "*/"+desc[0].getQualifiedName();
  419. }
  420. }
  421. Rule rule = new BeanCreateRule( childDescriptor, context, path, matchIDs);
  422. addRule(path, rule);
  423. continue;
  424. }
  425. if ( childDescriptor.getUpdater() != null ) {
  426. if (log.isTraceEnabled()) {
  427. log.trace("Element has updater "
  428. + ((MethodUpdater) childDescriptor.getUpdater()).getMethod().getName());
  429. }
  430. if ( childDescriptor.isPrimitiveType() ) {
  431. addPrimitiveTypeRule(path, childDescriptor);
  432. } else {
  433. // add the first child to the path
  434. ElementDescriptor[] grandChildren = childDescriptor.getElementDescriptors();
  435. if ( grandChildren != null && grandChildren.length > 0 ) {
  436. ElementDescriptor grandChild = grandChildren[0];
  437. String grandChildQName = grandChild.getQualifiedName();
  438. if ( grandChildQName != null && grandChildQName.length() > 0 ) {
  439. if (childDescriptor.isWrapCollectionsInElement()) {
  440. path += '/' + grandChildQName;
  441. } else {
  442. path = prefix + (prefix.endsWith("/")?"":"/") + grandChildQName;
  443. }
  444. }
  445. }
  446. // maybe we are adding a primitve type to a collection/array
  447. Class beanClass = childDescriptor.getSingularPropertyType();
  448. if ( XMLIntrospectorHelper.isPrimitiveType( beanClass ) ) {
  449. addPrimitiveTypeRule(path, childDescriptor);
  450. } else {
  451. Rule rule = new BeanCreateRule(
  452. childDescriptor,
  453. context,
  454. path + '/',
  455. matchIDs );
  456. addRule( path, rule );
  457. }
  458. }
  459. } else {
  460. log.trace("Element does not have updater");
  461. }
  462. ElementDescriptor[] grandChildren = childDescriptor.getElementDescriptors();
  463. if ( grandChildren != null && grandChildren.length > 0 ) {
  464. log.trace("Adding grand children");
  465. addChildRules( path + '/', childDescriptor );
  466. }
  467. }
  468. }
  469. }
  470. /**
  471. * Get the associated bean reader.
  472. *
  473. * @return the <code>BeanReader</code digesting the xml
  474. */
  475. protected BeanReader getBeanReader() {
  476. // XXX this breaks the rule contact
  477. // XXX maybe the reader should be passed in the constructor
  478. return (BeanReader) getDigester();
  479. }
  480. /** Allows the navigation from a reference to a property object to the descriptor defining what
  481. * the property is. i.e. doing the join from a reference to a type to lookup its descriptor.
  482. * This could be done automatically by the NodeDescriptors. Refer to TODO.txt for more info.
  483. *
  484. * @param propertyDescriptor find descriptor for property object referenced by this descriptor
  485. * @return descriptor for the singular property class type referenced.
  486. */
  487. protected ElementDescriptor getElementDescriptor( ElementDescriptor propertyDescriptor ) {
  488. Class beanClass = propertyDescriptor.getSingularPropertyType();
  489. if ( beanClass != null ) {
  490. XMLIntrospector introspector = getBeanReader().getXMLIntrospector();
  491. try {
  492. XMLBeanInfo xmlInfo = introspector.introspect( beanClass );
  493. return xmlInfo.getElementDescriptor();
  494. } catch (Exception e) {
  495. log.warn( "Could not introspect class: " + beanClass, e );
  496. }
  497. }
  498. // could not find a better descriptor so use the one we've got
  499. return propertyDescriptor;
  500. }
  501. /**
  502. * Adds a new Digester rule to process the text as a primitive type
  503. *
  504. * @param path digester path where this rule will be attached
  505. * @param childDescriptor update this <code>ElementDescriptor</code> with the body text
  506. */
  507. protected void addPrimitiveTypeRule(String path, final ElementDescriptor childDescriptor) {
  508. Rule rule = new Rule() {
  509. public void body(String text) throws Exception {
  510. childDescriptor.getUpdater().update( context, text );
  511. }
  512. };
  513. addRule( path, rule );
  514. }
  515. /**
  516. * Safely add a rule with given path.
  517. *
  518. * @param path the digester path to add rule at
  519. * @param rule the <code>Rule</code> to add
  520. */
  521. protected void addRule(String path, Rule rule) {
  522. Rules rules = digester.getRules();
  523. List matches = rules.match(null, path);
  524. if ( matches.isEmpty() ) {
  525. if ( log.isDebugEnabled() ) {
  526. log.debug( "Adding digester rule for path: " + path + " rule: " + rule );
  527. }
  528. digester.addRule( path, rule );
  529. } else {
  530. if ( log.isDebugEnabled() ) {
  531. log.debug( "Ignoring duplicate digester rule for path: "
  532. + path + " rule: " + rule );
  533. log.debug( "New rule (not added): " + rule );
  534. log.debug( "Existing rule:" + matches.get(0) );
  535. }
  536. }
  537. }
  538. /**
  539. * Get the map used to index beans (previously read in) by id.
  540. * This is stored in the evaluation context.
  541. *
  542. * @return map indexing beans created by id
  543. */
  544. protected Map getBeansById() {
  545. //
  546. // we need a single index for beans read in by id
  547. // so that we can use them for idref-matching
  548. // store this in the context
  549. //
  550. Map beansById = (Map) context.getVariable( "beans-index" );
  551. if ( beansById == null ) {
  552. // lazy creation
  553. beansById = new HashMap();
  554. context.setVariable( "beans-index", beansById );
  555. log.trace( "Created new index-by-id map" );
  556. }
  557. return beansById;
  558. }
  559. /**
  560. * Return something meaningful for logging.
  561. *
  562. * @return something useful for logging
  563. */
  564. public String toString() {
  565. return "BeanCreateRule [path prefix=" + pathPrefix + " descriptor=" + descriptor + "]";
  566. }
  567. }