Coverage Report - org.apache.commons.betwixt.io.read.ReadContext

Classes in this File Line Coverage Branch Coverage Complexity
ReadContext
92% 
100% 
2.086

 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.read;
 17  
 
 18  
 import java.beans.IntrospectionException;
 19  
 
 20  
 import org.apache.commons.betwixt.AttributeDescriptor;
 21  
 import org.apache.commons.betwixt.BindingConfiguration;
 22  
 import org.apache.commons.betwixt.ElementDescriptor;
 23  
 import org.apache.commons.betwixt.Options;
 24  
 import org.apache.commons.betwixt.XMLBeanInfo;
 25  
 import org.apache.commons.betwixt.XMLIntrospector;
 26  
 import org.apache.commons.betwixt.expression.Context;
 27  
 import org.apache.commons.betwixt.expression.Updater;
 28  
 import org.apache.commons.betwixt.registry.PolymorphicReferenceResolver;
 29  
 import org.apache.commons.betwixt.strategy.ActionMappingStrategy;
 30  
 import org.apache.commons.collections.ArrayStack;
 31  
 import org.apache.commons.logging.Log;
 32  
 import org.apache.commons.logging.LogFactory;
 33  
 import org.xml.sax.Attributes;
 34  
 
 35  
 /**  
 36  
   * <p>Extends <code>Context</code> to provide read specific functionality.</p> 
 37  
   * <p>
 38  
   * Three stacks are used to manage the reading:
 39  
   * </p>
 40  
   * <ul>
 41  
   *     <li><strong>Action mapping stack</strong> contains the {@link MappingAction}'s
 42  
   * used to execute the mapping of the current element and it's ancesters back to the 
 43  
   * document root.</li>
 44  
   *     <li><strong>Result stack</strong> contains the objects which are bound
 45  
   * to the current element and to each of it's ancester's back to the root</li>
 46  
   *     <li><strong>Element mapping stack</strong> records the names of the element
 47  
   * and the classes to which they are bound</li>
 48  
   * </ul>
 49  
   * @author Robert Burrell Donkina
 50  
   * @since 0.5
 51  
   */
 52  
 public class ReadContext extends Context {
 53  
 ;
 54  
         /** Classloader to be used to load beans during reading */
 55  
         private ClassLoader classLoader;
 56  
         /** The read specific configuration */
 57  
         private ReadConfiguration readConfiguration;
 58  
         /** Records the element path together with the locations where classes were mapped*/
 59  3234
         private ArrayStack elementMappingStack = new ArrayStack();
 60  
         /** Contains actions for each element */
 61  3234
         private ArrayStack actionMappingStack = new ArrayStack();
 62  
         /** Stack contains all beans created */
 63  3234
         private ArrayStack objectStack = new ArrayStack();
 64  
     /** Stack contains element descriptors */
 65  3234
     private ArrayStack descriptorStack = new ArrayStack();
 66  
     /** Stack contains updaters */
 67  3234
     private ArrayStack updaterStack = new ArrayStack();
 68  
 
 69  
         private Class rootClass;
 70  
     /** The <code>XMLIntrospector</code> to be used to map the xml*/
 71  
         private XMLIntrospector xmlIntrospector;
 72  
 
 73  
         /** 
 74  
           * Constructs a <code>ReadContext</code> with the same settings 
 75  
           * as an existing <code>Context</code>.
 76  
           * @param context not null
 77  
           * @param readConfiguration not null
 78  
           */
 79  
         public ReadContext(Context context, ReadConfiguration readConfiguration) {
 80  0
                 super(context);
 81  0
                 this.readConfiguration = readConfiguration;
 82  0
         }
 83  
 
 84  
         /**
 85  
           * Constructs a <code>ReadContext</code> with standard log.
 86  
           * @param bindingConfiguration the dynamic configuration, not null
 87  
           * @param readConfiguration the extra read configuration not null
 88  
           */
 89  
         public ReadContext(
 90  
                 BindingConfiguration bindingConfiguration,
 91  
                 ReadConfiguration readConfiguration) {
 92  156
                 this(
 93  156
                         LogFactory.getLog(ReadContext.class),
 94  156
                         bindingConfiguration,
 95  156
                         readConfiguration);
 96  156
         }
 97  
 
 98  
         /** 
 99  
           * Base constructor
 100  
           * @param log log to this Log
 101  
           * @param bindingConfiguration the dynamic configuration, not null
 102  
           * @param readConfiguration the extra read configuration not null
 103  
           */
 104  
         public ReadContext(
 105  
                 Log log,
 106  
                 BindingConfiguration bindingConfiguration,
 107  
                 ReadConfiguration readConfiguration) {
 108  1695
                 super(null, log, bindingConfiguration);
 109  1695
                 this.readConfiguration = readConfiguration;
 110  1695
         }
 111  
 
 112  
         /** 
 113  
           * Constructs a <code>ReadContext</code> 
 114  
           * with the same settings as an existing <code>Context</code>.
 115  
           * @param readContext not null
 116  
           */
 117  
         public ReadContext(ReadContext readContext) {
 118  1539
                 super(readContext);
 119  1539
                 classLoader = readContext.classLoader;
 120  1539
                 readConfiguration = readContext.readConfiguration;
 121  1539
         }
 122  
 
 123  
         /**
 124  
          * Puts a bean into storage indexed by an (xml) ID.
 125  
          *
 126  
          * @param id the ID string of the xml element associated with the bean
 127  
          * @param bean the Object to store, not null
 128  
          */
 129  
         public void putBean(String id, Object bean) {
 130  299
                 getIdMappingStrategy().setReference(this, bean, id);
 131  299
         }
 132  
 
 133  
         /**
 134  
          * Gets a bean from storage by an (xml) ID.
 135  
          *
 136  
          * @param id the ID string of the xml element associated with the bean
 137  
          * @return the Object that the ID references, otherwise null
 138  
          */
 139  
         public Object getBean(String id) {
 140  39
                 return getIdMappingStrategy().getReferenced(this, id);
 141  
         }
 142  
 
 143  
         /** 
 144  
          * Clears the beans indexed by id.
 145  
          */
 146  
         public void clearBeans() {
 147  1500
         getIdMappingStrategy().reset();
 148  1500
         }
 149  
 
 150  
         /**
 151  
           * Gets the classloader to be used.
 152  
           * @return the classloader that should be used to load all classes, possibly null
 153  
           */
 154  
         public ClassLoader getClassLoader() {
 155  1656
                 return classLoader;
 156  
         }
 157  
 
 158  
         /**
 159  
           * Sets the classloader to be used.
 160  
           * @param classLoader the ClassLoader to be used, possibly null
 161  
           */
 162  
         public void setClassLoader(ClassLoader classLoader) {
 163  1539
                 this.classLoader = classLoader;
 164  1539
         }
 165  
 
 166  
         /** 
 167  
           * Gets the <code>BeanCreationChange</code> to be used to create beans 
 168  
           * when an element is mapped.
 169  
           * @return the BeanCreationChain not null
 170  
           */
 171  
         public BeanCreationChain getBeanCreationChain() {
 172  3663
                 return readConfiguration.getBeanCreationChain();
 173  
         }
 174  
 
 175  
     /**
 176  
      * Gets the strategy used to define default mappings actions
 177  
      * for elements.
 178  
      * @return <code>ActionMappingStrategy</code>. not null
 179  
      */
 180  
     public ActionMappingStrategy getActionMappingStrategy() {
 181  7786
         return readConfiguration.getActionMappingStrategy();
 182  
     }
 183  
 
 184  
         /**
 185  
           * Pops the top element from the element mapping stack.
 186  
           * Also removes any mapped class marks below the top element.
 187  
           *
 188  
           * @return the name of the element popped 
 189  
           * if there are any more elements on the stack, otherwise null.
 190  
           * This is the local name if the parser is namespace aware, otherwise the name
 191  
           */
 192  
         public String popElement() {
 193  
         // since the descriptor stack is populated by pushElement,
 194  
         // need to ensure that it's correct popped by popElement
 195  13302
         if (!descriptorStack.isEmpty()) {
 196  13250
             descriptorStack.pop();
 197  
         }
 198  
         
 199  13302
         if (!updaterStack.isEmpty()) {
 200  13250
             updaterStack.pop();
 201  
         }
 202  
         
 203  13302
         popOptions();
 204  
         
 205  13302
                 Object top = null;
 206  13302
                 if (!elementMappingStack.isEmpty()) {
 207  13250
                         top = elementMappingStack.pop();
 208  13250
                         if (top != null) {
 209  13250
                                 if (!(top instanceof String)) {
 210  4379
                                         return popElement();
 211  
                                 }
 212  
                         }
 213  
                 }
 214  
 
 215  8923
                 return (String) top;
 216  
         }
 217  
 
 218  
     /**
 219  
      * Gets the element name for the currently mapped element.
 220  
      * @return the name of the currently mapped element, 
 221  
      * or null if there has been no element mapped 
 222  
      */
 223  
         public String getCurrentElement() {
 224  13
             String result = null;
 225  13
             int stackSize = elementMappingStack.size();
 226  13
             int i = 0;
 227  39
             while ( i < stackSize ) {
 228  26
                 Object mappedElement = elementMappingStack.peek(i);
 229  26
                 if (mappedElement instanceof String) {
 230  13
                     result  = (String) mappedElement;
 231  13
                     break;
 232  
                 }
 233  13
                 ++i;
 234  
             }
 235  13
             return result;
 236  
         }
 237  
 
 238  
         /**
 239  
           * Gets the Class that was last mapped, if there is one.
 240  
           * 
 241  
           * @return the Class last marked as mapped 
 242  
       * or null if no class has been mapped
 243  
           */
 244  
         public Class getLastMappedClass() {
 245  52
         Class lastMapped = null;
 246  52
         for (int i = 0, size = elementMappingStack.size();
 247  195
             i < size;
 248  91
             i++) {
 249  130
             Object entry = elementMappingStack.peek(i);
 250  130
             if (entry instanceof Class) {
 251  39
                 lastMapped = (Class) entry;
 252  39
                 break;
 253  
             }
 254  
         }
 255  52
         return lastMapped;
 256  
         }
 257  
 
 258  
     private ElementDescriptor getParentDescriptor() throws IntrospectionException {
 259  330
         ElementDescriptor result = null;
 260  330
         if (descriptorStack.size() > 1) {
 261  330
             result = (ElementDescriptor) descriptorStack.peek(1);
 262  
         }
 263  330
         return result;
 264  
     }
 265  
     
 266  
 
 267  
         /** 
 268  
           * Pushes the given element onto the element mapping stack.
 269  
           *
 270  
           * @param elementName the local name if the parser is namespace aware,
 271  
           * otherwise the full element name. Not null
 272  
           */
 273  
         public void pushElement(String elementName) throws Exception {
 274  
 
 275  9235
                 elementMappingStack.push(elementName);
 276  
                 // special case to ensure that root class is appropriately marked
 277  
                 //TODO: is this really necessary?
 278  9235
         ElementDescriptor nextDescriptor = null;
 279  9235
                 if (elementMappingStack.size() == 1 && rootClass != null) {
 280  942
                         markClassMap(rootClass);
 281  942
             XMLBeanInfo rootClassInfo 
 282  942
                 = getXMLIntrospector().introspect(rootClass);
 283  942
             nextDescriptor = rootClassInfo.getElementDescriptor();
 284  
                 } else {
 285  8293
             ElementDescriptor currentDescriptor = getCurrentDescriptor();
 286  8293
             if (currentDescriptor != null) {
 287  8033
                 nextDescriptor = currentDescriptor.getElementDescriptor(elementName);
 288  
             }
 289  
         }
 290  9235
         Updater updater = null;
 291  9235
         Options options = null;
 292  9235
         if (nextDescriptor != null) {
 293  8494
             updater = nextDescriptor.getUpdater();
 294  8494
             options = nextDescriptor.getOptions();
 295  
         }
 296  9235
         updaterStack.push(updater);
 297  9235
         descriptorStack.push(nextDescriptor);
 298  9235
         pushOptions(options);
 299  9235
         }
 300  
 
 301  
         /**
 302  
           * Marks the element name stack with a class mapping.
 303  
           * Relative paths and last mapped class are calculated using these marks.
 304  
           * 
 305  
           * @param mappedClazz the Class which has been mapped at the current path, not null
 306  
           */
 307  
         public void markClassMap(Class mappedClazz) throws IntrospectionException {
 308  4600
         if (mappedClazz.isArray()) {
 309  0
             mappedClazz = mappedClazz.getComponentType();
 310  
         }
 311  4600
                 elementMappingStack.push(mappedClazz);
 312  
         
 313  4600
         XMLBeanInfo mappedClassInfo = getXMLIntrospector().introspect(mappedClazz);
 314  4600
         ElementDescriptor mappedElementDescriptor = mappedClassInfo.getElementDescriptor();
 315  4600
         descriptorStack.push(mappedElementDescriptor);
 316  
         
 317  4600
         Updater updater = mappedElementDescriptor.getUpdater();
 318  4600
         updaterStack.push(updater);
 319  4600
         }
 320  
 
 321  
         /**
 322  
          * Pops an action mapping from the stack
 323  
          * @return <code>MappingAction</code>, not null
 324  
          */
 325  
         public MappingAction popMappingAction() {
 326  8819
                 return (MappingAction) actionMappingStack.pop();
 327  
         }
 328  
 
 329  
         /**
 330  
          * Pushs an action mapping onto the stack
 331  
          * @param mappingAction
 332  
          */
 333  
         public void pushMappingAction(MappingAction mappingAction) {
 334  8845
                 actionMappingStack.push(mappingAction);
 335  8845
         }
 336  
 
 337  
         /**
 338  
          * Gets the current mapping action
 339  
          * @return MappingAction 
 340  
          */
 341  
         public MappingAction currentMappingAction() {
 342  17677
                 if (actionMappingStack.size() == 0)
 343  
                 {
 344  942
                         return null;        
 345  
                 }
 346  16735
                 return (MappingAction) actionMappingStack.peek();
 347  
         }
 348  
 
 349  
         public Object getBean() {
 350  11054
                 return objectStack.peek();
 351  
         }
 352  
 
 353  
         public void setBean(Object bean) {
 354  
                 // TODO: maybe need to deprecate the set bean method
 355  
                 // and push into subclass
 356  
                 // for now, do nothing                
 357  13
         }
 358  
 
 359  
     /**
 360  
      * Pops the last mapping <code>Object</code> from the 
 361  
      * stack containing beans that have been mapped.
 362  
      * @return the last bean pushed onto the stack
 363  
      */
 364  
         public Object popBean() {
 365  3476
                 return objectStack.pop();
 366  
         }
 367  
 
 368  
     /**
 369  
      * Pushs a newly mapped <code>Object</code> onto the mapped bean stack.
 370  
      * @param bean
 371  
      */
 372  
         public void pushBean(Object bean) {
 373  3502
                 objectStack.push(bean);
 374  3502
         }
 375  
 
 376  
     /**
 377  
      * Gets the <code>XMLIntrospector</code> to be used to create
 378  
      * the mappings for the xml.
 379  
      * @return <code>XMLIntrospector</code>, not null
 380  
      */
 381  
         public XMLIntrospector getXMLIntrospector() {
 382  
         // read context is not intended to be used by multiple threads
 383  
         // so no need to worry about lazy creation
 384  9975
         if (xmlIntrospector == null) {
 385  117
             xmlIntrospector = new XMLIntrospector();
 386  
         }
 387  9975
                 return xmlIntrospector;
 388  
         }
 389  
 
 390  
     /**
 391  
      * Sets the <code>XMLIntrospector</code> to be used to create
 392  
      * the mappings for the xml.
 393  
      * @param xmlIntrospector <code>XMLIntrospector</code>, not null
 394  
      */
 395  
         public void setXMLIntrospector(XMLIntrospector xmlIntrospector) {
 396  1565
                 this.xmlIntrospector = xmlIntrospector;
 397  1565
         }
 398  
 
 399  
         public Class getRootClass() {
 400  0
                 return rootClass;
 401  
         }
 402  
 
 403  
         public void setRootClass(Class rootClass) {
 404  1539
                 this.rootClass = rootClass;
 405  1539
         }
 406  
 
 407  
     /**
 408  
      * Gets the <code>ElementDescriptor</code> that describes the
 409  
      * mapping for the current element.
 410  
      * @return <code>ElementDescriptor</code> or null if there is no
 411  
      * current mapping
 412  
      * @throws Exception
 413  
      */
 414  
         public ElementDescriptor getCurrentDescriptor() throws Exception {
 415  31777
                 ElementDescriptor result = null;
 416  31777
         if (!descriptorStack.empty()) {
 417  31686
             result = (ElementDescriptor) descriptorStack.peek();
 418  
         }
 419  31777
                 return result;
 420  
         }
 421  
     
 422  
     /**
 423  
      * Populates the object mapped by the <code>AttributeDescriptor</code>s
 424  
      * with the values in the given <code>Attributes</code>.
 425  
      * @param attributeDescriptors <code>AttributeDescriptor</code>s, not null
 426  
      * @param attributes <code>Attributes</code>, not null
 427  
      */
 428  
         public void populateAttributes(
 429  
                 AttributeDescriptor[] attributeDescriptors,
 430  
                 Attributes attributes) {
 431  
 
 432  8047
                 Log log = getLog();
 433  8047
                 if (attributeDescriptors != null) {
 434  8047
                         for (int i = 0, size = attributeDescriptors.length;
 435  17953
                                 i < size;
 436  1859
                                 i++) {
 437  1859
                                 AttributeDescriptor attributeDescriptor =
 438  1859
                                         attributeDescriptors[i];
 439  
 
 440  
                                 // The following isn't really the right way to find the attribute
 441  
                                 // but it's quite robust.
 442  
                                 // The idea is that you try both namespace and local name first
 443  
                                 // and if this returns null try the qName.
 444  1859
                                 String value =
 445  3718
                                         attributes.getValue(
 446  1859
                                                 attributeDescriptor.getURI(),
 447  1859
                                                 attributeDescriptor.getLocalName());
 448  
 
 449  1859
                                 if (value == null) {
 450  78
                                         value =
 451  156
                                                 attributes.getValue(
 452  78
                                                         attributeDescriptor.getQualifiedName());
 453  
                                 }
 454  
 
 455  1859
                                 if (log.isTraceEnabled()) {
 456  0
                                         log.trace("Attr URL:" + attributeDescriptor.getURI());
 457  0
                                         log.trace(
 458  0
                                                 "Attr LocalName:" + attributeDescriptor.getLocalName());
 459  0
                                         log.trace(value);
 460  
                                 }
 461  
 
 462  1859
                                 Updater updater = attributeDescriptor.getUpdater();
 463  1859
                                 log.trace(updater);
 464  1859
                                 if (updater != null && value != null) {
 465  1729
                                         updater.update(this, value);
 466  
                                 }
 467  
                         }
 468  
                 }
 469  8047
         }
 470  
 
 471  
     /**
 472  
      * <p>Pushes an <code>Updater</code> onto the stack.</p>
 473  
      * <p>
 474  
      * <strong>Note</strong>Any action pushing an <code>Updater</code> onto
 475  
      * the stack should take responsibility for popping
 476  
      * the updater from the stack at an appropriate time.
 477  
      * </p>
 478  
      * <p>
 479  
      * <strong>Usage:</strong> this may be used by actions
 480  
      * which require a temporary object to be updated.
 481  
      * Pushing an updater onto the stack allow actions
 482  
      * downstream to transparently update the temporary proxy.
 483  
      * </p>
 484  
      * @param updater Updater, possibly null
 485  
      */
 486  
     public void pushUpdater(Updater updater) {
 487  65
         updaterStack.push(updater);
 488  65
     }
 489  
     
 490  
     /**
 491  
      * Pops the top <code>Updater</code> from the stack.
 492  
      * <p>
 493  
      * <strong>Note</strong>Any action pushing an <code>Updater</code> onto
 494  
      * the stack should take responsibility for popping
 495  
      * the updater from the stack at an appropriate time.
 496  
      * </p>
 497  
      * @return <code>Updater</code>, possibly null
 498  
      */
 499  
     public Updater popUpdater() {
 500  65
         return (Updater) updaterStack.pop();
 501  
     }
 502  
 
 503  
     /**
 504  
      * Gets the current <code>Updater</code>.
 505  
      * This may (or may not) be the updater for the current
 506  
      * descriptor.
 507  
      * If the current descriptor is a bean child,
 508  
      * the the current updater will (most likely) 
 509  
      * be the updater for the property.
 510  
      * Actions (that, for example, use proxy objects)
 511  
      * may push updaters onto the stack.
 512  
      * @return Updater, possibly null
 513  
      */
 514  
     public Updater getCurrentUpdater() {
 515  
         // TODO: think about whether this is right
 516  
         //       it makes some sense to look back up the 
 517  
         //       stack until a non-empty updater is found.
 518  
         //       actions who need to put a stock to this 
 519  
         //       behaviour can always use an ignoring implementation. 
 520  7280
         Updater result = null;
 521  7280
         if (!updaterStack.empty()) {
 522  7280
             result = (Updater) updaterStack.peek();
 523  7280
             if ( result == null && updaterStack.size() >1 ) {
 524  3770
                 result = (Updater) updaterStack.peek(1);
 525  
             }
 526  
         }
 527  7280
         return result;  
 528  
     }
 529  
 
 530  
     /**
 531  
      * Resolves any polymorphism in the element mapping.
 532  
      * @param mapping <code>ElementMapping</code> describing the mapped element
 533  
      * @return <code>null</code> if the type cannot be resolved 
 534  
      * or if the current descriptor is not polymorphic
 535  
      */
 536  
     public Class resolvePolymorphicType(ElementMapping mapping) {
 537  3455
         Class result = null;
 538  3455
         Log log = getLog();
 539  
         try {
 540  3455
             ElementDescriptor currentDescriptor = getCurrentDescriptor();
 541  3455
             if (currentDescriptor != null) {
 542  3455
                 if (currentDescriptor.isPolymorphic()) {
 543  485
                     PolymorphicReferenceResolver resolver = getXMLIntrospector().getPolymorphicReferenceResolver();
 544  485
                     result = resolver.resolveType(mapping, this);
 545  485
                     if (result == null) {
 546  
                         // try the other polymorphic descriptors
 547  330
                         ElementDescriptor parent = getParentDescriptor();
 548  330
                         if (parent != null) {
 549  330
                             ElementDescriptor[] descriptors = parent.getElementDescriptors();
 550  330
                             ElementDescriptor originalDescriptor = mapping.getDescriptor();
 551  330
                             boolean resolved = false;
 552  717
                             for (int i=0; i<descriptors.length;i++) {
 553  465
                                 ElementDescriptor descriptor = descriptors[i];
 554  465
                                 if (descriptor.isPolymorphic()) {
 555  447
                                     mapping.setDescriptor(descriptor);
 556  447
                                     result = resolver.resolveType(mapping, this);
 557  447
                                     if (result != null) {
 558  78
                                         resolved = true;
 559  78
                                         descriptorStack.pop();
 560  78
                                         popOptions();
 561  78
                                         descriptorStack.push(descriptor);
 562  78
                                         pushOptions(descriptor.getOptions());
 563  78
                                         Updater originalUpdater = originalDescriptor.getUpdater();
 564  78
                                         Updater newUpdater = descriptor.getUpdater();
 565  78
                                         substituteUpdater(originalUpdater, newUpdater);
 566  78
                                         break;
 567  
                                     }
 568  
                                 }
 569  
                             }
 570  330
                             if (resolved) {
 571  78
                                 log.debug("Resolved polymorphic type");
 572  
                             } else {
 573  252
                                 log.debug("Failed to resolve polymorphic type");
 574  252
                                 mapping.setDescriptor(originalDescriptor);
 575  
                             }
 576  
                         }
 577  
                     }
 578  
                 }
 579  
             }
 580  0
         } catch (Exception e) {
 581  0
             log.info("Failed to resolved polymorphic type");
 582  0
             log.debug(mapping, e);
 583  
         }
 584  3455
         return result;
 585  
     }
 586  
 
 587  
     /**
 588  
      * Substitutes one updater in the stack for another.
 589  
      * @param originalUpdater <code>Updater</code> possibly null
 590  
      * @param newUpdater <code>Updater</code> possibly null
 591  
      */
 592  
     private void substituteUpdater(Updater originalUpdater, Updater newUpdater) {
 593  
         // recursively pop elements off the stack until the first match is found
 594  
         // TODO: may need to consider using custom NILL object and match descriptors
 595  78
         if (!updaterStack.isEmpty()) {
 596  78
             Updater updater = (Updater) updaterStack.pop();
 597  78
             if (originalUpdater == null && updater == null) {
 598  0
                 updaterStack.push(newUpdater);
 599  78
             } else if (originalUpdater.equals(updater)) {
 600  78
                 updaterStack.push(newUpdater);
 601  
             } else {
 602  0
                 substituteUpdater(originalUpdater, newUpdater);
 603  0
                 updaterStack.push(updater);
 604  
             }
 605  
         }
 606  78
     }
 607  
 
 608  
 }