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