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