    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