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.schema;
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.FilenameFilter;
26  
27  import java.security.AccessController;
28  import java.security.PrivilegedAction;
29  import java.util.ArrayList;
30  
31  import javax.jdo.JDOFatalException;
32  import javax.jdo.util.AbstractTest;
33  import javax.jdo.util.BatchTestRunner;
34  
35  import javax.xml.parsers.*;
36  import org.w3c.dom.Document;
37  import org.xml.sax.*;
38  import org.xml.sax.helpers.*;
39  
40  /***
41   * Tests schema files.
42   * <p>
43   */
44  public class XMLTest extends AbstractTest {
45  
46      /*** */
47      protected static String BASEDIR = System.getProperty("basedir", ".");
48  
49      /*** "http://www.w3.org/2001/XMLSchema" target="alexandria_uri">http://www.w3.org/2001/XMLSchema" */
50      protected static final String XSD_TYPE = 
51          "http://www.w3.org/2001/XMLSchema";
52  
53      /*** */
54      protected static final String SCHEMA_LANGUAGE_PROP = 
55          "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
56  
57      /*** */
58      protected static final String SCHEMA_LOCATION_PROP =
59          "http://apache.org/xml/properties/schema/external-noNamespaceSchemaLocation";
60      
61      /*** */
62      protected static final File JDO_XSD_FILE = 
63          new File(BASEDIR + "/target/classes/javax/jdo/jdo.xsd");
64  
65      /*** */
66      protected static final File ORM_XSD_FILE = 
67          new File(BASEDIR + "/target/classes/javax/jdo/orm.xsd");
68  
69      /*** */
70      protected static final File JDOQL_XSD_FILE = 
71          new File(BASEDIR + "/target/classes/javax/jdo/jdoquery.xsd");
72  
73      /*** File prefix */
74      protected static final String FILE_PREFIX = BASEDIR + "/test/schema/";
75  
76      /*** Entity resolver */
77      protected static final EntityResolver resolver = new JDOEntityResolver();
78  
79      /*** Error handler */
80      protected static final Handler handler = new Handler();
81  
82      /*** .xsd files */
83      protected static final File[] XSD_FILES = {
84          JDO_XSD_FILE, ORM_XSD_FILE, JDOQL_XSD_FILE
85      };
86      
87      /*** XSD metadata files. */
88      protected static File[] positiveXSDJDO = getFiles("Positive", "-xsd.jdo");
89      protected static File[] negativeXSDJDO = getFiles("Negative", "-xsd.jdo");
90      protected static File[] positiveXSDORM = getFiles("Positive", "-xsd.orm");
91      protected static File[] negativeXSDORM = getFiles("Negative", "-xsd.orm");
92      protected static File[] positiveXSDJDOQL = getFiles("Positive", "-xsd.jdoquery");
93      protected static File[] negativeXSDJDOQL = getFiles("Negative", "-xsd.jdoquery");
94  
95      /*** DTD metadata files. */
96      protected static File[] positiveDTDJDO = getFiles("Positive", "-dtd.jdo");
97      protected static File[] negativeDTDJDO = getFiles("Negative", "-dtd.jdo");
98      protected static File[] positiveDTDORM = getFiles("Positive", "-dtd.orm");
99      protected static File[] negativeDTDORM = getFiles("Negative", "-dtd.orm");
100     protected static File[] positiveDTDJDOQL = getFiles("Positive", "-dtd.jdoquery");
101     protected static File[] negativeDTDJDOQL = getFiles("Negative", "-dtd.jdoquery");
102 
103     /*** Returns array of files of matching file names. */
104     protected static File[] getFiles(final String prefix, final String suffix) {
105         FilenameFilter filter = new FilenameFilter () {
106             public boolean accept(File file, String name) {
107                 return (name.startsWith(prefix) && name.endsWith(suffix));
108             }
109         };
110         File dir = new File(FILE_PREFIX);
111         return dir.listFiles(filter);
112     }
113 
114     /*** */
115     public static void main(String args[]) {
116         BatchTestRunner.run(XMLTest.class);
117     }
118 
119     /*** Test XSD files jdo.xsd, orm.xsd, and jdoquery.xsd. */
120     public void testXSD() throws SAXException, IOException {
121         DocumentBuilder builder = null;
122         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
123         factory.setNamespaceAware(true);
124         builder = getParser(factory);
125         checkXML(builder, XSD_FILES, true);
126         String messages = retrieveMessages();
127         if (messages != null) {
128             fail(messages);
129         }        
130     }
131 
132     /*** Test XSD based .jdo, .orm and .jdoquery files. */
133     public void testXSDBased() {
134         // create XSD parser
135         DocumentBuilder builder = null;
136         builder = createBuilder(JDO_XSD_FILE.toURI().toString());
137         checkXML(builder, positiveXSDJDO, true);
138         checkXML(builder, negativeXSDJDO, false);
139         builder = createBuilder(ORM_XSD_FILE.toURI().toString());
140         checkXML(builder, positiveXSDORM, true);
141         checkXML(builder, negativeXSDORM, false);
142         builder = createBuilder(JDOQL_XSD_FILE.toURI().toString());
143         checkXML(builder, positiveXSDJDOQL, true);
144         checkXML(builder, negativeXSDJDOQL, false);
145         String messages = retrieveMessages();
146         if (messages != null) {
147             fail(messages);
148         }        
149     }
150 
151     /*** Test DTD based .jdo, .orm and .jdoquery files. */
152     public void testDTDBased() {
153         // create DTD parser 
154         DocumentBuilder builder = createBuilder();
155         checkXML(builder, positiveDTDJDO, true);
156         checkXML(builder, negativeDTDJDO, false);
157         checkXML(builder, positiveDTDORM, true);
158         checkXML(builder, negativeDTDORM, false);
159         checkXML(builder, positiveDTDJDOQL, true);
160         checkXML(builder, negativeDTDJDOQL, false);
161         String messages = retrieveMessages();
162         if (messages != null) {
163             fail(messages);
164         }        
165     }
166 
167     /*** Create XSD builder.
168      */
169     private DocumentBuilder createBuilder(String location) {
170         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
171         factory.setValidating(true);
172         factory.setNamespaceAware(true);
173         factory.setAttribute(SCHEMA_LANGUAGE_PROP, XSD_TYPE);
174         factory.setAttribute(SCHEMA_LOCATION_PROP, location);
175         return getParser(factory);
176     }
177 
178     /*** Create DTD builder.
179      */
180     private DocumentBuilder createBuilder() {
181         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
182         factory.setValidating(true);
183         factory.setNamespaceAware(true);
184         return getParser(factory);
185     }
186 
187     /*** Returns a parser obtained from specified factroy. */
188     private DocumentBuilder getParser(DocumentBuilderFactory factory) {
189         try {
190             DocumentBuilder builder = factory.newDocumentBuilder();
191             builder.setEntityResolver(resolver);
192             builder.setErrorHandler(handler);
193             return builder;
194         } catch (ParserConfigurationException ex) {
195             throw new JDOFatalException("Cannot create XML parser", ex);
196         }
197     }
198 
199     /*** Parse the specified files using the specified builder. The valid
200      * parameter determines whether the specified files are valid JDO metadata
201      * files. The method does not throw an exception on an error, instead it
202      * appends any error message to the global message handler.
203      */
204     private void checkXML(DocumentBuilder builder, File[] files, boolean valid) {
205         for (int i = 0; i < files.length; i++) {
206             File file = files[i];
207             handler.init(file);
208             try {
209                 builder.parse(file);
210             } catch (SAXParseException ex) {
211                 handler.error(ex);
212             } catch (Exception ex) {
213                 throw new JDOFatalException("Fatal error", ex);
214             }
215             String messages = handler.getMessages();
216             if (valid && (messages != null)) {
217                 appendMessage(messages);
218             } else if (!valid && (messages == null)) {
219                 appendMessage(file.getName() + " is not valid, " +
220                               "but the parser did not catch the error.");
221             }
222         }
223     }
224 
225     /*** ErrorHandler implementation. */
226     private static class Handler implements ErrorHandler {
227 
228         private File fileUnderTest;
229         private String[] lines;
230         private StringBuffer messages;
231 
232         public void error(SAXParseException ex) {
233             append("Handler.error: ", ex);
234         }
235             
236         public void fatalError(SAXParseException ex) {
237             append("Handler.fatalError: ", ex);
238         }
239         
240         public void warning(SAXParseException ex) {
241             append("Handler.warning: ", ex);
242         }
243         
244         public void init(File file) {
245             this.fileUnderTest = file;
246             this.messages = new StringBuffer();
247             this.lines = null;
248         }
249 
250         public String getMessages() {
251             return (messages.length() == 0) ? null : messages.toString();
252         }
253 
254         private void append(String prefix, SAXParseException ex) {
255             int lineNumber = ex.getLineNumber();
256             int columnNumber = ex.getColumnNumber();
257             messages.append("------------------------").append(NL);
258             messages.append(prefix).append(fileUnderTest.getName());
259             messages.append(" [line=").append(lineNumber);
260             messages.append(", col=").append(columnNumber).append("]: ");
261             messages.append(ex.getMessage()).append(NL);
262             messages.append(getErrorLocation(lineNumber, columnNumber));
263         }
264 
265         private String[] getLines() {
266             if (lines == null) {
267                 try {
268                     BufferedReader bufferedReader =
269                         new BufferedReader(new FileReader(fileUnderTest));
270                     ArrayList tmp = new ArrayList();
271                     while (bufferedReader.ready()) {
272                         tmp.add(bufferedReader.readLine());
273                     }
274                     lines = (String[])tmp.toArray(new String[tmp.size()]);
275                 } catch (IOException ex) {
276                     throw new JDOFatalException("getLines: caught IOException", ex);
277                 }
278             }
279             return lines;
280         }
281         
282         /*** Return the error location for the file under test.
283          */
284         private String getErrorLocation(int lineNumber, int columnNumber) {
285             String[] lines = getLines();
286             int length = lines.length;
287             if (lineNumber > length) {
288                 return "Line number " + lineNumber +
289                     " exceeds the number of lines in the file (" +
290                     lines.length + ")";
291             } else if (lineNumber < 1) {
292                 return "Line number " + lineNumber +
293                     " does not allow retriving the error location.";
294             }
295             StringBuffer buf = new StringBuffer();
296             if (lineNumber > 2) {
297                 buf.append(lines[lineNumber-3]);
298                 buf.append(NL);
299                 buf.append(lines[lineNumber-2]);
300                 buf.append(NL);
301             }
302             buf.append(lines[lineNumber-1]);
303             buf.append(NL);
304             for (int i = 1; i < columnNumber; ++i) {
305                 buf.append(' ');
306             }
307             buf.append("^\n");
308             if (lineNumber + 1 < length) {
309                 buf.append(lines[lineNumber]);
310                 buf.append(NL);
311                 buf.append(lines[lineNumber+1]);
312                 buf.append(NL);
313             }
314             return buf.toString();
315         }
316     }
317 
318     /*** Implementation of EntityResolver interface to check the jdo.dtd location
319      **/
320     private static class JDOEntityResolver 
321         implements EntityResolver {
322 
323         private static final String RECOGNIZED_PUBLIC_ID = 
324             "-//Sun Microsystems, Inc.//DTD Java Data Objects Metadata 2.0//EN";
325         private static final String RECOGNIZED_SYSTEM_ID = 
326             "file:/javax/jdo/jdo.dtd";
327 
328         public InputSource resolveEntity(String publicId, final String systemId)
329             throws SAXException, IOException 
330         {
331             // check for recognized ids
332             if (((publicId != null) && RECOGNIZED_PUBLIC_ID.equals(publicId)) ||
333                 ((publicId == null) && (systemId != null) && 
334                  RECOGNIZED_SYSTEM_ID.equals(systemId))) {
335                 // Substitute the dtd with the one from javax.jdo.jdo.dtd,
336                 // but only if the publicId is equal to RECOGNIZED_PUBLIC_ID
337                 // or there is no publicID and the systemID is equal to
338                 // RECOGNIZED_SYSTEM_ID. 
339                     InputStream stream = (InputStream) AccessController.doPrivileged (
340                         new PrivilegedAction () {
341                             public Object run () {
342                             return getClass().getClassLoader().
343                                 getResourceAsStream("javax/jdo/jdo.dtd");
344                             }
345                          }
346                      );
347                     if (stream == null) {
348                         // TDB: error handling + I18N
349                         throw new JDOFatalException("Cannot load javax/jdo/jdo.dtd, " +
350                             "because the file does not exist in the jdo.jar file, " +
351                             "or the JDOParser class is not granted permission to read this file.  " +
352                             "The metadata .xml file contained PUBLIC=" + publicId +
353                             " SYSTEM=" + systemId + ".");
354                     }
355                 return new InputSource(new InputStreamReader(stream));
356             }
357             return null;
358         }
359     }
360 }
361