| /* |
| * 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 |
| * |
| * http://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 org.apache.openjpa.lib.meta; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.net.URL; |
| import java.security.AccessController; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import javax.xml.parsers.SAXParser; |
| |
| import org.apache.openjpa.lib.log.Log; |
| import org.apache.openjpa.lib.util.J2DoPrivHelper; |
| import org.apache.openjpa.lib.util.Localizer; |
| import org.apache.openjpa.lib.util.Localizer.Message; |
| import org.apache.openjpa.lib.xml.Commentable; |
| import org.apache.openjpa.lib.xml.DocTypeReader; |
| import org.apache.openjpa.lib.xml.Location; |
| import org.apache.openjpa.lib.xml.XMLFactory; |
| import org.xml.sax.Attributes; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.Locator; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.SAXParseException; |
| import org.xml.sax.ext.LexicalHandler; |
| import org.xml.sax.helpers.DefaultHandler; |
| |
| /** |
| * Custom SAX parser used by the system to quickly parse metadata files. |
| * Subclasses should handle the processing of the content. |
| * |
| * @author Abe White |
| */ |
| public abstract class XMLMetaDataParser extends DefaultHandler |
| implements LexicalHandler, MetaDataParser { |
| |
| private static final Localizer _loc = Localizer.forPackage |
| (XMLMetaDataParser.class); |
| private static boolean _schemaBug; |
| |
| private static final String OPENJPA_NAMESPACE = "http://openjpa.apache.org/ns/orm"; |
| protected int _extendedNamespace = 0; |
| protected int _openjpaNamespace = 0; |
| |
| static { |
| try { |
| // check for Xerces version 2.0.2 to see if we need to disable |
| // schema validation, which works around the bug reported at: |
| // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4708859 |
| _schemaBug = "Xerces-J 2.0.2".equals(Class.forName |
| ("org.apache.xerces.impl.Version").getField("fVersion"). |
| get(null)); |
| } catch (Throwable t) { |
| // Xerces might not be available |
| _schemaBug = false; |
| } |
| } |
| |
| // map of classloaders to sets of parsed locations, so that we don't parse |
| // the same resource multiple times for the same class |
| private Map<ClassLoader, Set<String>> _parsed = null; |
| |
| private Log _log = null; |
| private boolean _validating = true; |
| private boolean _systemId = true; |
| private boolean _caching = true; |
| private boolean _parseText = true; |
| private boolean _parseComments = true; |
| private String _suffix = null; |
| private ClassLoader _loader = null; |
| private ClassLoader _curLoader = null; |
| |
| // state for current parse |
| private final Collection _curResults = new LinkedList(); |
| private List _results = null; |
| private String _sourceName = null; |
| private File _sourceFile = null; |
| private StringBuffer _text = null; |
| private List<String> _comments = null; |
| private Location _location = new Location(); |
| private LexicalHandler _lh = null; |
| private int _depth = -1; |
| private int _ignore = Integer.MAX_VALUE; |
| |
| private boolean _parsing = false; |
| |
| private boolean _overrideContextClassloader = true; |
| |
| public boolean getOverrideContextClassloader() { |
| return _overrideContextClassloader; |
| } |
| |
| public void setOverrideContextClassloader(boolean overrideCCL) { |
| _overrideContextClassloader = overrideCCL; |
| } |
| |
| /* |
| * Whether the parser is currently parsing. |
| */ |
| public boolean isParsing() { |
| return _parsing; |
| } |
| |
| /* |
| * Whether the parser is currently parsing. |
| */ |
| public void setParsing(boolean parsing) { |
| this._parsing = parsing; |
| } |
| |
| /** |
| * Whether to parse element text. |
| */ |
| public boolean getParseText() { |
| return _parseText; |
| } |
| |
| /** |
| * Whether to parse element text. |
| */ |
| public void setParseText(boolean text) { |
| _parseText = text; |
| } |
| |
| /** |
| * Whether to parse element comments. |
| */ |
| public boolean getParseComments() { |
| return _parseComments; |
| } |
| |
| /** |
| * Whether to parse element comments. |
| */ |
| public void setParseComments(boolean comments) { |
| _parseComments = comments; |
| } |
| |
| /** |
| * The XML document location. |
| */ |
| public Location getLocation() { |
| return _location; |
| } |
| |
| /** |
| * The lexical handler that should be registered with the SAX parser used |
| * by this class. Since the <code>org.xml.sax.ext</code> package is not |
| * a required part of SAX2, this handler might not be used by the parser. |
| */ |
| public LexicalHandler getLexicalHandler() { |
| return _lh; |
| } |
| |
| /** |
| * The lexical handler that should be registered with the SAX parser used |
| * by this class. Since the <code>org.xml.sax.ext</code> package is not |
| * a required part of SAX2, this handler might not be used by the parser. |
| */ |
| public void setLexicalHandler(LexicalHandler lh) { |
| _lh = lh; |
| } |
| |
| /** |
| * The XML document location. |
| */ |
| public void setLocation(Location location) { |
| _location = location; |
| } |
| |
| /** |
| * Whether to use the source name as the XML system id. |
| */ |
| public boolean getSourceIsSystemId() { |
| return _systemId; |
| } |
| |
| /** |
| * Whether to use the source name as the XML system id. |
| */ |
| public void setSourceIsSystemId(boolean systemId) { |
| _systemId = systemId; |
| } |
| |
| /** |
| * Whether this is a validating parser. |
| */ |
| public boolean isValidating() { |
| return _validating; |
| } |
| |
| /** |
| * Whether this is a validating parser. |
| */ |
| public void setValidating(boolean validating) { |
| _validating = validating; |
| } |
| |
| /** |
| * Expected suffix for metadata resources, or null if unknown. |
| */ |
| public String getSuffix() { |
| return _suffix; |
| } |
| |
| /** |
| * Expected suffix for metadata resources, or null if unknown. |
| */ |
| public void setSuffix(String suffix) { |
| _suffix = suffix; |
| } |
| |
| /** |
| * Whether parsed resource names are cached to avoid duplicate parsing. |
| */ |
| public boolean isCaching() { |
| return _caching; |
| } |
| |
| /** |
| * Whether parsed resource names are cached to avoid duplicate parsing. |
| */ |
| public void setCaching(boolean caching) { |
| _caching = caching; |
| if (!caching) |
| clear(); |
| } |
| |
| /** |
| * The log to write to. |
| */ |
| public Log getLog() { |
| return _log; |
| } |
| |
| /** |
| * The log to write to. |
| */ |
| public void setLog(Log log) { |
| _log = log; |
| } |
| |
| /** |
| * Classloader to use for class name resolution. |
| */ |
| public ClassLoader getClassLoader() { |
| return _loader; |
| } |
| |
| /** |
| * Classloader to use for class name resolution. |
| */ |
| @Override |
| public void setClassLoader(ClassLoader loader) { |
| _loader = loader; |
| } |
| |
| @Override |
| public List getResults() { |
| if (_results == null) |
| return Collections.emptyList(); |
| return _results; |
| } |
| |
| @Override |
| public void parse(String rsrc) throws IOException { |
| if (rsrc != null) |
| parse(new ResourceMetaDataIterator(rsrc, _loader)); |
| } |
| |
| @Override |
| public void parse(URL url) throws IOException { |
| if (url != null) |
| parse(new URLMetaDataIterator(url)); |
| } |
| |
| @Override |
| public void parse(File file) throws IOException { |
| if (file == null) |
| return; |
| if (!(AccessController.doPrivileged(J2DoPrivHelper |
| .isDirectoryAction(file))).booleanValue()) |
| parse(new FileMetaDataIterator(file)); |
| else { |
| String suff = (_suffix == null) ? "" : _suffix; |
| parse(new FileMetaDataIterator(file, |
| new SuffixMetaDataFilter(suff))); |
| } |
| } |
| |
| @Override |
| public void parse(Class cls, boolean topDown) throws IOException { |
| String suff = (_suffix == null) ? "" : _suffix; |
| parse(new ClassMetaDataIterator(cls, suff, topDown), !topDown); |
| } |
| |
| @Override |
| public void parse(Reader xml, String sourceName) throws IOException { |
| if (xml != null && (sourceName == null || !parsed(sourceName))) |
| parseNewResource(xml, sourceName); |
| } |
| |
| @Override |
| public void parse(MetaDataIterator itr) throws IOException { |
| parse(itr, false); |
| } |
| |
| /** |
| * Parse the resources returned by the given iterator, optionally stopping |
| * when the first valid resource is found. |
| */ |
| private void parse(MetaDataIterator itr, boolean stopFirst) |
| throws IOException { |
| if (itr == null) |
| return; |
| try { |
| String sourceName; |
| while (itr.hasNext()) { |
| sourceName = itr.next().toString(); |
| if (parsed(sourceName)) { |
| if (stopFirst) |
| break; |
| continue; |
| } |
| |
| // individual files of the resource might already be parsed |
| _sourceFile = itr.getFile(); |
| parseNewResource(new InputStreamReader(itr.getInputStream()), |
| sourceName); |
| if (stopFirst) |
| break; |
| } |
| } |
| finally { |
| itr.close(); |
| } |
| } |
| |
| /** |
| * Parse a previously-unseen source. All parsing methods delegate |
| * to this one. |
| */ |
| protected void parseNewResource(Reader xml, String sourceName) |
| throws IOException { |
| if (_log != null && _log.isTraceEnabled()) |
| _log.trace(_loc.get("start-parse", sourceName)); |
| |
| // even if we want to validate, specify that it won't happen |
| // if we have neither a DocType not a Schema |
| Object schemaSource = getSchemaSource(); |
| if (schemaSource != null && _schemaBug) { |
| if (_log != null && _log.isTraceEnabled()) |
| _log.trace(_loc.get("parser-schema-bug")); |
| schemaSource = null; |
| } |
| boolean validating = _validating && (getDocType() != null |
| || schemaSource != null); |
| |
| // parse the metadata with a SAX parser |
| try { |
| setParsing(true); |
| _sourceName = sourceName; |
| |
| SAXParser parser = null; |
| boolean overrideCL = _overrideContextClassloader; |
| ClassLoader oldLoader = null; |
| ClassLoader newLoader = null; |
| |
| try { |
| if (overrideCL == true) { |
| oldLoader = |
| (ClassLoader) AccessController.doPrivileged(J2DoPrivHelper.getContextClassLoaderAction()); |
| newLoader = XMLMetaDataParser.class.getClassLoader(); |
| AccessController.doPrivileged(J2DoPrivHelper.setContextClassLoaderAction(newLoader)); |
| |
| if (_log != null && _log.isTraceEnabled()) { |
| _log.trace(_loc.get("override-contextclassloader-begin", oldLoader, newLoader)); |
| } |
| } |
| |
| parser = XMLFactory.getSAXParser(validating, true); |
| Object schema = null; |
| if (validating) { |
| schema = schemaSource; |
| if (schema == null && getDocType() != null) |
| xml = new DocTypeReader(xml, getDocType()); |
| } |
| |
| if (_parseComments || _lh != null) |
| parser.setProperty |
| ("http://xml.org/sax/properties/lexical-handler", this); |
| |
| if (schema != null) { |
| parser.setProperty |
| ("http://java.sun.com/xml/jaxp/properties/schemaLanguage", |
| "http://www.w3.org/2001/XMLSchema"); |
| parser.setProperty |
| ("http://java.sun.com/xml/jaxp/properties/schemaSource", |
| schema); |
| } |
| |
| InputSource is = new InputSource(xml); |
| if (_systemId && sourceName != null) |
| is.setSystemId(sourceName); |
| parser.parse(is, this); |
| finish(); |
| } catch (SAXException se) { |
| IOException ioe = new IOException(se.toString()); |
| ioe.initCause(se); |
| throw ioe; |
| } finally { |
| if (overrideCL == true) { |
| // Restore the old ContextClassloader |
| try { |
| if (_log != null && _log.isTraceEnabled()) { |
| _log.trace(_loc.get("override-contextclassloader-end", newLoader, oldLoader)); |
| } |
| AccessController.doPrivileged(J2DoPrivHelper.setContextClassLoaderAction(oldLoader)); |
| } catch (Throwable t) { |
| if (_log != null && _log.isWarnEnabled()) { |
| _log.warn(_loc.get("restore-contextclassloader-failed")); |
| } |
| } |
| } |
| } |
| } finally { |
| reset(); |
| } |
| } |
| |
| /** |
| * Return true if the given source is parsed. Otherwise, record that |
| * it will be parsed. |
| */ |
| protected boolean parsed(String src) { |
| if (!_caching) |
| return false; |
| if (_parsed == null) |
| _parsed = new HashMap<>(); |
| |
| ClassLoader loader = currentClassLoader(); |
| Set<String> set = _parsed.get(loader); |
| if (set == null) { |
| set = new HashSet<>(); |
| _parsed.put(loader, set); |
| } |
| boolean added = set.add(src); |
| if (!added && _log != null && _log.isTraceEnabled()) |
| _log.trace(_loc.get("already-parsed", src)); |
| return !added; |
| } |
| |
| @Override |
| public void clear() { |
| if (_log != null && _log.isTraceEnabled()) |
| _log.trace(_loc.get("clear-parser", this)); |
| if (_parsed != null) |
| _parsed.clear(); |
| } |
| |
| @Override |
| public void error(SAXParseException se) throws SAXException { |
| throw getException(se.toString()); |
| } |
| |
| @Override |
| public void fatalError(SAXParseException se) throws SAXException { |
| throw getException(se.toString()); |
| } |
| |
| @Override |
| public void setDocumentLocator(Locator locator) { |
| _location.setLocator(locator); |
| } |
| |
| @Override |
| public void startElement(String uri, String name, String qName, |
| Attributes attrs) throws SAXException { |
| _depth++; |
| if (_depth <= _ignore){ |
| if (uri.equals(OPENJPA_NAMESPACE)) { |
| _extendedNamespace++; |
| _openjpaNamespace++; |
| } |
| if (!startElement(qName, attrs)) |
| ignoreContent(true); |
| } |
| } |
| |
| @Override |
| public void endElement(String uri, String name, String qName) |
| throws SAXException { |
| if (_depth < _ignore) { |
| endElement(qName); |
| _extendedNamespace = (_extendedNamespace > 0) ? _extendedNamespace - 1 : 0; |
| _openjpaNamespace = (_openjpaNamespace > 0) ? _openjpaNamespace - 1 : 0; |
| } |
| else if (_depth ==_ignore) { |
| _extendedNamespace = (_extendedNamespace > 0) ? _extendedNamespace - 1 : 0; |
| _openjpaNamespace = (_openjpaNamespace > 0) ? _openjpaNamespace - 1 : 0; |
| } |
| |
| _text = null; |
| if (_comments != null) |
| _comments.clear(); |
| if (_depth == _ignore) |
| _ignore = Integer.MAX_VALUE; |
| _depth--; |
| } |
| |
| @Override |
| public void characters(char[] ch, int start, int length) { |
| if (_parseText && _depth <= _ignore) { |
| if (_text == null) |
| _text = new StringBuffer(); |
| _text.append(ch, start, length); |
| } |
| } |
| |
| @Override |
| public void comment(char[] ch, int start, int length) throws SAXException { |
| if (_parseComments && _depth <= _ignore) { |
| if (_comments == null) |
| _comments = new ArrayList<>(3); |
| _comments.add(String.valueOf(ch, start, length)); |
| } |
| if (_lh != null) |
| _lh.comment(ch, start, length); |
| } |
| |
| @Override |
| public void startCDATA() throws SAXException { |
| if (_lh != null) |
| _lh.startCDATA(); |
| } |
| |
| @Override |
| public void endCDATA() throws SAXException { |
| if (_lh != null) |
| _lh.endCDATA(); |
| } |
| |
| @Override |
| public void startDTD(String name, String publicId, String systemId) |
| throws SAXException { |
| if (_lh != null) |
| _lh.startDTD(name, publicId, systemId); |
| } |
| |
| @Override |
| public void endDTD() throws SAXException { |
| if (_lh != null) |
| _lh.endDTD(); |
| } |
| |
| @Override |
| public void startEntity(String name) throws SAXException { |
| if (_lh != null) |
| _lh.startEntity(name); |
| } |
| |
| @Override |
| public void endEntity(String name) throws SAXException { |
| if (_lh != null) |
| _lh.endEntity(name); |
| } |
| |
| /** |
| * Override this method marking the start of some element. If this method |
| * returns false, the content of the element and the end element event will |
| * be ignored. |
| */ |
| protected abstract boolean startElement(String name, Attributes attrs) |
| throws SAXException; |
| |
| /** |
| * Override this method marking the end of some element. |
| */ |
| protected abstract void endElement(String name) throws SAXException; |
| |
| /** |
| * Add a result to be returned from the current parse. |
| */ |
| protected void addResult(Object result) { |
| if (_log != null && _log.isTraceEnabled()) |
| _log.trace(_loc.get("add-result", result)); |
| _curResults.add(result); |
| } |
| |
| /** |
| * Override this method to finish up after a parse; this is only |
| * called if no errors are encountered during parsing. Subclasses should |
| * call <code>super.finish()</code> to resolve superclass state. |
| */ |
| protected void finish() { |
| if (_log != null && _log.isTraceEnabled()) |
| _log.trace(_loc.get("end-parse", getSourceName())); |
| _results = new ArrayList(_curResults); |
| } |
| |
| /** |
| * Override this method to clear any state and ready the parser for |
| * a new document. Subclasses should call |
| * <code>super.reset()</code> to clear superclass state. |
| */ |
| protected void reset() { |
| _curResults.clear(); |
| _curLoader = null; |
| _sourceName = null; |
| _sourceFile = null; |
| _depth = -1; |
| _ignore = Integer.MAX_VALUE; |
| if (_comments != null) |
| _comments.clear(); |
| clearDeferredMetaData(); |
| setParsing(false); |
| } |
| |
| /** |
| * Implement to return the XML schema source for the document. Returns |
| * null by default. May return: |
| * <ul> |
| * <li><code>String</code> pointing to schema URI.</li> |
| * <li><code>InputStream</code> containing schema contents.</li> |
| * <li><code>InputSource</code> containing schema contents.</li> |
| * <li><code>File</code> containing schema contents.</li> |
| * <li>Array of any of the above elements.</li> |
| * </ul> |
| */ |
| protected Object getSchemaSource() throws IOException { |
| return null; |
| } |
| |
| /** |
| * Override this method to return any <code>DOCTYPE</code> declaration |
| * that should be dynamically included in xml documents that will be |
| * validated. Returns null by default. |
| */ |
| protected Reader getDocType() throws IOException { |
| return null; |
| } |
| |
| /** |
| * Return the name of the source file being parsed. |
| */ |
| protected String getSourceName() { |
| return _sourceName; |
| } |
| |
| /** |
| * Return the file of the source being parsed. |
| */ |
| protected File getSourceFile() { |
| return _sourceFile; |
| } |
| |
| /** |
| * Add current comments to the given entity. By default, assumes entity |
| * is {@link Commentable}. |
| */ |
| protected void addComments(Object obj) { |
| String[] comments = currentComments(); |
| if (comments.length > 0 && obj instanceof Commentable) |
| ((Commentable) obj).setComments(comments); |
| } |
| |
| /** |
| * Array of comments for the current node, or empty array if none. |
| */ |
| protected String[] currentComments() { |
| if (_comments == null || _comments.isEmpty()) |
| return Commentable.EMPTY_COMMENTS; |
| return _comments.toArray(new String[_comments.size()]); |
| } |
| |
| /** |
| * Return the text value within the current node. |
| */ |
| protected String currentText() { |
| if (_text == null) |
| return ""; |
| return _text.toString().trim(); |
| } |
| |
| /** |
| * Return the current location within the source file. |
| */ |
| protected String currentLocation() { |
| return " [" + _loc.get("loc-prefix") + _location.getLocation() + "]"; |
| } |
| |
| /** |
| * Return the parse depth. Within the root element, the depth is 0, |
| * within the first nested element, it is 1, and so forth. |
| */ |
| protected int currentDepth() { |
| return _depth; |
| } |
| |
| /** |
| * Return the class loader to use when resolving resources and loading |
| * classes. |
| */ |
| protected ClassLoader currentClassLoader() { |
| if (_loader != null) |
| return _loader; |
| if (_curLoader == null) |
| _curLoader = AccessController.doPrivileged( |
| J2DoPrivHelper.getContextClassLoaderAction()); |
| return _curLoader; |
| } |
| |
| /** |
| * Ignore all content below the current element. |
| * |
| * @param ignoreEnd whether to ignore the end element event |
| */ |
| protected void ignoreContent(boolean ignoreEnd) { |
| _ignore = _depth; |
| if (!ignoreEnd) |
| _ignore++; |
| } |
| |
| /** |
| * Returns a SAXException with the source file name and the given error |
| * message. |
| */ |
| protected SAXException getException(String msg) { |
| return new SAXException(getSourceName() + currentLocation() + |
| ": " + msg); |
| } |
| |
| /** |
| * Returns a SAXException with the source file name and the given error |
| * message. |
| */ |
| protected SAXException getException(Message msg) { |
| return new SAXException(getSourceName() + currentLocation() + |
| ": " + msg.getMessage()); |
| } |
| |
| /** |
| * Returns a SAXException with the source file name and the given error |
| * message. |
| */ |
| protected SAXException getException(Message msg, Throwable cause) { |
| if (cause != null && _log != null && _log.isTraceEnabled()) |
| _log.trace(_loc.get("sax-exception", |
| getSourceName(), _location.getLocation()), cause); |
| SAXException e = new SAXException(getSourceName() + currentLocation() + |
| ": " + msg + " [" + cause + "]"); |
| e.initCause(cause); |
| return e; |
| } |
| |
| protected void clearDeferredMetaData() { |
| } |
| } |