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