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  */