1 /*
   2  * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 package catalog;
  24 
  25 import java.io.IOException;
  26 import java.nio.file.Paths;
  27 import javax.xml.catalog.Catalog;
  28 import javax.xml.catalog.CatalogException;
  29 import javax.xml.catalog.CatalogFeatures;
  30 import javax.xml.catalog.CatalogFeatures.Feature;
  31 import javax.xml.catalog.CatalogManager;
  32 import javax.xml.catalog.CatalogResolver;
  33 import javax.xml.catalog.CatalogUriResolver;
  34 import javax.xml.parsers.ParserConfigurationException;
  35 import javax.xml.parsers.SAXParser;
  36 import javax.xml.parsers.SAXParserFactory;
  37 import javax.xml.transform.Source;
  38 import org.testng.Assert;
  39 import org.testng.annotations.BeforeClass;
  40 import org.testng.annotations.DataProvider;
  41 import org.testng.annotations.Test;
  42 import org.xml.sax.Attributes;
  43 import org.xml.sax.ErrorHandler;
  44 import org.xml.sax.InputSource;
  45 import org.xml.sax.SAXException;
  46 import org.xml.sax.XMLReader;
  47 import org.xml.sax.ext.DefaultHandler2;
  48 
  49 /*
  50  * @bug 8081248, 8144966, 8146606, 8146237, 8151154, 8150969, 8151162, 8152527, 8154220
  51  * @summary Tests basic Catalog functions.
  52  */
  53 public class CatalogTest {
  54     static final String KEY_FILES = "javax.xml.catalog.files";
  55 
  56     public String filepath;
  57 
  58     /*
  59      * Initializing fields
  60      */
  61     @BeforeClass
  62     public void setUpClass() throws Exception {
  63         String file1 = getClass().getResource("first_cat.xml").getFile();
  64         if (System.getProperty("os.name").contains("Windows")) {
  65             filepath = file1.substring(1, file1.lastIndexOf("/") + 1);
  66         } else {
  67             filepath = file1.substring(0, file1.lastIndexOf("/") + 1);
  68         }
  69     }
  70 
  71     /*
  72      * @bug 8150187
  73      * NPE is expected if the systemId is null. The specification for systemId
  74      * is as follows:
  75      * A system identifier is required on all external entities. XML
  76      * requires a system identifier on all external entities, so this value is
  77      * always specified.
  78      */
  79     @Test(expectedExceptions = NullPointerException.class)
  80     public void sysIdCantBeNull() {
  81         CatalogResolver catalogResolver = CatalogManager.catalogResolver(CatalogFeatures.defaults());
  82         InputSource is = catalogResolver.resolveEntity("-//FOO//DTD XML Dummy V0.0//EN", null);
  83     }
  84 
  85     /*
  86      * @bug 8156845
  87      * Verifies that an URI reference with a urn:publicid is correctly resolved
  88      * with an uri entry with a publicId.
  89      *
  90      * @param expectedFile is not used in this test, it's kept since we're
  91      * copying the JCK test and its dataProvider. This test may be reused for
  92      * other cases in that test.
  93      */
  94     @Test(dataProvider = "resolveUri")
  95     public void testMatch1(String cFile, String href, String expectedFile, String expectedUri, String msg) {
  96         String catalogFile = getClass().getResource(cFile).getFile();
  97         CatalogUriResolver cur = CatalogManager.catalogUriResolver(CatalogFeatures.defaults(), catalogFile);
  98         Source source = cur.resolve(href, null);
  99         Assert.assertNotNull(source, "Source returned is null");
 100         Assert.assertEquals(expectedUri, source.getSystemId(), msg);
 101     }
 102 
 103     /*
 104      * @bug 8154220
 105      * Verifies that the file input is validated properly. Valid input includes
 106      * multiple file paths separated by semicolon.
 107      */
 108     @Test(dataProvider = "hierarchyOfCatFilesData")
 109     public void hierarchyOfCatFiles2(String systemId, String expectedUri) {
 110         String file1 = getClass().getResource("first_cat.xml").getFile();
 111         String file2 = getClass().getResource("second_cat.xml").getFile();
 112         String files = file1 + ";" + file2;
 113 
 114         try {
 115             System.setProperty(KEY_FILES, files);
 116             CatalogResolver catalogResolver = CatalogManager.catalogResolver(CatalogFeatures.defaults());
 117             String sysId = catalogResolver.resolveEntity(null, systemId).getSystemId();
 118             Assert.assertEquals(sysId, Paths.get(filepath + expectedUri).toUri().toString().replace("///", "/"), "System ID match not right");
 119         } finally {
 120             System.clearProperty(KEY_FILES);
 121         }
 122 
 123     }
 124 
 125     /*
 126      * @bug 8152527
 127      * This test is the same as the JDK test ResolveEntityTests:testMatch1.
 128      * Verifies that the CatalogResolver resolves a publicId and/or systemId as
 129      * expected.
 130      */
 131     @Test(dataProvider = "resolveEntity")
 132     public void testMatch1(String cfile, String prefer, String sysId, String pubId, String expectedUri, String expectedFile, String msg) {
 133         String catalogFile = getClass().getResource(cfile).getFile();
 134         CatalogFeatures features = CatalogFeatures.builder().with(CatalogFeatures.Feature.PREFER, prefer).build();
 135         CatalogResolver catalogResolver = CatalogManager.catalogResolver(features, catalogFile);
 136         InputSource is = catalogResolver.resolveEntity(pubId, sysId);
 137         Assert.assertNotNull(is, msg);
 138         String expected = (expectedUri == null) ? expectedFile : expectedUri;
 139         Assert.assertEquals(expected, is.getSystemId(), msg);
 140     }
 141 
 142     /*
 143      * @bug 8151162
 144      * Verifies that the Catalog matches specified publicId or systemId and returns
 145      * results as expected.
 146      */
 147     @Test(dataProvider = "matchWithPrefer")
 148     public void matchWithPrefer(String prefer, String cfile, String publicId, String systemId, String expected) {
 149         String catalogFile = getClass().getResource(cfile).getFile();
 150         Catalog c = CatalogManager.catalog(CatalogFeatures.builder().with(CatalogFeatures.Feature.PREFER, prefer).build(), catalogFile);
 151         String result;
 152         if (publicId != null && publicId.length() > 0) {
 153             result = c.matchPublic(publicId);
 154         } else {
 155             result = c.matchSystem(systemId);
 156         }
 157         Assert.assertEquals(expected, result);
 158     }
 159 
 160     /*
 161      * @bug 8151162
 162      * Verifies that the CatalogResolver resolves specified publicId or systemId
 163      * in accordance with the prefer setting.
 164      * prefer "system": resolves with a system entry.
 165      *                  Exception: use the public entry when the catalog contains
 166      *                  only public entry and only publicId is specified.
 167      * prefer "public": attempts to resolve with a system entry;
 168      *                  attempts to resolve with a public entry if no matching
 169      *                  system entry is found.
 170      */
 171     @Test(dataProvider = "resolveWithPrefer")
 172     public void resolveWithPrefer(String prefer, String cfile, String publicId, String systemId, String expected) {
 173         String catalogFile = getClass().getResource(cfile).getFile();
 174         CatalogFeatures f = CatalogFeatures.builder().with(CatalogFeatures.Feature.PREFER, prefer).with(CatalogFeatures.Feature.RESOLVE, "ignore").build();
 175         CatalogResolver catalogResolver = CatalogManager.catalogResolver(f, catalogFile);
 176         String result = catalogResolver.resolveEntity(publicId, systemId).getSystemId();
 177         Assert.assertEquals(expected, result);
 178     }
 179 
 180     /**
 181      * @bug 8150969
 182      * Verifies that the defer attribute set in the catalog file takes precedence
 183      * over other settings, in which case, whether next and delegate Catalogs will
 184      * be loaded is determined by the defer attribute.
 185      */
 186     @Test(dataProvider = "invalidAltCatalogs", expectedExceptions = CatalogException.class)
 187     public void testDeferAltCatalogs(String file) {
 188         String catalogFile = getClass().getResource(file).getFile();
 189         CatalogFeatures features = CatalogFeatures.builder().with(CatalogFeatures.Feature.DEFER, "true").build();
 190         /*
 191           Since the defer attribute is set to false in the specified catalog file,
 192           the parent catalog will try to load the alt catalog, which will fail
 193           since it points to an invalid catalog.
 194         */
 195         Catalog catalog = CatalogManager.catalog(features, catalogFile);
 196     }
 197 
 198     /**
 199      * @bug 8151154
 200      * Verifies that the CatalogFeatures' builder throws IllegalArgumentException
 201      * on invalid file inputs.
 202      * @param file the file path
 203      */
 204     @Test(dataProvider = "invalidPaths", expectedExceptions = IllegalArgumentException.class)
 205     public void testFileInput(String file) {
 206             CatalogFeatures features = CatalogFeatures.builder()
 207                 .with(CatalogFeatures.Feature.FILES, file)
 208                 .build();
 209     }
 210 
 211     /**
 212      * @bug 8146237
 213      * PREFER from Features API taking precedence over catalog file
 214      */
 215     @Test
 216     public void testJDK8146237() {
 217         String catalogFile = getClass().getResource("JDK8146237_catalog.xml").getFile();
 218 
 219         try {
 220             CatalogFeatures features = CatalogFeatures.builder().with(CatalogFeatures.Feature.PREFER, "system").build();
 221             Catalog catalog = CatalogManager.catalog(features, catalogFile);
 222             CatalogResolver catalogResolver = CatalogManager.catalogResolver(catalog);
 223             String actualSystemId = catalogResolver.resolveEntity("-//FOO//DTD XML Dummy V0.0//EN", "http://www.oracle.com/alt1sys.dtd").getSystemId();
 224             Assert.assertTrue(actualSystemId.contains("dummy.dtd"), "Resulting id should contain dummy.dtd, indicating a match by publicId");
 225 
 226         } catch (Exception e) {
 227             Assert.fail(e.getMessage());
 228         }
 229     }
 230 
 231     /*
 232        @bug 8146606
 233        Verifies that the resulting systemId does not contain duplicate slashes
 234     */
 235     @Test
 236     public void testRewriteSystem() {
 237         String catalog = getClass().getResource("rewriteCatalog.xml").getFile();
 238 
 239         try {
 240             CatalogResolver resolver = CatalogManager.catalogResolver(CatalogFeatures.defaults(), catalog);
 241             String actualSystemId = resolver.resolveEntity(null, "http://remote.com/dtd/book.dtd").getSystemId();
 242             Assert.assertTrue(!actualSystemId.contains("//"), "result contains duplicate slashes");
 243         } catch (Exception e) {
 244             Assert.fail(e.getMessage());
 245         }
 246 
 247     }
 248 
 249     /*
 250        @bug 8146606
 251        Verifies that the resulting systemId does not contain duplicate slashes
 252     */
 253     @Test
 254     public void testRewriteUri() {
 255         String catalog = getClass().getResource("rewriteCatalog.xml").getFile();
 256 
 257         try {
 258 
 259             CatalogUriResolver resolver = CatalogManager.catalogUriResolver(CatalogFeatures.defaults(), catalog);
 260             String actualSystemId = resolver.resolve("http://remote.com/import/import.xsl", null).getSystemId();
 261             Assert.assertTrue(!actualSystemId.contains("//"), "result contains duplicate slashes");
 262         } catch (Exception e) {
 263             Assert.fail(e.getMessage());
 264         }
 265     }
 266 
 267     /*
 268        @bug 8144966
 269        Verifies that passing null as CatalogFeatures will result in a NPE.
 270     */
 271     @Test(expectedExceptions = NullPointerException.class)
 272     public void testFeatureNull() {
 273         CatalogResolver resolver = CatalogManager.catalogResolver(null, "");
 274 
 275     }
 276 
 277     /*
 278        @bug 8144966
 279        Verifies that passing null as the path will result in a NPE.
 280     */
 281     @Test(expectedExceptions = NullPointerException.class)
 282     public void testPathNull() {
 283         String path = null;
 284         CatalogResolver resolver = CatalogManager.catalogResolver(CatalogFeatures.defaults(), path);
 285     }
 286 
 287     /*
 288        Tests basic catalog feature by using a CatalogResolver instance to
 289     resolve a DTD reference to a locally specified DTD file. If the resolution
 290     is successful, the Handler shall return the value of the entity reference
 291     that matches the expected value.
 292      */
 293     @Test(dataProvider = "catalog")
 294     public void testCatalogResolver(String test, String expected, String catalogFile, String xml, SAXParser saxParser) {
 295         String catalog = null;
 296         if (catalogFile != null) {
 297             catalog = getClass().getResource(catalogFile).getFile();
 298         }
 299         String url = getClass().getResource(xml).getFile();
 300         try {
 301             CatalogResolver cr = CatalogManager.catalogResolver(CatalogFeatures.defaults(), catalog);
 302             XMLReader reader = saxParser.getXMLReader();
 303             reader.setEntityResolver(cr);
 304             MyHandler handler = new MyHandler(saxParser);
 305             reader.setContentHandler(handler);
 306             reader.parse(url);
 307             System.out.println(test + ": expected [" + expected + "] <> actual [" + handler.getResult() + "]");
 308             Assert.assertEquals(handler.getResult(), expected);
 309         } catch (SAXException | IOException e) {
 310             Assert.fail(e.getMessage());
 311         }
 312     }
 313 
 314     /*
 315        Verifies that when there's no match, in this case only an invalid
 316     catalog is provided, the resolver will throw an exception by default.
 317     */
 318     @Test
 319     public void testInvalidCatalog() {
 320         String catalog = getClass().getResource("catalog_invalid.xml").getFile();
 321 
 322         String test = "testInvalidCatalog";
 323         try {
 324             CatalogResolver resolver = CatalogManager.catalogResolver(CatalogFeatures.defaults(), catalog);
 325             String actualSystemId = resolver.resolveEntity(null, "http://remote/xml/dtd/sys/alice/docAlice.dtd").getSystemId();
 326         } catch (Exception e) {
 327             String msg = e.getMessage();
 328             if (msg != null) {
 329                 if (msg.contains("No match found for publicId")) {
 330                     Assert.assertEquals(msg, "No match found for publicId 'null' and systemId 'http://remote/xml/dtd/sys/alice/docAlice.dtd'.");
 331                     System.out.println(test + ": expected [No match found for publicId 'null' and systemId 'http://remote/xml/dtd/sys/alice/docAlice.dtd'.]");
 332                     System.out.println("actual [" + msg + "]");
 333                 }
 334             }
 335         }
 336     }
 337 
 338     /*
 339        Verifies that if resolve is "ignore", an empty InputSource will be returned
 340     when there's no match. The systemId is then null.
 341     */
 342     @Test
 343     public void testIgnoreInvalidCatalog() {
 344         String catalog = getClass().getResource("catalog_invalid.xml").getFile();
 345         CatalogFeatures f = CatalogFeatures.builder()
 346                 .with(Feature.FILES, catalog)
 347                 .with(Feature.PREFER, "public")
 348                 .with(Feature.DEFER, "true")
 349                 .with(Feature.RESOLVE, "ignore")
 350                 .build();
 351 
 352         String test = "testInvalidCatalog";
 353         try {
 354             CatalogResolver resolver = CatalogManager.catalogResolver(f, "");
 355             String actualSystemId = resolver.resolveEntity(null, "http://remote/xml/dtd/sys/alice/docAlice.dtd").getSystemId();
 356             System.out.println("testIgnoreInvalidCatalog: expected [null]");
 357             System.out.println("testIgnoreInvalidCatalog: expected [null]");
 358             System.out.println("actual [" + actualSystemId + "]");
 359             Assert.assertEquals(actualSystemId, null);
 360         } catch (Exception e) {
 361             Assert.fail(e.getMessage());
 362         }
 363     }
 364 
 365 
 366     /*
 367         DataProvider: used to verify CatalogUriResolver's resolve function.
 368         Data columns:
 369         catalog, uri or publicId, expectedFile, expectedUri, msg
 370 
 371         This DataProvider is copied from JCK ResolveTests' dataMatch1
 372      */
 373     @DataProvider(name = "resolveUri")
 374     Object[][] getDataForUriResolver() {
 375         return new Object[][]{
 376             {"uri.xml", "urn:publicid:-:Acme,+Inc.:DTD+Book+Version+1.0", null, "http://local/base/dtd/book.dtd", "Uri in publicId namespace is incorrectly unwrapped"},
 377         };
 378     }
 379 
 380     /*
 381         DataProvider: used to verify hierarchical catalogs. Refer to JCK test
 382     hierarchyOfCatFiles2.
 383      */
 384     @DataProvider(name = "hierarchyOfCatFilesData")
 385     Object[][] getHierarchyOfCatFilesData() {
 386         return new Object[][]{
 387             {"http://www.oracle.com/sequence.dtd", "first.dtd"},
 388             {"http://www.oracle.com/sequence_next.dtd", "next.dtd"},
 389             {"http://www.oracle.com/sequence_second.dtd", "second.dtd"}
 390         };
 391     }
 392 
 393     /*
 394         DataProvider: used to verify CatalogResolver's resolveEntity function.
 395         Data columns:
 396         catalog, prefer, systemId, publicId, expectedUri, expectedFile, msg
 397      */
 398     @DataProvider(name = "resolveEntity")
 399     Object[][] getDataForMatchingBothIds() {
 400         String expected = "http://www.groupxmlbase.com/dtds/rewrite.dtd";
 401         return new Object[][]{
 402             {"rewriteSystem_id.xml", "system", "http://www.sys00test.com/rewrite.dtd", "PUB-404", expected, expected, "Relative rewriteSystem with xml:base at group level failed"},
 403         };
 404     }
 405 
 406     static String id = "http://openjdk.java.net/xml/catalog/dtd/system.dtd";
 407     /*
 408        DataProvider: used to verify how prefer settings affect the result of the
 409         Catalog's matching operation.
 410         Data columns:
 411         prefer, catalog, publicId, systemId, expected result
 412      */
 413     @DataProvider(name = "matchWithPrefer")
 414     Object[][] getDataForMatch() {
 415         return new Object[][]{
 416             {"public", "pubOnly.xml", id, "", "http://local/base/dtd/public.dtd"},
 417             {"public", "sysOnly.xml", id, "", null},
 418             {"public", "sysAndPub.xml", id, "", "http://local/base/dtd/public.dtd"},
 419             {"system", "pubOnly.xml", id, "", "http://local/base/dtd/public.dtd"},
 420             {"system", "sysOnly.xml", id, "", null},
 421             {"system", "sysAndPub.xml", id, "", "http://local/base/dtd/public.dtd"},
 422             {"public", "pubOnly.xml", "", id, null},
 423             {"public", "sysOnly.xml", "", id, "http://local/base/dtd/system.dtd"},
 424             {"public", "sysAndPub.xml", "", id, "http://local/base/dtd/system.dtd"},
 425             {"system", "pubOnly.xml", "", id, null},
 426             {"system", "sysOnly.xml", "", id, "http://local/base/dtd/system.dtd"},
 427             {"system", "sysAndPub.xml", "", id, "http://local/base/dtd/system.dtd"},
 428         };
 429     }
 430 
 431     /*
 432        DataProvider: used to verify how prefer settings affect the result of the
 433         CatalogResolver's resolution operation.
 434         Data columns:
 435         prefer, catalog, publicId, systemId, expected result
 436      */
 437     @DataProvider(name = "resolveWithPrefer")
 438     Object[][] getDataForResolve() {
 439         return new Object[][]{
 440             {"system", "pubOnly.xml", id, "", "http://local/base/dtd/public.dtd"},
 441             {"system", "pubOnly.xml", "", id, null},
 442             {"system", "pubOnly.xml", id, id, null},
 443             {"public", "pubOnly.xml", id, "", "http://local/base/dtd/public.dtd"},
 444             {"public", "pubOnly.xml", "", id, null},
 445             {"public", "pubOnly.xml", id, id, "http://local/base/dtd/public.dtd"},
 446             {"system", "sysOnly.xml", id, "", null},
 447             {"system", "sysOnly.xml", "", id, "http://local/base/dtd/system.dtd"},
 448             {"system", "sysOnly.xml", id, id, "http://local/base/dtd/system.dtd"},
 449             {"public", "sysOnly.xml", id, "", null},
 450             {"public", "sysOnly.xml", "", id, "http://local/base/dtd/system.dtd"},
 451             {"public", "sysOnly.xml", id, id, "http://local/base/dtd/system.dtd"},
 452             {"system", "sysAndPub.xml", id, "", "http://local/base/dtd/public.dtd"},
 453             {"system", "sysAndPub.xml", "", id, "http://local/base/dtd/system.dtd"},
 454             {"system", "sysAndPub.xml", id, id, "http://local/base/dtd/system.dtd"},
 455             {"public", "sysAndPub.xml", id, "", "http://local/base/dtd/public.dtd"},
 456             {"public", "sysAndPub.xml", "", id, "http://local/base/dtd/system.dtd"},
 457             {"public", "sysAndPub.xml", id, id, "http://local/base/dtd/system.dtd"},
 458         };
 459     }
 460     /*
 461        DataProvider: catalogs that contain invalid next or delegate catalogs.
 462                      The defer attribute is set to false.
 463      */
 464     @DataProvider(name = "invalidAltCatalogs")
 465     Object[][] getCatalogs() {
 466         return new Object[][]{
 467             {"defer_false_2.xml"},
 468             {"defer_del_false.xml"}
 469         };
 470     }
 471 
 472     /*
 473        DataProvider: for testing the verification of file paths by
 474                      the CatalogFeatures builder
 475      */
 476     @DataProvider(name = "invalidPaths")
 477     Object[][] getFiles() {
 478         return new Object[][]{
 479             {null},
 480             {""},
 481             {"file:a/b\\c"},
 482             {"file:/../../.."},
 483             {"c:/te:t"},
 484             {"c:/te?t"},
 485             {"c/te*t"},
 486             {"in|valid.txt"},
 487             {"shema:invalid.txt"},
 488         };
 489     }
 490 
 491     /*
 492        DataProvider: provides test name, expected string, the catalog, and XML
 493        document.
 494      */
 495     @DataProvider(name = "catalog")
 496     Object[][] getCatalog() {
 497         return new Object[][]{
 498             {"testSystem", "Test system entry", "catalog.xml", "system.xml", getParser()},
 499             {"testRewriteSystem", "Test rewritesystem entry", "catalog.xml", "rewritesystem.xml", getParser()},
 500             {"testRewriteSystem1", "Test rewritesystem entry", "catalog.xml", "rewritesystem1.xml", getParser()},
 501             {"testSystemSuffix", "Test systemsuffix entry", "catalog.xml", "systemsuffix.xml", getParser()},
 502             {"testDelegateSystem", "Test delegatesystem entry", "catalog.xml", "delegatesystem.xml", getParser()},
 503             {"testPublic", "Test public entry", "catalog.xml", "public.xml", getParser()},
 504             {"testDelegatePublic", "Test delegatepublic entry", "catalog.xml", "delegatepublic.xml", getParser()},
 505         };
 506     }
 507 
 508     SAXParser getParser() {
 509         SAXParser saxParser = null;
 510         try {
 511             SAXParserFactory factory = SAXParserFactory.newInstance();
 512             factory.setNamespaceAware(true);
 513             saxParser = factory.newSAXParser();
 514         } catch (ParserConfigurationException | SAXException e) {
 515         }
 516 
 517         return saxParser;
 518     }
 519 
 520 
 521     /**
 522      * SAX handler
 523      */
 524     public class MyHandler extends DefaultHandler2 implements ErrorHandler {
 525 
 526         StringBuilder textContent = new StringBuilder();
 527         SAXParser saxParser;
 528 
 529         MyHandler(SAXParser saxParser) {
 530             textContent.setLength(0);
 531             this.saxParser = saxParser;
 532         }
 533 
 534         String getResult() {
 535             return textContent.toString();
 536         }
 537 
 538         @Override
 539         public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
 540             textContent.delete(0, textContent.length());
 541             try {
 542                 System.out.println("Element: " + uri + ":" + localName + " " + qName);
 543             } catch (Exception e) {
 544                 throw new SAXException(e);
 545             }
 546 
 547         }
 548 
 549         @Override
 550         public void characters(char ch[], int start, int length) throws SAXException {
 551             textContent.append(ch, start, length);
 552         }
 553     }
 554 }