1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software 
12   * distributed under the License is distributed on an "AS IS" BASIS, 
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
14   * See the License for the specific language governing permissions and 
15   * limitations under the License.
16   */
17  
18  package javax.jdo.util;
19  
20  import java.io.BufferedReader;
21  import java.io.File;
22  import java.io.FileReader;
23  import java.io.InputStream;
24  import java.io.InputStreamReader;
25  import java.io.IOException;
26  import java.io.FileFilter;
27  import java.io.FilenameFilter;
28  
29  import java.security.AccessController;
30  import java.security.PrivilegedAction;
31  import java.util.Arrays;
32  import java.util.ArrayList;
33  import java.util.HashMap;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.StringTokenizer;
38  
39  import javax.jdo.JDOFatalException;
40  
41  import javax.xml.parsers.*;
42  import org.w3c.dom.Document;
43  import org.xml.sax.*;
44  import org.xml.sax.helpers.*;
45  
46  /**
47   * Tests schema files.
48   * <p>
49   */
50  public class XMLTestUtil {
51  
52      /** */
53      protected static String BASEDIR = System.getProperty("basedir", ".");
54  
55      /** "http://www.w3.org/2001/XMLSchema" target="alexandria_uri">http://www.w3.org/2001/XMLSchema" */
56      protected static final String XSD_TYPE = 
57          "http://www.w3.org/2001/XMLSchema";
58  
59      /** */
60      protected static final String SCHEMA_LANGUAGE_PROP = 
61          "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
62  
63      /** */
64      protected static final String SCHEMA_LOCATION_PROP =
65          "http://apache.org/xml/properties/schema/external-schemaLocation";
66      
67      /** jdo namespace */
68      protected static final String JDO_XSD_NS = 
69          "http://java.sun.com/xml/ns/jdo/jdo";
70  
71      /** orm namespace */
72      protected static final String ORM_XSD_NS = 
73          "http://java.sun.com/xml/ns/jdo/orm";
74  
75      /** jdoquery namespace */
76      protected static final String JDOQUERY_XSD_NS = 
77          "http://java.sun.com/xml/ns/jdo/jdoquery";
78  
79      /** jdo xsd file */
80      protected static final File JDO_XSD_FILE = 
81          new File(BASEDIR + "/target/classes/javax/jdo/jdo_2_2.xsd");
82  
83      /** orm xsd file */
84      protected static final File ORM_XSD_FILE = 
85          new File(BASEDIR + "/target/classes/javax/jdo/orm_2_2.xsd");
86  
87      /** jdoquery xsd file */
88      protected static final File JDOQUERY_XSD_FILE = 
89          new File(BASEDIR + "/target/classes/javax/jdo/jdoquery_2_2.xsd");
90  
91      /** Entity resolver */
92      protected static final EntityResolver resolver = new JDOEntityResolver();
93  
94      /** Error handler */
95      protected static final Handler handler = new Handler();
96  
97      /** Name of the metadata property, a comma separated list of JDO metadata
98       * file or directories containing such files. */
99      protected static String METADATA_PROP = "javax.jdo.metadata";
100 
101     /** Name of the recursive property, allowing recursive search of metadata
102      * files. */
103     protected static String RECURSIVE_PROP = "javax.jdo.recursive";
104     
105     /** Separator character for the metadata property. */
106     protected static final String DELIM = ",;";
107 
108     /** Newline. */
109     protected static final String NL = System.getProperty("line.separator");
110 
111     /** XSD builder for jdo namespace. */
112     private final DocumentBuilder jdoXsdBuilder = 
113         createBuilder(JDO_XSD_NS + " " + JDO_XSD_FILE.toURI().toString());
114     
115     /** XSD builder for orm namespace. */
116     private final DocumentBuilder ormXsdBuilder = 
117         createBuilder(ORM_XSD_NS + " " + ORM_XSD_FILE.toURI().toString());
118     
119     /** XSD builder for jdoquery namespace. */
120     private final DocumentBuilder jdoqueryXsdBuilder = 
121         createBuilder(JDOQUERY_XSD_NS + " " + JDOQUERY_XSD_FILE.toURI().toString());
122     
123     /** DTD builder. */
124     private final DocumentBuilder dtdBuilder = createBuilder(true);
125     
126     /** Non validating builder. */
127     private final DocumentBuilder nonValidatingBuilder = createBuilder(false);
128 
129     /** Create XSD builder. */
130     private DocumentBuilder createBuilder(String location) {
131         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
132         factory.setValidating(true);
133         factory.setNamespaceAware(true);
134         factory.setAttribute(SCHEMA_LANGUAGE_PROP, XSD_TYPE);
135         factory.setAttribute(SCHEMA_LOCATION_PROP, location);
136         return getParser(factory);
137     }
138 
139     /** Create builder. */
140     private DocumentBuilder createBuilder(boolean validating) {
141         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
142         factory.setValidating(validating);
143         factory.setNamespaceAware(true);
144         return getParser(factory);
145     }
146 
147     /** Returns a parser obtained from specified factroy. */
148     private DocumentBuilder getParser(DocumentBuilderFactory factory) {
149         try {
150             DocumentBuilder builder = factory.newDocumentBuilder();
151             builder.setEntityResolver(resolver);
152             builder.setErrorHandler(handler);
153             return builder;
154         } catch (ParserConfigurationException ex) {
155             throw new JDOFatalException("Cannot create XML parser", ex);
156         }
157     }
158 
159     /** Parse the specified files. The valid parameter determines whether the
160      * specified files are valid JDO metadata files. The method does not throw
161      * an exception on an error, instead it instead it returns the error
162      * message(s) as string. 
163      */ 
164     public String checkXML(File[] files, boolean valid) {
165         StringBuffer messages = new StringBuffer();
166         for (int i = 0; i < files.length; i++) {
167             String msg = checkXML(files[i], valid);
168             if (msg != null) {
169                 messages.append(msg);
170             }
171         }
172         return (messages.length() == 0) ? null : messages.toString();
173     }
174     
175     /** Parse the specified files using a non validating parser. The method
176      * does not throw an exception on an error, instead it instead it returns
177      * the error message(s) as string.
178      */ 
179     public String checkXMLNonValidating(File[] files) {
180         StringBuffer messages = new StringBuffer();
181         for (int i = 0; i < files.length; i++) {
182             String msg = checkXML(nonValidatingBuilder, files[i], true);
183             if (msg != null) {
184                 messages.append(msg);
185             }
186         }
187         return (messages.length() == 0) ? null : messages.toString();
188     }
189     
190      /** Parse the specified file. The method checks whether it is a XSD or
191      * DTD base file and parses the file using a builder according to the file
192      * name suffix. The valid parameter determines whether the specified files
193      * are valid JDO metadata files. The method does not throw an exception on
194      * an error, instead it returns the error message(s) as string. 
195      */
196     private String checkXML(File file, boolean valid) {
197         String messages = null;
198         String fileName = file.getName();
199         try {
200             if (isDTDBased(file)) {
201                 messages = checkXML(dtdBuilder, file, valid);
202             } else if (fileName.endsWith(".jdo")) {
203                 messages = checkXML(jdoXsdBuilder, file, valid);
204             } else if (fileName.endsWith(".orm")) {
205                 messages = checkXML(ormXsdBuilder, file, valid);
206             } else if (fileName.endsWith(".jdoquery")) {
207                 messages = checkXML(jdoqueryXsdBuilder, file, valid);
208             }
209         } catch (SAXException ex) {
210             messages = ex.getMessage();
211         }
212         return messages;
213     }
214 
215     /** Parse the specified file using the specified builder. The valid
216      * parameter determines whether the specified files are valid JDO metadata
217      * files. The method does not throw an exception on an error, instead it
218      * returns the error message(s) as string.
219      */
220     private String checkXML(DocumentBuilder builder, File file, boolean valid) {
221         String messages = null;
222         handler.init(file);
223         try {
224             builder.parse(file);
225         } catch (SAXParseException ex) {
226             handler.error(ex);
227         } catch (Exception ex) {
228             messages = "Fatal error processing " + file.getName() + ":  " + ex + NL;
229         }
230         if (messages == null) {
231             messages = handler.getMessages();
232         }
233         if (!valid) {
234             if (messages != null) {
235                 // expected error for negative test
236                 messages = null;
237             } else {
238                 messages = file.getName() + " is not valid, " +
239                     "but the parser did not catch the error.";
240             } 
241         }
242         return messages;
243     }
244 
245     /** Checks whether the specifeid file is DTD or XSD based. The method
246      * throws a SAXException if the file has syntax errors. */
247     private boolean isDTDBased(File file) throws SAXException {
248         handler.init(file);
249         try {
250             Document document = nonValidatingBuilder.parse(file);
251             return document.getDoctype() != null;
252         } catch (SAXParseException ex) {
253             handler.error(ex);
254             throw new SAXException(handler.getMessages());
255         } catch (Exception ex) {
256             throw new SAXException(
257                 "Fatal error processing " + file.getName() + ":  " + ex);
258         }
259     }
260     
261     /** ErrorHandler implementation. */
262     private static class Handler implements ErrorHandler {
263 
264         private File fileUnderTest;
265         private String[] lines;
266         private StringBuffer messages;
267 
268         public void error(SAXParseException ex) {
269             append("Handler.error: ", ex);
270         }
271             
272         public void fatalError(SAXParseException ex) {
273             append("Handler.fatalError: ", ex);
274         }
275         
276         public void warning(SAXParseException ex) {
277             append("Handler.warning: ", ex);
278         }
279         
280         public void init(File file) {
281             this.fileUnderTest = file;
282             this.messages = new StringBuffer();
283             this.lines = null;
284         }
285 
286         public String getMessages() {
287             return (messages.length() == 0) ? null : messages.toString();
288         }
289 
290         private void append(String prefix, SAXParseException ex) {
291             int lineNumber = ex.getLineNumber();
292             int columnNumber = ex.getColumnNumber();
293             messages.append("------------------------").append(NL);
294             messages.append(prefix).append(fileUnderTest.getName());
295             messages.append(" [line=").append(lineNumber);
296             messages.append(", col=").append(columnNumber).append("]: ");
297             messages.append(ex.getMessage()).append(NL);
298             messages.append(getErrorLocation(lineNumber, columnNumber));
299         }
300 
301         private String[] getLines() {
302             if (lines == null) {
303                 try {
304                     BufferedReader bufferedReader =
305                         new BufferedReader(new FileReader(fileUnderTest));
306                     ArrayList<String> tmp = new ArrayList<String>();
307                     while (bufferedReader.ready()) {
308                         tmp.add(bufferedReader.readLine());
309                     }
310                     lines = (String[])tmp.toArray(new String[tmp.size()]);
311                 } catch (IOException ex) {
312                     throw new JDOFatalException("getLines: caught IOException", ex);
313                 }
314             }
315             return lines;
316         }
317         
318         /** Return the error location for the file under test.
319          */
320         private String getErrorLocation(int lineNumber, int columnNumber) {
321             String[] lines = getLines();
322             int length = lines.length;
323             if (lineNumber > length) {
324                 return "Line number " + lineNumber +
325                     " exceeds the number of lines in the file (" +
326                     lines.length + ")";
327             } else if (lineNumber < 1) {
328                 return "Line number " + lineNumber +
329                     " does not allow retriving the error location.";
330             }
331             StringBuffer buf = new StringBuffer();
332             if (lineNumber > 2) {
333                 buf.append(lines[lineNumber-3]);
334                 buf.append(NL);
335                 buf.append(lines[lineNumber-2]);
336                 buf.append(NL);
337             }
338             buf.append(lines[lineNumber-1]);
339             buf.append(NL);
340             for (int i = 1; i < columnNumber; ++i) {
341                 buf.append(' ');
342             }
343             buf.append("^\n");
344             if (lineNumber + 1 < length) {
345                 buf.append(lines[lineNumber]);
346                 buf.append(NL);
347                 buf.append(lines[lineNumber+1]);
348                 buf.append(NL);
349             }
350             return buf.toString();
351         }
352     }
353 
354     /** Implementation of EntityResolver interface to check the jdo.dtd location
355      **/
356     private static class JDOEntityResolver 
357         implements EntityResolver {
358 
359         private static final String RECOGNIZED_JDO_PUBLIC_ID = 
360             "-//Sun Microsystems, Inc.//DTD Java Data Objects Metadata 2.2//EN";
361         private static final String RECOGNIZED_JDO_SYSTEM_ID = 
362             "file:/javax/jdo/jdo_2_2.dtd";
363         private static final String RECOGNIZED_JDO_SYSTEM_ID2 = 
364             "http://java.sun.com/dtd/jdo_2_2.dtd";
365         private static final String RECOGNIZED_ORM_PUBLIC_ID = 
366             "-//Sun Microsystems, Inc.//DTD Java Data Objects Mapping Metadata 2.2//EN";
367         private static final String RECOGNIZED_ORM_SYSTEM_ID = 
368             "file:/javax/jdo/orm_2_2.dtd";
369         private static final String RECOGNIZED_ORM_SYSTEM_ID2 = 
370             "http://java.sun.com/dtd/orm_2_2.dtd";
371         private static final String RECOGNIZED_JDOQUERY_PUBLIC_ID = 
372             "-//Sun Microsystems, Inc.//DTD Java Data Objects Query Metadata 2.2//EN";
373         private static final String RECOGNIZED_JDOQUERY_SYSTEM_ID = 
374             "file:/javax/jdo/jdoquery_2_2.dtd";
375         private static final String RECOGNIZED_JDOQUERY_SYSTEM_ID2 = 
376             "http://java.sun.com/dtd/jdoquery_2_2.dtd";
377         private static final String JDO_DTD_FILENAME = 
378             "javax/jdo/jdo_2_2.dtd";
379         private static final String ORM_DTD_FILENAME = 
380             "javax/jdo/orm_2_2.dtd";
381         private static final String JDOQUERY_DTD_FILENAME = 
382             "javax/jdo/jdoquery_2_2.dtd";
383 
384         static Map<String,String> publicIds = new HashMap<String,String>();
385         static Map<String,String> systemIds = new HashMap<String,String>();
386         static {
387             publicIds.put(RECOGNIZED_JDO_PUBLIC_ID, JDO_DTD_FILENAME);
388             publicIds.put(RECOGNIZED_ORM_PUBLIC_ID, ORM_DTD_FILENAME);
389             publicIds.put(RECOGNIZED_JDOQUERY_PUBLIC_ID, JDOQUERY_DTD_FILENAME);
390             systemIds.put(RECOGNIZED_JDO_SYSTEM_ID, JDO_DTD_FILENAME);
391             systemIds.put(RECOGNIZED_ORM_SYSTEM_ID, ORM_DTD_FILENAME);
392             systemIds.put(RECOGNIZED_JDOQUERY_SYSTEM_ID, JDOQUERY_DTD_FILENAME);
393             systemIds.put(RECOGNIZED_JDO_SYSTEM_ID2, JDO_DTD_FILENAME);
394             systemIds.put(RECOGNIZED_ORM_SYSTEM_ID2, ORM_DTD_FILENAME);
395             systemIds.put(RECOGNIZED_JDOQUERY_SYSTEM_ID2, JDOQUERY_DTD_FILENAME);
396         }
397         public InputSource resolveEntity(String publicId, final String systemId)
398             throws SAXException, IOException 
399         {
400             // check for recognized ids
401             String filename = (String)publicIds.get(publicId);
402             if (filename == null) {
403                 filename = (String)systemIds.get(systemId);
404             }
405             final String finalName = filename;
406             if (finalName == null) {
407                 return null;
408             } else {
409                 // Substitute the dtd with the one from javax.jdo.jdo.dtd,
410                 // but only if the publicId is equal to RECOGNIZED_PUBLIC_ID
411                 // or there is no publicID and the systemID is equal to
412                 // RECOGNIZED_SYSTEM_ID. 
413                     InputStream stream = AccessController.doPrivileged (
414                         new PrivilegedAction<InputStream> () {
415                             public InputStream run () {
416                             return getClass().getClassLoader().
417                                 getResourceAsStream(finalName);
418                             }
419                          }
420                      );
421                     if (stream == null) {
422                         throw new JDOFatalException("Cannot load " + finalName + 
423                             ", because the file does not exist in the jdo.jar file, " +
424                             "or the JDOParser class is not granted permission to read this file.  " +
425                             "The metadata .xml file contained PUBLIC=" + publicId +
426                             " SYSTEM=" + systemId + ".");
427                     }
428                 return new InputSource(new InputStreamReader(stream));
429             }
430         }
431     }
432 
433     /** Helper class to find all test JDO metadata files. */
434     public static class XMLFinder {
435 
436         private List<File> metadataFiles = new ArrayList<File>();
437         private final boolean recursive;
438         
439         /** Constructor. */
440         public XMLFinder(String[] fileNames, boolean recursive) {
441             this.recursive = recursive;
442             if (fileNames == null) return;
443             for (int i = 0; i < fileNames.length; i++) {
444                 appendTestFiles(fileNames[i]);
445             }
446         }
447         
448         /** Returns array of files of matching file names. */
449         private File[] getFiles(File dir, final String suffix) {
450             FilenameFilter filter = new FilenameFilter() {
451                     public boolean accept(File file, String name) {
452                         return name.endsWith(suffix);
453                     }
454                 };
455             return dir.listFiles(filter);
456         }
457 
458         /** */
459         private File[] getDirectories(File dir) {
460             FileFilter filter = new FileFilter() {
461                     public boolean accept(File pathname) {
462                         return pathname.isDirectory();
463                     }
464                 };
465             return dir.listFiles(filter);
466         }
467 
468         /** */
469         private void appendTestFiles(String fileName) {
470             File file = new File(fileName);
471             if (file.isDirectory()) {
472                 processDirectory(file);
473             } else if (fileName.endsWith(".jdo") || 
474                        fileName.endsWith(".orm") ||
475                        fileName.endsWith(".jdoquery")) {
476                 metadataFiles.add(new File(fileName));
477             }
478         }
479 
480         /** Adds all files with suffix .jdo, .orm and .jdoquery to the list of
481          * metadata files. Recursively process subdirectories if recursive
482          * flag is set. */
483         private void processDirectory(File dir) {
484             metadataFiles.addAll(Arrays.asList(getFiles(dir, ".jdo")));
485             metadataFiles.addAll(Arrays.asList(getFiles(dir, ".orm")));
486             metadataFiles.addAll(Arrays.asList(getFiles(dir, ".jdoquery")));
487             if (recursive) {
488                 File[] subdirs = getDirectories(dir);
489                 for (int i = 0; i < subdirs.length; i++) {
490                     processDirectory(subdirs[i]);
491                 }
492             }
493         }
494 
495         /** Returns an array of test files with suffix .jdo, .orm or .jdoquery. */
496         public File[] getMetadataFiles() {
497             return (File[])metadataFiles.toArray(new File[metadataFiles.size()]);
498         }
499 
500     }
501 
502     /** */
503     private static String[] checkMetadataSystemProperty() {
504         String[] ret = null;
505         String metadata = System.getProperty(METADATA_PROP);
506         if ((metadata != null) && (metadata.length() > 0)) {
507             List<String> entries = new ArrayList<String>();
508             StringTokenizer st = new StringTokenizer(metadata, DELIM);
509             while (st.hasMoreTokens()) {
510                 entries.add(st.nextToken());
511             }
512             ret = (String[])entries.toArray(new String[entries.size()]);
513         }
514         return ret;
515     }
516 
517     /**
518      * Command line tool to test JDO metadata files. 
519      * Usage: XMLTestUtil [-r] <file or directory>+
520      */
521     public static void main(String args[]) {
522         String[] fromProp = checkMetadataSystemProperty();
523         boolean recursive = Boolean.getBoolean(RECURSIVE_PROP);
524 
525         // handle command line args
526         String[] fileNames = null;
527         if ((args.length > 0) && ("-r".equals(args[0]))) {
528             recursive = true;
529             fileNames = new String[args.length - 1];
530             System.arraycopy(args, 1, fileNames, 0, args.length - 1);
531         } else {
532             fileNames = args;
533         }
534         
535         // check args
536         if ((fileNames.length == 0) && (fromProp == null)) {
537             System.err.println(
538                 "No commandline arguments and system property metadata not defined; " + 
539                 "nothing to be tested.\nUsage: XMLTestUtil [-r] <directories>\n" + 
540                 "\tAll .jdo, .orm, and .jdoquery files in the directory (recursively) will be tested.");
541         } else if ((fileNames.length == 0) && (fromProp != null)) {
542             // use metadata system property
543             fileNames = fromProp;
544         } else if ((fileNames.length != 0) && (fromProp != null)) {
545             System.err.println(
546                 "Commandline arguments specified and system property metadata defined; " +
547                 "ignoring system property metadata.");
548         }
549 
550         // run the test
551         XMLTestUtil xmlTest = new XMLTestUtil();
552         File[] files = new XMLFinder(fileNames, recursive).getMetadataFiles();
553         for (int i = 0; i < files.length; i++) {
554             File file = files[i];
555             System.out.print("Checking " + file.getPath() + ": ");
556             String messages = xmlTest.checkXML(file, true);
557             messages = (messages == null) ?  "OK" : NL + messages;
558             System.out.println(messages);
559         }
560     }
561 }
562