    1   /*
    2    * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
    3    *
    4    * This software is open source.
    5    * See the bottom of this file for the licence.
    6    */
    7
    8   package org.dom4j.io;
    9
    10  import junit.framework.AssertionFailedError;
    11
    12  import junit.textui.TestRunner;
    13
    14  import java.io.FileInputStream;
    15  import java.io.IOException;
    16  import java.io.InputStream;
    17  import java.util.ArrayList;
    18  import java.util.Iterator;
    19  import java.util.List;
    20
    21  import org.dom4j.AbstractTestCase;
    22  import org.dom4j.Document;
    23  import org.dom4j.DocumentType;
    24  import org.dom4j.dtd.AttributeDecl;
    25  import org.dom4j.dtd.ElementDecl;
    26  import org.dom4j.dtd.ExternalEntityDecl;
    27  import org.dom4j.dtd.InternalEntityDecl;
    28  import org.dom4j.tree.DefaultDocumentType;
    29
    30  import org.xml.sax.EntityResolver;
    31  import org.xml.sax.InputSource;
    32  import org.xml.sax.SAXException;
    33
    34  /***
    35   * Tests the DocType functionality.
    36   *
    37   * <p>
    38   * Incorporated additional test cases for optional processing of the internal
    39   * and external DTD subsets. The "external" and "mixed" tests both <strong>fail
    40   * </strong> due to a reported bug. See http://tinyurl.com/4dzyq
    41   * </p>
    42   *
    43   * @author <a href="mailto:jstrachan@apache.org">James Strachan </a>
    44   * @version $Revision: 1.4 $
    45   */
    46  public class DTDTest extends AbstractTestCase {
    47      /***
    48       * Input XML file to read <code>xml/dtd/internal.xml</code>- document
    49       * using internal DTD subset, but no external DTD subset.
    50       */
    51      private static final String XML_INTERNAL_FILE = "xml/dtd/internal.xml";
    52
    53      /***
    54       * Input XML file to read <code>xml/dtd/external.xml</code>- document
    55       * using external DTD subset, but no internal DTD subset. The external
    56       * entity should be locatable by either PUBLIC or SYSTEM identifier. The
    57       * testing harness should use an appropriate EntityResolver to locate the
    58       * external entity as a local resource (no internet access).
    59       */
    60      private static final String XML_EXTERNAL_FILE = "xml/dtd/external.xml";
    61
    62      /***
    63       * Input XML file to read <code>xml/dtd/mixed.xml</code>- document using
    64       * both an internal and an external DTD subset. The external entity should
    65       * be locatable by either PUBLIC or SYSTEM identifier. The testing harness
    66       * should use an appropriate EntityResolver to locate the external entity as
    67       * a local resource (no internet access).
    68       */
    69      private static final String XML_MIXED = "xml/dtd/mixed.xml";
    70
    71      /***
    72       * Input XML file to for {@linkEntityResolver}
    73       * <code>xml/dtd/sample.dtd</code>- the external entity providing the
    74       * external DTD subset for test cases that need one. The SYSTEM identifier
    75       * for this external entity is given by {@link#DTD_SYSTEM_ID}.
    76       */
    77      private static final String DTD_FILE = "xml/dtd/sample.dtd";
    78
    79      /***
    80       * The PUBLIC identifier, which is <code>-//dom4j//DTD sample</code>, for
    81       * the external entity providing DTD for tests.
    82       */
    83      protected static final String DTD_PUBLICID = "-//dom4j//DTD sample";
    84
    85      /***
    86       * The SYSTEM identifier, which is <code>sample.dtd</code>, for the
    87       * external entity providing DTD for tests.
    88       */
    89      protected static final String DTD_SYSTEM_ID = "sample.dtd";
    90
    91      public static void main(String[] args) {
    92          TestRunner.run(DTDTest.class);
    93      }
    94
    95      // Test case(s)
    96      // -------------------------------------------------------------------------
    97
    98      /***
    99       * Test verifies correct identification of the internal DTD subset and
    100      * correct non-presence of the external DTD subset.
    101      *
    102      * @throws Exception
    103      *             DOCUMENT ME!
    104      */
    105     public void testInternalDTDSubset() throws Exception {
    106         /*
    107          * Setup the expected DocumentType.
    108          *
    109          * @todo dom4j should expose a DefaultDocumentType constructor that
    110          * accepts only the elementName property. This is used when only an
    111          * internal DTD subset is being provided via the <!DOCTYPE foo [...]>
    112          * syntax, in which case there is neither a SYSTEM nor PUBLIC
    113          * identifier.
    114          */
    115         DocumentType expected = new DefaultDocumentType();
    116
    117         expected.setElementName("greeting");
    118
    119         expected.setInternalDeclarations(getInternalDeclarations());
    120
    121         /*
    122          * Parse the test XML document and compare the expected and actual
    123          * DOCTYPEs.
    124          */
    125         try {
    126             assertSameDocumentType(expected, readDocument(
    127                     XML_INTERNAL_FILE, true, false).getDocType());
    128         } catch (AssertionFailedError ex) {
    129             throw ex;
    130         } catch (Throwable t) {
    131             fail("Not expecting: " + t);
    132         }
    133     }
    134
    135     /***
    136      * Test verifies correct identification of the external DTD subset and
    137      * correct non-presence of the internal DTD subset.
    138      */
    139     public void testExternalDTDSubset() {
    140         /*
    141          * Setup the expected DocumentType.
    142          */
    143         DocumentType expected = new DefaultDocumentType("another-greeting",
    144                 null, DTD_SYSTEM_ID);
    145
    146         expected.setExternalDeclarations(getExternalDeclarations());
    147
    148         /*
    149          * Parse the test XML document and compare the expected and actual
    150          * DOCTYPEs.
    151          */
    152         try {
    153             assertSameDocumentType(expected, readDocument(
    154                     XML_EXTERNAL_FILE, false, true).getDocType());
    155         } catch (AssertionFailedError ex) {
    156             throw ex;
    157         } catch (Throwable t) {
    158             fail("Not expecting: " + t);
    159         }
    160     }
    161
    162     /***
    163      * Test verifies correct identification of the internal and external DTD
    164      * subsets.
    165      */
    166     public void testMixedDTDSubset() {
    167         /*
    168          * Setup the expected DocumentType.
    169          */
    170         DocumentType expected = new DefaultDocumentType("another-greeting",
    171                 null, DTD_SYSTEM_ID);
    172
    173         expected.setInternalDeclarations(getInternalDeclarations());
    174
    175         expected.setExternalDeclarations(getExternalDeclarations());
    176
    177         /*
    178          * Parse the test XML document and compare the expected and actual
    179          * DOCTYPEs.
    180          */
    181         try {
    182             assertSameDocumentType(expected, readDocument(XML_MIXED,
    183                     true, true).getDocType());
    184         } catch (AssertionFailedError ex) {
    185             throw ex;
    186         } catch (Throwable t) {
    187             fail("Not expecting: " + t);
    188         }
    189     }
    190
    191     // Implementation methods
    192     // -------------------------------------------------------------------------
    193
    194     /***
    195      * Test helper method returns a {@link List}of DTD declarations that
    196      * represents the expected internal DTD subset (for the tests that use an
    197      * internal DTD subset).
    198      *
    199      * <p>
    200      * Note: The declarations returned by this method MUST agree those actually
    201      * declared in {@link #XML_INTERNAL_FILE}and {@link
    202      * #XML_MIXED}.
    203      * </p>
    204      *
    205      * <p>
    206      * </p>
    207      *
    208      * @return DOCUMENT ME!
    209      */
    210     protected List getInternalDeclarations() {
    211         List decls = new ArrayList();
    212
    213         decls.add(new ElementDecl("greeting", "(#PCDATA)"));
    214
    215         decls.add(new AttributeDecl("greeting", "foo", "ID", "#IMPLIED", null));
    216
    217         decls.add(new InternalEntityDecl("%boolean", "( true | false )"));
    218
    219         return decls;
    220     }
    221
    222     /***
    223      * Test helper method returns a {@link List}of DTD declarations that
    224      * represents the expected external DTD subset (for the tests that use an
    225      * external DTD subset).
    226      *
    227      * @return DOCUMENT ME!
    228      */
    229     protected List getExternalDeclarations() {
    230         List decls = new ArrayList();
    231
    232         decls.add(new ElementDecl("another-greeting", "(#PCDATA)"));
    233
    234         return decls;
    235     }
    236
    237     /***
    238      * Test helper method compares the expected and actual {@link DocumentType}
    239      * objects, including their internal and external DTD subsets.
    240      *
    241      * <p>
    242      * </p>
    243      *
    244      * @param expected
    245      *            DOCUMENT ME!
    246      * @param actual
    247      *            DOCUMENT ME!
    248      */
    249     protected void assertSameDocumentType(DocumentType expected,
    250             DocumentType actual) {
    251         /*
    252          * Nothing expected?
    253          */
    254         if (expected == null) {
    255             if (actual == null) {
    256                 return; // Nothing found.
    257             } else {
    258                 fail("Not expecting DOCTYPE.");
    259             }
    260         } else {
    261             /*
    262              * Something expected.
    263              */
    264             if (actual == null) {
    265                 fail("Expecting DOCTYPE");
    266             }
    267
    268             log("Expected DocumentType:\n" + expected.toString());
    269
    270             log("Actual DocumentType:\n" + actual.toString());
    271
    272             // Check the internal DTD subset.
    273             assertSameDTDSubset("Internal", expected.getInternalDeclarations(),
    274                     actual.getInternalDeclarations());
    275
    276             // Check the external DTD subset.
    277             assertSameDTDSubset("External", expected.getExternalDeclarations(),
    278                     actual.getExternalDeclarations());
    279         }
    280     }
    281
    282     /***
    283      * Test helper method compares an expected set of DTD declarations with an
    284      * actual set of DTD declarations. This method should be invoked seperately
    285      * for the internal DTD subset and the external DTD subset. The declarations
    286      * must occur in their logical ordering. See <a
    287      * href="http://tinyurl.com/5jhd8">Lexical Handler </a> for conformance
    288      * criteria.
    289      *
    290      * @param txt
    291      *            DOCUMENT ME!
    292      * @param expected
    293      *            DOCUMENT ME!
    294      * @param actual
    295      *            DOCUMENT ME!
    296      *
    297      * @throws AssertionError
    298      *             DOCUMENT ME!
    299      */
    300     protected void assertSameDTDSubset(String txt, List expected, List actual) {
    301         /*
    302          * Nothing expected?
    303          */
    304         if (expected == null) {
    305             if (actual == null) {
    306                 return; // Nothing found.
    307             } else {
    308                 fail("Not expecting " + txt + " DTD subset.");
    309             }
    310         } else {
    311             /*
    312              * Something expected.
    313              */
    314             if (actual == null) {
    315                 fail("Expecting " + txt + " DTD subset.");
    316             }
    317
    318             /*
    319              * Correct #of declarations found?
    320              */
    321             assertEquals(txt + " DTD subset has correct #of declarations"
    322                     + ": expected=[" + expected.toString() + "]" + ", actual=["
    323                     + actual.toString() + "]", expected.size(), actual.size());
    324
    325             /*
    326              * Check order, type, and values of each declaration. Serialization
    327              * tests are done separately.
    328              */
    329             Iterator itr1 = expected.iterator();
    330
    331             Iterator itr2 = actual.iterator();
    332
    333             while (itr1.hasNext()) {
    334                 Object obj1 = itr1.next();
    335
    336                 Object obj2 = itr2.next();
    337
    338                 assertEquals(txt + " DTD subset: Same type of declaration",
    339                         obj1.getClass().getName(), obj2.getClass().getName());
    340
    341                 if (obj1 instanceof AttributeDecl) {
    342                     assertSameDecl((AttributeDecl) obj1, (AttributeDecl) obj2);
    343                 } else if (obj1 instanceof ElementDecl) {
    344                     assertSameDecl((ElementDecl) obj1, (ElementDecl) obj2);
    345                 } else if (obj1 instanceof InternalEntityDecl) {
    346                     assertSameDecl((InternalEntityDecl) obj1,
    347                             (InternalEntityDecl) obj2);
    348                 } else if (obj1 instanceof ExternalEntityDecl) {
    349                     assertSameDecl((ExternalEntityDecl) obj1,
    350                             (ExternalEntityDecl) obj2);
    351                 } else {
    352                     throw new AssertionError("Unexpected declaration type: "
    353                             + obj1.getClass());
    354                 }
    355             }
    356         }
    357     }
    358
    359     /***
    360      * Test helper method compares an expected and an actual {@link
    361      * AttributeDecl}.
    362      *
    363      * @param expected
    364      *            DOCUMENT ME!
    365      * @param actual
    366      *            DOCUMENT ME!
    367      */
    368     public void assertSameDecl(AttributeDecl expected, AttributeDecl actual) {
    369         assertEquals("attributeName is correct", expected.getAttributeName(),
    370                 actual.getAttributeName());
    371
    372         assertEquals("elementName is correct", expected.getElementName(),
    373                 actual.getElementName());
    374
    375         assertEquals("type is correct", expected.getType(), actual.getType());
    376
    377         assertEquals("value is not correct", expected.getValue(), actual
    378                 .getValue());
    379
    380         assertEquals("valueDefault is correct", expected.getValueDefault(),
    381                 actual.getValueDefault());
    382
    383         assertEquals("toString() is correct", expected.toString(), actual
    384                 .toString());
    385     }
    386
    387     /***
    388      * Test helper method compares an expected and an actual {@link
    389      * ElementDecl}.
    390      *
    391      * @param expected
    392      *            DOCUMENT ME!
    393      * @param actual
    394      *            DOCUMENT ME!
    395      */
    396     protected void assertSameDecl(ElementDecl expected, ElementDecl actual) {
    397         assertEquals("name is correct", expected.getName(), actual.getName());
    398
    399         assertEquals("model is not correct", expected.getModel(), actual
    400                 .getModel());
    401
    402         assertEquals("toString() is correct", expected.toString(), actual
    403                 .toString());
    404     }
    405
    406     /***
    407      * Test helper method compares an expected and an actual {@link
    408      * InternalEntityDecl}.
    409      *
    410      * @param expected
    411      *            DOCUMENT ME!
    412      * @param actual
    413      *            DOCUMENT ME!
    414      */
    415     protected void assertSameDecl(InternalEntityDecl expected,
    416             InternalEntityDecl actual) {
    417         assertEquals("name is correct", expected.getName(), actual.getName());
    418
    419         assertEquals("value is not correct", expected.getValue(), actual
    420                 .getValue());
    421
    422         assertEquals("toString() is correct", expected.toString(), actual
    423                 .toString());
    424     }
    425
    426     /***
    427      * Test helper method compares an expected and an actual {@link
    428      * ExternalEntityDecl}.
    429      *
    430      * @param expected
    431      *            DOCUMENT ME!
    432      * @param actual
    433      *            DOCUMENT ME!
    434      */
    435     protected void assertSameDecl(ExternalEntityDecl expected,
    436             ExternalEntityDecl actual) {
    437         assertEquals("name is correct", expected.getName(), actual.getName());
    438
    439         assertEquals("publicID is correct", expected.getPublicID(), actual
    440                 .getPublicID());
    441
    442         assertEquals("systemID is correct", expected.getSystemID(), actual
    443                 .getSystemID());
    444
    445         assertEquals("toString() is correct", expected.toString(), actual
    446                 .toString());
    447     }
    448
    449     /***
    450      * Helper method reads a local resource and parses it as an XML document.
    451      * The internal and external DTD subsets are optionally retained by the
    452      * parser and exposed via the {@link DocumentType}object on the returned
    453      * {@link Document}. The parser is configured with an {@link
    454      * EntityResolver}that knows how to find the local resource identified by
    455      * {@link #DTD_FILE}whose SYSTEM identifier is given by {@link
    456      * #DTD_SYSTEM_ID}.
    457      *
    458      * @param resourceName
    459      *            DOCUMENT ME!
    460      * @param includeInternal
    461      *            DOCUMENT ME!
    462      * @param includeExternal
    463      *            DOCUMENT ME!
    464      *
    465      * @return DOCUMENT ME!
    466      *
    467      * @throws Exception
    468      *             DOCUMENT ME!
    469      */
    470     protected Document readDocument(String resourceName,
    471             boolean includeInternal, boolean includeExternal) throws Exception {
    472         SAXReader reader = new SAXReader();
    473
    474         reader.setIncludeInternalDTDDeclarations(includeInternal);
    475
    476         reader.setIncludeExternalDTDDeclarations(includeExternal);
    477
    478         reader.setEntityResolver(new MyEntityResolver(DTD_FILE,
    479                 DTD_PUBLICID, DTD_SYSTEM_ID));
    480
    481         return getDocument(resourceName, reader);
    482     }
    483
    484     /***
    485      * Provides a resolver for the local test DTD resource.
    486      */
    487     protected static class MyEntityResolver implements EntityResolver {
    488         private String resourceName;
    489
    490         private String pubId;
    491
    492         private String sysId;
    493
    494         public MyEntityResolver(String localResourceName, String publicId,
    495                 String systemId) {
    496             resourceName = localResourceName;
    497
    498             sysId = systemId;
    499         }
    500
    501         public InputSource resolveEntity(String publicId, String systemId)
    502                 throws SAXException, IOException {
    503             if (pubId != null) {
    504                 if (pubId.equals(publicId)) {
    505                     return new InputSource(getInputStream(resourceName));
    506                 }
    507             }
    508
    509             if (sysId.equals(systemId)) {
    510                 return new InputSource(getInputStream(resourceName));
    511             } else {
    512                 return null;
    513             }
    514         }
    515
    516         /***
    517          * Returns an {@link InputStream}that will read from the indicated
    518          * local resource.
    519          *
    520          * @param localResourceName
    521          *            DOCUMENT ME!
    522          *
    523          * @return DOCUMENT ME!
    524          *
    525          * @throws IOException
    526          *             DOCUMENT ME!
    527          */
    528         protected InputStream getInputStream(String localResourceName)
    529                 throws IOException {
    530             InputStream is = new FileInputStream(localResourceName);
    531
    532             return is;
    533         }
    534     }
    535 }
    536
    537 /*
    538  * Redistribution and use of this software and associated documentation
    539  * ("Software"), with or without modification, are permitted provided that the
    540  * following conditions are met:
    541  *
    542  * 1. Redistributions of source code must retain copyright statements and
    543  * notices. Redistributions must also contain a copy of this document.
    544  *
    545  * 2. Redistributions in binary form must reproduce the above copyright notice,
    546  * this list of conditions and the following disclaimer in the documentation
    547  * and/or other materials provided with the distribution.
    548  *
    549  * 3. The name "DOM4J" must not be used to endorse or promote products derived
    550  * from this Software without prior written permission of MetaStuff, Ltd. For
    551  * written permission, please contact dom4j-info@metastuff.com.
    552  *
    553  * 4. Products derived from this Software may not be called "DOM4J" nor may
    554  * "DOM4J" appear in their names without prior written permission of MetaStuff,
    555  * Ltd. DOM4J is a registered trademark of MetaStuff, Ltd.
    556  *
    557  * 5. Due credit should be given to the DOM4J Project - http://www.dom4j.org
    558  *
    559  * THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS ``AS IS'' AND
    560  * ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    561  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    562  * ARE DISCLAIMED. IN NO EVENT SHALL METASTUFF, LTD. OR ITS CONTRIBUTORS BE
    563  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    564  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    565  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    566  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    567  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    568  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    569  * POSSIBILITY OF SUCH DAMAGE.
    570  *
    571  * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
    572  */