| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one or more | |
| * contributor license agreements. See the NOTICE file distributed with | |
| * this work for additional information regarding copyright ownership. | |
| * The ASF licenses this file to You under the Apache License, Version 2.0 | |
| * (the "License"); you may not use this file except in compliance with | |
| * the License. You may obtain a copy of the License at | |
| * | |
| * https://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| package javax.jdo.util; | |
| import java.io.BufferedReader; | |
| import java.io.File; | |
| import java.io.FileFilter; | |
| import java.io.FileReader; | |
| import java.io.FilenameFilter; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.InputStreamReader; | |
| import java.lang.reflect.InvocationTargetException; | |
| import java.security.PrivilegedAction; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.StringTokenizer; | |
| import javax.jdo.JDOFatalException; | |
| import javax.jdo.JDOFatalInternalException; | |
| import javax.jdo.LegacyJava; | |
| import javax.xml.parsers.DocumentBuilder; | |
| import javax.xml.parsers.DocumentBuilderFactory; | |
| import javax.xml.parsers.ParserConfigurationException; | |
| import org.w3c.dom.Document; | |
| import org.xml.sax.EntityResolver; | |
| import org.xml.sax.ErrorHandler; | |
| import org.xml.sax.InputSource; | |
| import org.xml.sax.SAXException; | |
| import org.xml.sax.SAXParseException; | |
| /** | |
| * Tests schema files. | |
| * | |
| * <p> | |
| */ | |
| public class XMLTestUtil { | |
| /** */ | |
| protected static final String BASEDIR = System.getProperty("basedir", "."); | |
| /** "http://www.w3.org/2001/XMLSchema" */ | |
| protected static final String XSD_TYPE = "http://www.w3.org/2001/XMLSchema"; | |
| /** */ | |
| protected static final String SCHEMA_LANGUAGE_PROP = | |
| "http://java.sun.com/xml/jaxp/properties/schemaLanguage"; | |
| /** */ | |
| protected static final String SCHEMA_LOCATION_PROP = | |
| "http://apache.org/xml/properties/schema/external-schemaLocation"; | |
| /** jdo namespace */ | |
| protected static final String JDO_XSD_NS = "https://db.apache.org/jdo/xmlns/jdo"; | |
| /** orm namespace */ | |
| protected static final String ORM_XSD_NS = "https://db.apache.org/jdo/xmlns/orm"; | |
| /** jdoquery namespace */ | |
| protected static final String JDOQUERY_XSD_NS = "https://db.apache.org/jdo/xmlns/jdoquery"; | |
| /** jdo xsd file */ | |
| protected static final File JDO_XSD_FILE = | |
| new File(BASEDIR + "/target/classes/javax/jdo/jdo_3_2.xsd"); | |
| /** orm xsd file */ | |
| protected static final File ORM_XSD_FILE = | |
| new File(BASEDIR + "/target/classes/javax/jdo/orm_3_2.xsd"); | |
| /** jdoquery xsd file */ | |
| protected static final File JDOQUERY_XSD_FILE = | |
| new File(BASEDIR + "/target/classes/javax/jdo/jdoquery_3_2.xsd"); | |
| /** Entity resolver */ | |
| protected static final EntityResolver resolver = new JDOEntityResolver(); | |
| /** Error handler */ | |
| protected static final Handler handler = new Handler(); | |
| /** | |
| * Name of the metadata property, a comma separated list of JDO metadata file or directories | |
| * containing such files. | |
| */ | |
| protected static final String METADATA_PROP = "javax.jdo.metadata"; | |
| /** Name of the recursive property, allowing recursive search of metadata files. */ | |
| protected static final String RECURSIVE_PROP = "javax.jdo.recursive"; | |
| /** Separator character for the metadata property. */ | |
| protected static final String DELIM = ",;"; | |
| /** Newline. */ | |
| protected static final String NL = System.getProperty("line.separator"); | |
| /** XSD builder for jdo namespace. */ | |
| private final DocumentBuilder jdoXsdBuilder = | |
| createBuilder(JDO_XSD_NS + " " + JDO_XSD_FILE.toURI().toString()); | |
| /** XSD builder for orm namespace. */ | |
| private final DocumentBuilder ormXsdBuilder = | |
| createBuilder(ORM_XSD_NS + " " + ORM_XSD_FILE.toURI().toString()); | |
| /** XSD builder for jdoquery namespace. */ | |
| private final DocumentBuilder jdoqueryXsdBuilder = | |
| createBuilder(JDOQUERY_XSD_NS + " " + JDOQUERY_XSD_FILE.toURI().toString()); | |
| /** DTD builder. */ | |
| private final DocumentBuilder dtdBuilder = createBuilder(true); | |
| /** Non validating builder. */ | |
| private final DocumentBuilder nonValidatingBuilder = createBuilder(false); | |
| /** Create XSD builder. */ | |
| private DocumentBuilder createBuilder(String location) { | |
| DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); | |
| factory.setValidating(true); | |
| factory.setNamespaceAware(true); | |
| factory.setAttribute(SCHEMA_LANGUAGE_PROP, XSD_TYPE); | |
| factory.setAttribute(SCHEMA_LOCATION_PROP, location); | |
| return getParser(factory); | |
| } | |
| /** Create builder. */ | |
| private DocumentBuilder createBuilder(boolean validating) { | |
| DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); | |
| factory.setValidating(validating); | |
| factory.setNamespaceAware(true); | |
| return getParser(factory); | |
| } | |
| /** Returns a parser obtained from specified factroy. */ | |
| private DocumentBuilder getParser(DocumentBuilderFactory factory) { | |
| try { | |
| DocumentBuilder builder = factory.newDocumentBuilder(); | |
| builder.setEntityResolver(resolver); | |
| builder.setErrorHandler(handler); | |
| return builder; | |
| } catch (ParserConfigurationException ex) { | |
| throw new JDOFatalException("Cannot create XML parser", ex); | |
| } | |
| } | |
| /** | |
| * Parse the specified files. The valid parameter determines whether the specified files are valid | |
| * JDO metadata files. The method does not throw an exception on an error, instead it instead it | |
| * returns the error message(s) as string. | |
| */ | |
| public String checkXML(File[] files, boolean valid) { | |
| StringBuffer messages = new StringBuffer(); | |
| for (int i = 0; i < files.length; i++) { | |
| String msg = checkXML(files[i], valid); | |
| if (msg != null) { | |
| messages.append(msg); | |
| } | |
| } | |
| return (messages.length() == 0) ? null : messages.toString(); | |
| } | |
| /** | |
| * Parse the specified files using a non validating parser. The method does not throw an exception | |
| * on an error, instead it instead it returns the error message(s) as string. | |
| */ | |
| public String checkXMLNonValidating(File[] files) { | |
| StringBuffer messages = new StringBuffer(); | |
| for (int i = 0; i < files.length; i++) { | |
| String msg = checkXML(nonValidatingBuilder, files[i], true); | |
| if (msg != null) { | |
| messages.append(msg); | |
| } | |
| } | |
| return (messages.length() == 0) ? null : messages.toString(); | |
| } | |
| /** | |
| * Parse the specified file. The method checks whether it is a XSD or DTD base file and parses the | |
| * file using a builder according to the file name suffix. The valid parameter determines whether | |
| * the specified files are valid JDO metadata files. The method does not throw an exception on an | |
| * error, instead it returns the error message(s) as string. | |
| */ | |
| private String checkXML(File file, boolean valid) { | |
| String messages = null; | |
| String fileName = file.getName(); | |
| try { | |
| if (isDTDBased(file)) { | |
| messages = checkXML(dtdBuilder, file, valid); | |
| } else if (fileName.endsWith(".jdo")) { | |
| messages = checkXML(jdoXsdBuilder, file, valid); | |
| } else if (fileName.endsWith(".orm")) { | |
| messages = checkXML(ormXsdBuilder, file, valid); | |
| } else if (fileName.endsWith(".jdoquery")) { | |
| messages = checkXML(jdoqueryXsdBuilder, file, valid); | |
| } | |
| } catch (SAXException ex) { | |
| messages = ex.getMessage(); | |
| } | |
| return messages; | |
| } | |
| /** | |
| * Parse the specified file using the specified builder. The valid parameter determines whether | |
| * the specified files are valid JDO metadata files. The method does not throw an exception on an | |
| * error, instead it returns the error message(s) as string. | |
| */ | |
| private String checkXML(DocumentBuilder builder, File file, boolean valid) { | |
| String messages = null; | |
| handler.init(file); | |
| try { | |
| builder.parse(file); | |
| } catch (SAXParseException ex) { | |
| handler.error(ex); | |
| } catch (Exception ex) { | |
| messages = "Fatal error processing " + file.getName() + ": " + ex + NL; | |
| } | |
| if (messages == null) { | |
| messages = handler.getMessages(); | |
| } | |
| if (!valid) { | |
| if (messages != null) { | |
| // expected error for negative test | |
| messages = null; | |
| } else { | |
| messages = file.getName() + " is not valid, " + "but the parser did not catch the error."; | |
| } | |
| } | |
| return messages; | |
| } | |
| /** | |
| * Checks whether the specifeid file is DTD or XSD based. The method throws a SAXException if the | |
| * file has syntax errors. | |
| */ | |
| private boolean isDTDBased(File file) throws SAXException { | |
| handler.init(file); | |
| try { | |
| Document document = nonValidatingBuilder.parse(file); | |
| return document.getDoctype() != null; | |
| } catch (SAXParseException ex) { | |
| handler.error(ex); | |
| throw new SAXException(handler.getMessages()); | |
| } catch (Exception ex) { | |
| throw new SAXException("Fatal error processing " + file.getName() + ": " + ex); | |
| } | |
| } | |
| /** ErrorHandler implementation. */ | |
| private static class Handler implements ErrorHandler { | |
| private File fileUnderTest; | |
| private String[] lines; | |
| private StringBuffer messages; | |
| public void error(SAXParseException ex) { | |
| append("Handler.error: ", ex); | |
| } | |
| public void fatalError(SAXParseException ex) { | |
| append("Handler.fatalError: ", ex); | |
| } | |
| public void warning(SAXParseException ex) { | |
| append("Handler.warning: ", ex); | |
| } | |
| public void init(File file) { | |
| this.fileUnderTest = file; | |
| this.messages = new StringBuffer(); | |
| this.lines = null; | |
| } | |
| public String getMessages() { | |
| return (messages.length() == 0) ? null : messages.toString(); | |
| } | |
| private void append(String prefix, SAXParseException ex) { | |
| int lineNumber = ex.getLineNumber(); | |
| int columnNumber = ex.getColumnNumber(); | |
| messages.append("------------------------").append(NL); | |
| messages.append(prefix).append(fileUnderTest.getName()); | |
| messages.append(" [line=").append(lineNumber); | |
| messages.append(", col=").append(columnNumber).append("]: "); | |
| messages.append(ex.getMessage()).append(NL); | |
| messages.append(getErrorLocation(lineNumber, columnNumber)); | |
| } | |
| private String[] getLines() { | |
| if (lines == null) { | |
| try { | |
| BufferedReader bufferedReader = new BufferedReader(new FileReader(fileUnderTest)); | |
| ArrayList<String> tmp = new ArrayList<>(); | |
| while (bufferedReader.ready()) { | |
| tmp.add(bufferedReader.readLine()); | |
| } | |
| lines = tmp.toArray(new String[tmp.size()]); | |
| } catch (IOException ex) { | |
| throw new JDOFatalException("getLines: caught IOException", ex); | |
| } | |
| } | |
| return lines; | |
| } | |
| /** Return the error location for the file under test. */ | |
| private String getErrorLocation(int lineNumber, int columnNumber) { | |
| String[] linesArray = getLines(); | |
| int length = linesArray.length; | |
| if (lineNumber > length) { | |
| return "Line number " | |
| + lineNumber | |
| + " exceeds the number of linesArray in the file (" | |
| + linesArray.length | |
| + ")"; | |
| } else if (lineNumber < 1) { | |
| return "Line number " + lineNumber + " does not allow retriving the error location."; | |
| } | |
| StringBuffer buf = new StringBuffer(); | |
| if (lineNumber > 2) { | |
| buf.append(linesArray[lineNumber - 3]); | |
| buf.append(NL); | |
| buf.append(linesArray[lineNumber - 2]); | |
| buf.append(NL); | |
| } | |
| buf.append(linesArray[lineNumber - 1]); | |
| buf.append(NL); | |
| for (int i = 1; i < columnNumber; ++i) { | |
| buf.append(' '); | |
| } | |
| buf.append("^\n"); | |
| if (lineNumber + 1 < length) { | |
| buf.append(linesArray[lineNumber]); | |
| buf.append(NL); | |
| buf.append(linesArray[lineNumber + 1]); | |
| buf.append(NL); | |
| } | |
| return buf.toString(); | |
| } | |
| } | |
| /** | |
| * Implementation of EntityResolver interface to check the jdo.dtd location. TODO Somebody should | |
| * update these 2.2 to 3.1 or later at some point. | |
| */ | |
| private static class JDOEntityResolver implements EntityResolver { | |
| private static final String RECOGNIZED_JDO_PUBLIC_ID = | |
| "-//Sun Microsystems, Inc.//DTD Java Data Objects Metadata 3.2//EN"; | |
| private static final String RECOGNIZED_JDO_SYSTEM_ID = "file:/javax/jdo/jdo_3_2.dtd"; | |
| private static final String RECOGNIZED_JDO_SYSTEM_ID2 = "http://xmlns.jcp.org/dtd/jdo_3_2.dtd"; | |
| private static final String RECOGNIZED_ORM_PUBLIC_ID = | |
| "-//Sun Microsystems, Inc.//DTD Java Data Objects Mapping Metadata 3.2//EN"; | |
| private static final String RECOGNIZED_ORM_SYSTEM_ID = "file:/javax/jdo/orm_3_2.dtd"; | |
| private static final String RECOGNIZED_ORM_SYSTEM_ID2 = "http://xmlns.jcp.org/dtd/orm_3_2.dtd"; | |
| private static final String RECOGNIZED_JDOQUERY_PUBLIC_ID = | |
| "-//Sun Microsystems, Inc.//DTD Java Data Objects Query Metadata 3.2//EN"; | |
| private static final String RECOGNIZED_JDOQUERY_SYSTEM_ID = "file:/javax/jdo/jdoquery_3_2.dtd"; | |
| private static final String RECOGNIZED_JDOQUERY_SYSTEM_ID2 = | |
| "http://xmlns.jcp.org/dtd/jdoquery_3_2.dtd"; | |
| private static final String JDO_DTD_FILENAME = "javax/jdo/jdo_3_2.dtd"; | |
| private static final String ORM_DTD_FILENAME = "javax/jdo/orm_3_2.dtd"; | |
| private static final String JDOQUERY_DTD_FILENAME = "javax/jdo/jdoquery_3_2.dtd"; | |
| static final Map<String, String> publicIds = new HashMap<>(); | |
| static final Map<String, String> systemIds = new HashMap<>(); | |
| static { | |
| publicIds.put(RECOGNIZED_JDO_PUBLIC_ID, JDO_DTD_FILENAME); | |
| publicIds.put(RECOGNIZED_ORM_PUBLIC_ID, ORM_DTD_FILENAME); | |
| publicIds.put(RECOGNIZED_JDOQUERY_PUBLIC_ID, JDOQUERY_DTD_FILENAME); | |
| systemIds.put(RECOGNIZED_JDO_SYSTEM_ID, JDO_DTD_FILENAME); | |
| systemIds.put(RECOGNIZED_ORM_SYSTEM_ID, ORM_DTD_FILENAME); | |
| systemIds.put(RECOGNIZED_JDOQUERY_SYSTEM_ID, JDOQUERY_DTD_FILENAME); | |
| systemIds.put(RECOGNIZED_JDO_SYSTEM_ID2, JDO_DTD_FILENAME); | |
| systemIds.put(RECOGNIZED_ORM_SYSTEM_ID2, ORM_DTD_FILENAME); | |
| systemIds.put(RECOGNIZED_JDOQUERY_SYSTEM_ID2, JDOQUERY_DTD_FILENAME); | |
| } | |
| public InputSource resolveEntity(String publicId, final String systemId) | |
| throws SAXException, IOException { | |
| // check for recognized ids | |
| String filename = publicIds.get(publicId); | |
| if (filename == null) { | |
| filename = systemIds.get(systemId); | |
| } | |
| final String finalName = filename; | |
| if (finalName == null) { | |
| return null; | |
| } else { | |
| // Substitute the dtd with the one from javax.jdo.jdo.dtd, | |
| // but only if the publicId is equal to RECOGNIZED_PUBLIC_ID | |
| // or there is no publicID and the systemID is equal to | |
| // RECOGNIZED_SYSTEM_ID. | |
| InputStream stream = | |
| doPrivileged( | |
| new PrivilegedAction<InputStream>() { | |
| public InputStream run() { | |
| return getClass().getClassLoader().getResourceAsStream(finalName); | |
| } | |
| }); | |
| if (stream == null) { | |
| throw new JDOFatalException( | |
| "Cannot load " | |
| + finalName | |
| + ", because the file does not exist in the jdo.jar file, " | |
| + "or the JDOParser class is not granted permission to read this file. " | |
| + "The metadata .xml file contained PUBLIC=" | |
| + publicId | |
| + " SYSTEM=" | |
| + systemId | |
| + "."); | |
| } | |
| return new InputSource(new InputStreamReader(stream)); | |
| } | |
| } | |
| } | |
| /** Helper class to find all test JDO metadata files. */ | |
| public static class XMLFinder { | |
| private final List<File> metadataFiles = new ArrayList<>(); | |
| private final boolean recursive; | |
| /** Constructor. */ | |
| public XMLFinder(String[] fileNames, boolean recursive) { | |
| this.recursive = recursive; | |
| if (fileNames == null) return; | |
| for (int i = 0; i < fileNames.length; i++) { | |
| appendTestFiles(fileNames[i]); | |
| } | |
| } | |
| /** Returns array of files of matching file names. */ | |
| private File[] getFiles(File dir, final String suffix) { | |
| FilenameFilter filter = (file, name) -> name.endsWith(suffix); | |
| return dir.listFiles(filter); | |
| } | |
| /** */ | |
| private File[] getDirectories(File dir) { | |
| FileFilter filter = File::isDirectory; | |
| return dir.listFiles(filter); | |
| } | |
| /** */ | |
| private void appendTestFiles(String fileName) { | |
| File file = new File(fileName); | |
| if (file.isDirectory()) { | |
| processDirectory(file); | |
| } else if (fileName.endsWith(".jdo") | |
| || fileName.endsWith(".orm") | |
| || fileName.endsWith(".jdoquery")) { | |
| metadataFiles.add(new File(fileName)); | |
| } | |
| } | |
| /** | |
| * Adds all files with suffix .jdo, .orm and .jdoquery to the list of metadata files. | |
| * Recursively process subdirectories if recursive flag is set. | |
| */ | |
| private void processDirectory(File dir) { | |
| metadataFiles.addAll(Arrays.asList(getFiles(dir, ".jdo"))); | |
| metadataFiles.addAll(Arrays.asList(getFiles(dir, ".orm"))); | |
| metadataFiles.addAll(Arrays.asList(getFiles(dir, ".jdoquery"))); | |
| if (recursive) { | |
| File[] subdirs = getDirectories(dir); | |
| for (int i = 0; i < subdirs.length; i++) { | |
| processDirectory(subdirs[i]); | |
| } | |
| } | |
| } | |
| /** Returns an array of test files with suffix .jdo, .orm or .jdoquery. */ | |
| public File[] getMetadataFiles() { | |
| return metadataFiles.toArray(new File[metadataFiles.size()]); | |
| } | |
| } | |
| /** */ | |
| private static String[] checkMetadataSystemProperty() { | |
| String[] ret = null; | |
| String metadata = System.getProperty(METADATA_PROP); | |
| if ((metadata != null) && (!metadata.isEmpty())) { | |
| List<String> entries = new ArrayList<>(); | |
| StringTokenizer st = new StringTokenizer(metadata, DELIM); | |
| while (st.hasMoreTokens()) { | |
| entries.add(st.nextToken()); | |
| } | |
| ret = entries.toArray(new String[entries.size()]); | |
| } | |
| return ret; | |
| } | |
| /** Command line tool to test JDO metadata files. Usage: XMLTestUtil [-r] <file or directory>+ */ | |
| @SuppressWarnings("java:S106") // Standard outputs should not be used directly to log anything | |
| public static void main(String[] args) { | |
| String[] fromProp = checkMetadataSystemProperty(); | |
| boolean recursive = Boolean.getBoolean(RECURSIVE_PROP); | |
| // handle command line args | |
| String[] fileNames = null; | |
| if ((args.length > 0) && ("-r".equals(args[0]))) { | |
| recursive = true; | |
| fileNames = new String[args.length - 1]; | |
| System.arraycopy(args, 1, fileNames, 0, args.length - 1); | |
| } else { | |
| fileNames = args; | |
| } | |
| // check args | |
| if ((fileNames.length == 0) && (fromProp == null)) { | |
| System.err.println( | |
| "No commandline arguments and system property metadata not defined; " | |
| + "nothing to be tested.\nUsage: XMLTestUtil [-r] <directories>\n" | |
| + "\tAll .jdo, .orm, and .jdoquery files in the directory (recursively) will be tested."); | |
| } else if ((fileNames.length == 0) && (fromProp != null)) { | |
| // use metadata system property | |
| fileNames = fromProp; | |
| } else if ((fileNames.length != 0) && (fromProp != null)) { | |
| System.err.println( | |
| "Commandline arguments specified and system property metadata defined; " | |
| + "ignoring system property metadata."); | |
| } | |
| // run the test | |
| XMLTestUtil xmlTest = new XMLTestUtil(); | |
| File[] files = new XMLFinder(fileNames, recursive).getMetadataFiles(); | |
| for (int i = 0; i < files.length; i++) { | |
| File file = files[i]; | |
| System.out.print("Checking " + file.getPath() + ": "); | |
| String messages = xmlTest.checkXML(file, true); | |
| messages = (messages == null) ? "OK" : NL + messages; | |
| System.out.println(messages); | |
| } | |
| } | |
| @SuppressWarnings("unchecked") | |
| private static <T> T doPrivileged(PrivilegedAction<T> privilegedAction) { | |
| try { | |
| return (T) LegacyJava.doPrivilegedAction.invoke(null, privilegedAction); | |
| } catch (IllegalAccessException | InvocationTargetException e) { | |
| if (e.getCause() instanceof RuntimeException) { | |
| throw (RuntimeException) e.getCause(); | |
| } | |
| throw new JDOFatalInternalException(e.getMessage()); | |
| } | |
| } | |
| } |