blob: 4cf064eda5e7d74f38d19fe51b07c5e5cb47247c [file] [log] [blame]
/*
* 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)))
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) {
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(), se);
throw ioe;
} finally {
if (overrideCL) {
// 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() {
}
}