Coverage Report - org.apache.tapestry.util.xml.RuleDirectedParser
 
Classes in this File Line Coverage Branch Coverage Complexity
RuleDirectedParser
82% 
96% 
2.147
 
 1  
 // Copyright 2004, 2005 The Apache Software Foundation
 2  
 //
 3  
 // Licensed under the Apache License, Version 2.0 (the "License");
 4  
 // you may not use this file except in compliance with the License.
 5  
 // You may obtain a copy of the License at
 6  
 //
 7  
 //     http://www.apache.org/licenses/LICENSE-2.0
 8  
 //
 9  
 // Unless required by applicable law or agreed to in writing, software
 10  
 // distributed under the License is distributed on an "AS IS" BASIS,
 11  
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12  
 // See the License for the specific language governing permissions and
 13  
 // limitations under the License.
 14  
 
 15  
 package org.apache.tapestry.util.xml;
 16  
 
 17  
 import java.io.IOException;
 18  
 import java.io.InputStream;
 19  
 import java.net.URL;
 20  
 import java.util.ArrayList;
 21  
 import java.util.HashMap;
 22  
 import java.util.List;
 23  
 import java.util.Map;
 24  
 
 25  
 import javax.xml.parsers.ParserConfigurationException;
 26  
 import javax.xml.parsers.SAXParser;
 27  
 import javax.xml.parsers.SAXParserFactory;
 28  
 
 29  
 import org.apache.commons.logging.Log;
 30  
 import org.apache.commons.logging.LogFactory;
 31  
 import org.apache.hivemind.ApplicationRuntimeException;
 32  
 import org.apache.hivemind.HiveMind;
 33  
 import org.apache.hivemind.Location;
 34  
 import org.apache.hivemind.Resource;
 35  
 import org.apache.hivemind.impl.LocationImpl;
 36  
 import org.apache.tapestry.Tapestry;
 37  
 import org.apache.tapestry.util.RegexpMatcher;
 38  
 import org.xml.sax.Attributes;
 39  
 import org.xml.sax.InputSource;
 40  
 import org.xml.sax.Locator;
 41  
 import org.xml.sax.SAXException;
 42  
 import org.xml.sax.SAXParseException;
 43  
 import org.xml.sax.helpers.DefaultHandler;
 44  
 
 45  
 /**
 46  
  * A simplified version of org.apache.commons.digester.Digester. This version is without as
 47  
  * many bells and whistles but has some key features needed when parsing a document (rather than a
 48  
  * configuration file): <br>
 49  
  * <ul>
 50  
  * <li>Notifications for each bit of text</li>
 51  
  * <li>Tracking of exact location within the document.</li>
 52  
  * </ul>
 53  
  * <p>
 54  
  * Like Digester, there's an object stack and a rule stack. The rules are much simpler (more
 55  
  * coding), in that there's a one-to-one relationship between an element and a rule.
 56  
  * <p>
 57  
  * Based on SAX2.
 58  
  * 
 59  
  * @author Howard Lewis Ship
 60  
  * @since 3.0
 61  
  */
 62  
 
 63  16
 public class RuleDirectedParser extends DefaultHandler
 64  
 {
 65  2
     private static final Log LOG = LogFactory.getLog(RuleDirectedParser.class);
 66  
 
 67  
     private static SAXParserFactory _parserFactory;
 68  
     
 69  
     private Resource _documentLocation;
 70  
 
 71  16
     private List _ruleStack = new ArrayList();
 72  
 
 73  16
     private List _objectStack = new ArrayList();
 74  
 
 75  
     private Object _documentObject;
 76  
 
 77  
     private Locator _locator;
 78  
 
 79  16
     private int _line = -1;
 80  
 
 81  16
     private int _column = -1;
 82  
 
 83  
     private Location _location;
 84  
 
 85  
     private SAXParser _parser;
 86  
 
 87  
     private RegexpMatcher _matcher;
 88  
 
 89  
     private String _uri;
 90  
 
 91  
     private String _localName;
 92  
 
 93  
     private String _qName;
 94  
 
 95  
     /**
 96  
      * Map of {@link IRule}keyed on the local name of the element.
 97  
      */
 98  16
     private Map _ruleMap = new HashMap();
 99  
 
 100  
     /**
 101  
      * Used to accumlate content provided by
 102  
      * {@link org.xml.sax.ContentHandler#characters(char[], int, int)}.
 103  
      */
 104  
 
 105  16
     private StringBuffer _contentBuffer = new StringBuffer();
 106  
 
 107  
     /**
 108  
      * Map of paths to external entities (such as the DTD) keyed on public id.
 109  
      */
 110  
 
 111  16
     private Map _entities = new HashMap();
 112  
 
 113  
     public Object parse(Resource documentLocation)
 114  
     {
 115  16
         if (LOG.isDebugEnabled())
 116  0
             LOG.debug("Parsing: " + documentLocation);
 117  
 
 118  
         try
 119  
         {
 120  16
             _documentLocation = documentLocation;
 121  
 
 122  16
             URL url = documentLocation.getResourceURL();
 123  
 
 124  16
             if (url == null)
 125  0
                 throw new DocumentParseException(Tapestry.format(
 126  
                         "RuleDrivenParser.resource-missing",
 127  
                         documentLocation), documentLocation);
 128  
 
 129  16
             return parse(url);
 130  
         }
 131  
         finally
 132  
         {
 133  16
             _documentLocation = null;
 134  16
             _ruleStack.clear();
 135  16
             _objectStack.clear();
 136  16
             _documentObject = null;
 137  
 
 138  16
             _uri = null;
 139  16
             _localName = null;
 140  16
             _qName = null;
 141  
 
 142  16
             _line = -1;
 143  16
             _column = -1;
 144  16
             _location = null;
 145  16
             _locator = null;
 146  
 
 147  16
             _contentBuffer.setLength(0);
 148  
         }
 149  
     }
 150  
 
 151  
     protected Object parse(URL url)
 152  
     {
 153  16
         if (_parser == null)
 154  16
             _parser = constructParser();
 155  
 
 156  16
         InputStream stream = null;
 157  
 
 158  
         try
 159  
         {
 160  16
             stream = url.openStream();
 161  
         }
 162  0
         catch (IOException ex)
 163  
         {
 164  0
             throw new DocumentParseException(Tapestry.format(
 165  
                     "RuleDrivenParser.unable-to-open-resource",
 166  
                     url), _documentLocation, ex);
 167  16
         }
 168  
 
 169  16
         InputSource source = new InputSource(stream);
 170  
 
 171  
         try
 172  
         {
 173  16
             _parser.parse(source, this);
 174  
 
 175  13
             stream.close();
 176  
         }
 177  3
         catch (Exception ex)
 178  
         {
 179  3
             throw new DocumentParseException(Tapestry.format(
 180  
                     "RuleDrivenParser.parse-error",
 181  
                     url,
 182  
                     ex.getMessage()), getLocation(), ex);
 183  13
         }
 184  
 
 185  13
         if (LOG.isDebugEnabled())
 186  0
             LOG.debug("Document parsed as: " + _documentObject);
 187  
 
 188  13
         return _documentObject;
 189  
     }
 190  
 
 191  
     /**
 192  
      * Returns an {@link Location}representing the current position within the document (depending
 193  
      * on the parser, this may be accurate to column number level).
 194  
      */
 195  
 
 196  
     public Location getLocation()
 197  
     {
 198  75
         if (_locator == null)
 199  0
             return null;
 200  
 
 201  75
         int line = _locator.getLineNumber();
 202  75
         int column = _locator.getColumnNumber();
 203  
 
 204  75
         if (_line != line || _column != column)
 205  
         {
 206  69
             _location = null;
 207  69
             _line = line;
 208  69
             _column = column;
 209  
         }
 210  
 
 211  75
         if (_location == null)
 212  69
             _location = new LocationImpl(_documentLocation, _line, _column);
 213  
 
 214  75
         return _location;
 215  
     }
 216  
 
 217  
     /**
 218  
      * Pushes an object onto the object stack. The first object pushed is the "document object", the
 219  
      * root object returned by the parse.
 220  
      */
 221  
     public void push(Object object)
 222  
     {
 223  38
         if (_documentObject == null)
 224  15
             _documentObject = object;
 225  
 
 226  38
         push(_objectStack, object, "object stack");
 227  38
     }
 228  
 
 229  
     /**
 230  
      * Returns the top object on the object stack.
 231  
      */
 232  
     public Object peek()
 233  
     {
 234  55
         return peek(_objectStack, 0);
 235  
     }
 236  
 
 237  
     /**
 238  
      * Returns an object within the object stack, at depth. Depth 0 is the top object, depth 1 is
 239  
      * the next-to-top object, etc.
 240  
      */
 241  
 
 242  
     public Object peek(int depth)
 243  
     {
 244  0
         return peek(_objectStack, depth);
 245  
     }
 246  
 
 247  
     /**
 248  
      * Removes and returns the top object on the object stack.
 249  
      */
 250  
     public Object pop()
 251  
     {
 252  36
         return pop(_objectStack, "object stack");
 253  
     }
 254  
 
 255  
     private Object pop(List list, String name)
 256  
     {
 257  79
         Object result = list.remove(list.size() - 1);
 258  
 
 259  79
         if (LOG.isDebugEnabled())
 260  0
             LOG.debug("Popped " + result + " off " + name + " (at " + getLocation() + ")");
 261  
 
 262  79
         return result;
 263  
     }
 264  
 
 265  
     private Object peek(List list, int depth)
 266  
     {
 267  130
         return list.get(list.size() - 1 - depth);
 268  
     }
 269  
 
 270  
     private void push(List list, Object object, String name)
 271  
     {
 272  85
         if (LOG.isDebugEnabled())
 273  0
             LOG.debug("Pushing " + object + " onto " + name + " (at " + getLocation() + ")");
 274  
 
 275  85
         list.add(object);
 276  85
     }
 277  
 
 278  
     /**
 279  
      * Pushes a new rule onto the rule stack.
 280  
      */
 281  
 
 282  
     protected void pushRule(IRule rule)
 283  
     {
 284  47
         push(_ruleStack, rule, "rule stack");
 285  47
     }
 286  
 
 287  
     /**
 288  
      * Returns the top rule on the stack.
 289  
      */
 290  
 
 291  
     protected IRule peekRule()
 292  
     {
 293  75
         return (IRule) peek(_ruleStack, 0);
 294  
     }
 295  
 
 296  
     protected IRule popRule()
 297  
     {
 298  43
         return (IRule) pop(_ruleStack, "rule stack");
 299  
     }
 300  
 
 301  
     public void addRule(String localElementName, IRule rule)
 302  
     {
 303  192
         _ruleMap.put(localElementName, rule);
 304  192
     }
 305  
 
 306  
     /**
 307  
      * Registers a public id and corresponding input source. Generally, the source is a wrapper
 308  
      * around an input stream to a package resource.
 309  
      * 
 310  
      * @param publicId
 311  
      *            the public identifier to be registerred, generally the publicId of a DTD related
 312  
      *            to the document being parsed
 313  
      * @param entityPath
 314  
      *            the resource path of the entity, typically a DTD file. Relative files names are
 315  
      *            expected to be stored in the same package as the class file, otherwise a leading
 316  
      *            slash is an absolute pathname within the classpath.
 317  
      */
 318  
 
 319  
     public void registerEntity(String publicId, String entityPath)
 320  
     {
 321  80
         if (LOG.isDebugEnabled())
 322  0
             LOG.debug("Registering " + publicId + " as " + entityPath);
 323  
 
 324  80
         if (_entities == null)
 325  0
             _entities = new HashMap();
 326  
 
 327  80
         _entities.put(publicId, entityPath);
 328  80
     }
 329  
 
 330  
     protected IRule selectRule(String localName, Attributes attributes)
 331  
     {
 332  47
         IRule rule = (IRule) _ruleMap.get(localName);
 333  
 
 334  47
         if (rule == null)
 335  0
             throw new DocumentParseException(Tapestry.format(
 336  
                     "RuleDrivenParser.no-rule-for-element",
 337  
                     localName), getLocation());
 338  
 
 339  47
         return rule;
 340  
     }
 341  
 
 342  
     /**
 343  
      * Uses the {@link Locator}to track the position in the document as a {@link Location}. This
 344  
      * is invoked once (before the initial element is parsed) and the Locator is retained and
 345  
      * queried as to the current file location.
 346  
      * 
 347  
      * @see #getLocation()
 348  
      */
 349  
     public void setDocumentLocator(Locator locator)
 350  
     {
 351  16
         _locator = locator;
 352  16
     }
 353  
 
 354  
     /**
 355  
      * Accumulates the content in a buffer; the concatinated content is provided to the top rule
 356  
      * just before any start or end tag.
 357  
      */
 358  
     public void characters(char[] ch, int start, int length) throws SAXException
 359  
     {
 360  27
         _contentBuffer.append(ch, start, length);
 361  27
     }
 362  
 
 363  
     /**
 364  
      * Pops the top rule off the stack and invokes {@link IRule#endElement(RuleDirectedParser)}.
 365  
      */
 366  
     public void endElement(String uri, String localName, String qName) throws SAXException
 367  
     {
 368  43
         fireContentRule();
 369  
 
 370  43
         _uri = uri;
 371  43
         _localName = localName;
 372  43
         _qName = qName;
 373  
 
 374  43
         popRule().endElement(this);
 375  43
     }
 376  
 
 377  
     /**
 378  
      * Ignorable content is ignored.
 379  
      */
 380  
     public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException
 381  
     {
 382  43
     }
 383  
 
 384  
     /**
 385  
      * Invokes {@link #selectRule(String, Attributes)}to choose a new rule, which is pushed onto
 386  
      * the rule stack, then invokes {@link IRule#startElement(RuleDirectedParser, Attributes)}.
 387  
      */
 388  
     public void startElement(String uri, String localName, String qName, Attributes attributes)
 389  
             throws SAXException
 390  
     {
 391  47
         fireContentRule();
 392  
 
 393  47
         _uri = uri;
 394  47
         _localName = localName;
 395  47
         _qName = qName;
 396  
 
 397  47
         String name = extractName(uri, localName, qName);
 398  
 
 399  47
         IRule newRule = selectRule(name, attributes);
 400  
 
 401  47
         pushRule(newRule);
 402  
 
 403  47
         newRule.startElement(this, attributes);
 404  45
     }
 405  
 
 406  
     private String extractName(String uri, String localName, String qName)
 407  
     {
 408  47
         return HiveMind.isBlank(localName) ? qName : localName;
 409  
     }
 410  
 
 411  
     /**
 412  
      * Uses {@link javax.xml.parsers.SAXParserFactory}to create a instance of a validation SAX2
 413  
      * parser.
 414  
      */
 415  
     protected synchronized SAXParser constructParser()
 416  
     {
 417  16
         if (_parserFactory == null)
 418  
         {
 419  1
             _parserFactory = SAXParserFactory.newInstance();
 420  1
             configureParserFactory(_parserFactory);
 421  
         }
 422  
 
 423  
         try
 424  
         {
 425  16
             return _parserFactory.newSAXParser();
 426  
         }
 427  0
         catch (SAXException ex)
 428  
         {
 429  0
             throw new ApplicationRuntimeException(ex);
 430  
         }
 431  0
         catch (ParserConfigurationException ex)
 432  
         {
 433  0
             throw new ApplicationRuntimeException(ex);
 434  
         }
 435  
 
 436  
     }
 437  
 
 438  
     /**
 439  
      * Configures a {@link SAXParserFactory}before {@link SAXParserFactory#newSAXParser()}is
 440  
      * invoked. The default implementation sets validating to true and namespaceAware to false,
 441  
      */
 442  
 
 443  
     protected void configureParserFactory(SAXParserFactory factory)
 444  
     {
 445  1
         factory.setValidating(true);
 446  1
         factory.setNamespaceAware(false);
 447  1
     }
 448  
 
 449  
     /**
 450  
      * Throws the exception.
 451  
      */
 452  
     public void error(SAXParseException ex) throws SAXException
 453  
     {
 454  1
         fatalError(ex);
 455  0
     }
 456  
 
 457  
     /**
 458  
      * Throws the exception.
 459  
      */
 460  
     public void fatalError(SAXParseException ex) throws SAXException
 461  
     {
 462  
         // Sometimes, a bad parse "corrupts" a parser so that it doesn't
 463  
         // work properly for future parses (of valid documents),
 464  
         // so discard it here.
 465  
 
 466  1
         _parser = null;
 467  
 
 468  1
         throw ex;
 469  
     }
 470  
 
 471  
     /**
 472  
      * Throws the exception.
 473  
      */
 474  
     public void warning(SAXParseException ex) throws SAXException
 475  
     {
 476  0
         fatalError(ex);
 477  0
     }
 478  
 
 479  
     public InputSource resolveEntity(String publicId, String systemId) throws SAXException
 480  
     {
 481  15
         String entityPath = null;
 482  
 
 483  15
         if (LOG.isDebugEnabled())
 484  0
             LOG.debug("Attempting to resolve entity; publicId = " + publicId + " systemId = "
 485  
                     + systemId);
 486  
 
 487  15
         if (_entities != null)
 488  15
             entityPath = (String) _entities.get(publicId);
 489  
 
 490  15
         if (entityPath == null)
 491  
         {
 492  0
             if (LOG.isDebugEnabled())
 493  0
                 LOG.debug("Entity not found, using " + systemId);
 494  
 
 495  0
             return null;
 496  
         }
 497  
 
 498  15
         InputStream stream = getClass().getResourceAsStream(entityPath);
 499  
 
 500  15
         InputSource result = new InputSource(stream);
 501  
 
 502  15
         if (result != null && LOG.isDebugEnabled())
 503  0
             LOG.debug("Resolved " + publicId + " as " + result + " (for " + entityPath + ")");
 504  
 
 505  15
         return result;
 506  
     }
 507  
 
 508  
     /**
 509  
      * Validates that the input value matches against the specified Perl5 pattern. If valid, the
 510  
      * method simply returns. If not a match, then an error message is generated (using the errorKey
 511  
      * and the input value) and a {@link InvalidStringException}is thrown.
 512  
      */
 513  
 
 514  
     public void validate(String value, String pattern, String errorKey)
 515  
     {
 516  21
         if (_matcher == null)
 517  10
             _matcher = new RegexpMatcher();
 518  
 
 519  21
         if (_matcher.matches(pattern, value))
 520  19
             return;
 521  
 
 522  2
         throw new InvalidStringException(Tapestry.format(errorKey, value), value, getLocation());
 523  
     }
 524  
 
 525  
     public Resource getDocumentLocation()
 526  
     {
 527  0
         return _documentLocation;
 528  
     }
 529  
 
 530  
     /**
 531  
      * Returns the localName for the current element.
 532  
      * 
 533  
      * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String,
 534  
      *      java.lang.String, org.xml.sax.Attributes)
 535  
      */
 536  
     public String getLocalName()
 537  
     {
 538  0
         return _localName;
 539  
     }
 540  
 
 541  
     /**
 542  
      * Returns the qualified name for the current element.
 543  
      * 
 544  
      * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String,
 545  
      *      java.lang.String, org.xml.sax.Attributes)
 546  
      */
 547  
     public String getQName()
 548  
     {
 549  0
         return _qName;
 550  
     }
 551  
 
 552  
     /**
 553  
      * Returns the URI for the current element.
 554  
      * 
 555  
      * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String,
 556  
      *      java.lang.String, org.xml.sax.Attributes)
 557  
      */
 558  
     public String getUri()
 559  
     {
 560  0
         return _uri;
 561  
     }
 562  
 
 563  
     private void fireContentRule()
 564  
     {
 565  90
         String content = _contentBuffer.toString();
 566  90
         _contentBuffer.setLength(0);
 567  
 
 568  90
         if (!_ruleStack.isEmpty())
 569  75
             peekRule().content(this, content);
 570  90
     }
 571  
 
 572  
 }