001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one
003: * or more contributor license agreements. See the NOTICE file
004: * distributed with this work for additional information
005: * regarding copyright ownership. The ASF licenses this file
006: * to you under the Apache License, Version 2.0 (the
007: * "License"); you may not use this file except in compliance
008: * with the License. You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing,
013: * software distributed under the License is distributed on an
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015: * KIND, either express or implied. See the License for the
016: * specific language governing permissions and limitations
017: * under the License.
018: */
019: package org.apache.openjpa.lib.meta;
020:
021: import java.io.File;
022: import java.io.IOException;
023: import java.io.InputStreamReader;
024: import java.io.Reader;
025: import java.net.URL;
026: import java.security.AccessController;
027: import java.util.ArrayList;
028: import java.util.Collection;
029: import java.util.Collections;
030: import java.util.HashMap;
031: import java.util.HashSet;
032: import java.util.LinkedList;
033: import java.util.List;
034: import java.util.Map;
035: import java.util.Set;
036: import javax.xml.parsers.SAXParser;
037:
038: import org.xml.sax.Attributes;
039: import org.xml.sax.InputSource;
040: import org.xml.sax.Locator;
041: import org.xml.sax.SAXException;
042: import org.xml.sax.SAXParseException;
043: import org.xml.sax.ext.LexicalHandler;
044: import org.xml.sax.helpers.DefaultHandler;
045: import org.apache.openjpa.lib.log.Log;
046: import org.apache.openjpa.lib.util.J2DoPrivHelper;
047: import org.apache.openjpa.lib.util.JavaVersions;
048: import org.apache.openjpa.lib.util.Localizer.Message;
049: import org.apache.openjpa.lib.util.Localizer;
050: import org.apache.openjpa.lib.xml.Commentable;
051: import org.apache.openjpa.lib.xml.DocTypeReader;
052: import org.apache.openjpa.lib.xml.Location;
053: import org.apache.openjpa.lib.xml.XMLFactory;
054:
055: /**
056: * Custom SAX parser used by the system to quickly parse metadata files.
057: * Subclasses should handle the processing of the content.
058: *
059: * @author Abe White
060: * @nojavadoc
061: */
062: public abstract class XMLMetaDataParser extends DefaultHandler
063: implements LexicalHandler, MetaDataParser {
064:
065: private static final Localizer _loc = Localizer
066: .forPackage(XMLMetaDataParser.class);
067: private static boolean _schemaBug;
068:
069: static {
070: try {
071: // check for Xerces version 2.0.2 to see if we need to disable
072: // schema validation, which works around the bug reported at:
073: // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4708859
074: _schemaBug = "Xerces-J 2.0.2".equals(Class.forName(
075: "org.apache.xerces.impl.Version").getField(
076: "fVersion").get(null));
077: } catch (Throwable t) {
078: // Xerces might not be available
079: _schemaBug = false;
080: }
081: }
082:
083: // map of classloaders to sets of parsed locations, so that we don't parse
084: // the same resource multiple times for the same class
085: private Map _parsed = null;
086:
087: private Log _log = null;
088: private boolean _validating = true;
089: private boolean _systemId = true;
090: private boolean _caching = true;
091: private boolean _parseText = true;
092: private boolean _parseComments = true;
093: private String _suffix = null;
094: private ClassLoader _loader = null;
095: private ClassLoader _curLoader = null;
096:
097: // state for current parse
098: private final Collection _curResults = new LinkedList();
099: private List _results = null;
100: private String _sourceName = null;
101: private File _sourceFile = null;
102: private StringBuffer _text = null;
103: private List _comments = null;
104: private Location _location = new Location();
105: private LexicalHandler _lh = null;
106: private int _depth = -1;
107: private int _ignore = Integer.MAX_VALUE;
108:
109: /**
110: * Whether to parse element text.
111: */
112: public boolean getParseText() {
113: return _parseText;
114: }
115:
116: /**
117: * Whether to parse element text.
118: */
119: public void setParseText(boolean text) {
120: _parseText = text;
121: }
122:
123: /**
124: * Whether to parse element comments.
125: */
126: public boolean getParseComments() {
127: return _parseComments;
128: }
129:
130: /**
131: * Whether to parse element comments.
132: */
133: public void setParseComments(boolean comments) {
134: _parseComments = comments;
135: }
136:
137: /**
138: * The XML document location.
139: */
140: public Location getLocation() {
141: return _location;
142: }
143:
144: /**
145: * The lexical handler that should be registered with the SAX parser used
146: * by this class. Since the <code>org.xml.sax.ext</code> package is not
147: * a required part of SAX2, this handler might not be used by the parser.
148: */
149: public LexicalHandler getLexicalHandler() {
150: return _lh;
151: }
152:
153: /**
154: * The lexical handler that should be registered with the SAX parser used
155: * by this class. Since the <code>org.xml.sax.ext</code> package is not
156: * a required part of SAX2, this handler might not be used by the parser.
157: */
158: public void setLexicalHandler(LexicalHandler lh) {
159: _lh = lh;
160: }
161:
162: /**
163: * The XML document location.
164: */
165: public void setLocation(Location location) {
166: _location = location;
167: }
168:
169: /**
170: * Whether to use the source name as the XML system id.
171: */
172: public boolean getSourceIsSystemId() {
173: return _systemId;
174: }
175:
176: /**
177: * Whether to use the source name as the XML system id.
178: */
179: public void setSourceIsSystemId(boolean systemId) {
180: _systemId = systemId;
181: }
182:
183: /**
184: * Whether this is a validating parser.
185: */
186: public boolean isValidating() {
187: return _validating;
188: }
189:
190: /**
191: * Whether this is a validating parser.
192: */
193: public void setValidating(boolean validating) {
194: _validating = validating;
195: }
196:
197: /**
198: * Expected suffix for metadata resources, or null if unknown.
199: */
200: public String getSuffix() {
201: return _suffix;
202: }
203:
204: /**
205: * Expected suffix for metadata resources, or null if unknown.
206: */
207: public void setSuffix(String suffix) {
208: _suffix = suffix;
209: }
210:
211: /**
212: * Whether parsed resource names are cached to avoid duplicate parsing.
213: */
214: public boolean isCaching() {
215: return _caching;
216: }
217:
218: /**
219: * Whether parsed resource names are cached to avoid duplicate parsing.
220: */
221: public void setCaching(boolean caching) {
222: _caching = caching;
223: if (!caching)
224: clear();
225: }
226:
227: /**
228: * The log to write to.
229: */
230: public Log getLog() {
231: return _log;
232: }
233:
234: /**
235: * The log to write to.
236: */
237: public void setLog(Log log) {
238: _log = log;
239: }
240:
241: /**
242: * Classloader to use for class name resolution.
243: */
244: public ClassLoader getClassLoader() {
245: return _loader;
246: }
247:
248: /**
249: * Classloader to use for class name resolution.
250: */
251: public void setClassLoader(ClassLoader loader) {
252: _loader = loader;
253: }
254:
255: public List getResults() {
256: if (_results == null)
257: return Collections.EMPTY_LIST;
258: return _results;
259: }
260:
261: public void parse(String rsrc) throws IOException {
262: if (rsrc != null)
263: parse(new ResourceMetaDataIterator(rsrc, _loader));
264: }
265:
266: public void parse(URL url) throws IOException {
267: if (url != null)
268: parse(new URLMetaDataIterator(url));
269: }
270:
271: public void parse(File file) throws IOException {
272: if (file == null)
273: return;
274: if (!((Boolean) AccessController.doPrivileged(J2DoPrivHelper
275: .isDirectoryAction(file))).booleanValue())
276: parse(new FileMetaDataIterator(file));
277: else {
278: String suff = (_suffix == null) ? "" : _suffix;
279: parse(new FileMetaDataIterator(file,
280: new SuffixMetaDataFilter(suff)));
281: }
282: }
283:
284: public void parse(Class cls, boolean topDown) throws IOException {
285: String suff = (_suffix == null) ? "" : _suffix;
286: parse(new ClassMetaDataIterator(cls, suff, topDown), !topDown);
287: }
288:
289: public void parse(Reader xml, String sourceName) throws IOException {
290: if (xml != null && (sourceName == null || !parsed(sourceName)))
291: parseNewResource(xml, sourceName);
292: }
293:
294: public void parse(MetaDataIterator itr) throws IOException {
295: parse(itr, false);
296: }
297:
298: /**
299: * Parse the resources returned by the given iterator, optionally stopping
300: * when the first valid resource is found.
301: */
302: private void parse(MetaDataIterator itr, boolean stopFirst)
303: throws IOException {
304: if (itr == null)
305: return;
306: try {
307: String sourceName;
308: while (itr.hasNext()) {
309: sourceName = itr.next().toString();
310: if (parsed(sourceName)) {
311: if (stopFirst)
312: break;
313: continue;
314: }
315:
316: // individual files of the resource might already be parsed
317: _sourceFile = itr.getFile();
318: parseNewResource(new InputStreamReader(itr
319: .getInputStream()), sourceName);
320: if (stopFirst)
321: break;
322: }
323: } finally {
324: itr.close();
325: }
326: }
327:
328: /**
329: * Parse a previously-unseen source. All parsing methods delegate
330: * to this one.
331: */
332: protected void parseNewResource(Reader xml, String sourceName)
333: throws IOException {
334: if (_log != null && _log.isTraceEnabled())
335: _log.trace(_loc.get("start-parse", sourceName));
336:
337: // even if we want to validate, specify that it won't happen
338: // if we have neither a DocType not a Schema
339: Object schemaSource = getSchemaSource();
340: if (schemaSource != null && _schemaBug) {
341: if (_log != null && _log.isTraceEnabled())
342: _log.trace(_loc.get("parser-schema-bug"));
343: schemaSource = null;
344: }
345: boolean validating = _validating
346: && (getDocType() != null || schemaSource != null);
347:
348: // parse the metadata with a SAX parser
349: try {
350: _sourceName = sourceName;
351: SAXParser parser = XMLFactory
352: .getSAXParser(validating, true);
353: Object schema = null;
354: if (validating) {
355: schema = schemaSource;
356: if (schema == null && getDocType() != null)
357: xml = new DocTypeReader(xml, getDocType());
358: }
359:
360: if (_parseComments || _lh != null)
361: parser
362: .setProperty(
363: "http://xml.org/sax/properties/lexical-handler",
364: this );
365:
366: if (schema != null) {
367: parser
368: .setProperty(
369: "http://java.sun.com/xml/jaxp/properties/schemaLanguage",
370: "http://www.w3.org/2001/XMLSchema");
371: parser
372: .setProperty(
373: "http://java.sun.com/xml/jaxp/properties/schemaSource",
374: schema);
375: }
376:
377: InputSource is = new InputSource(xml);
378: if (_systemId && sourceName != null)
379: is.setSystemId(sourceName);
380: parser.parse(is, this );
381: finish();
382: } catch (SAXException se) {
383: IOException ioe = new IOException(se.toString());
384: JavaVersions.initCause(ioe, se);
385: throw ioe;
386: } finally {
387: reset();
388: }
389: }
390:
391: /**
392: * Return true if the given source is parsed. Otherwise, record that
393: * it will be parsed.
394: */
395: protected boolean parsed(String src) {
396: if (!_caching)
397: return false;
398: if (_parsed == null)
399: _parsed = new HashMap();
400:
401: ClassLoader loader = currentClassLoader();
402: Set set = (Set) _parsed.get(loader);
403: if (set == null) {
404: set = new HashSet();
405: _parsed.put(loader, set);
406: }
407: boolean added = set.add(src);
408: if (!added && _log != null && _log.isTraceEnabled())
409: _log.trace(_loc.get("already-parsed", src));
410: return !added;
411: }
412:
413: public void clear() {
414: if (_log != null && _log.isTraceEnabled())
415: _log.trace(_loc.get("clear-parser", this ));
416: if (_parsed != null)
417: _parsed.clear();
418: }
419:
420: public void error(SAXParseException se) throws SAXException {
421: throw getException(se.toString());
422: }
423:
424: public void fatalError(SAXParseException se) throws SAXException {
425: throw getException(se.toString());
426: }
427:
428: public void setDocumentLocator(Locator locator) {
429: _location.setLocator(locator);
430: }
431:
432: public void startElement(String uri, String name, String qName,
433: Attributes attrs) throws SAXException {
434: _depth++;
435: if (_depth <= _ignore)
436: if (!startElement(qName, attrs))
437: ignoreContent(true);
438: }
439:
440: public void endElement(String uri, String name, String qName)
441: throws SAXException {
442: if (_depth < _ignore)
443: endElement(qName);
444: _text = null;
445: if (_comments != null)
446: _comments.clear();
447: if (_depth == _ignore)
448: _ignore = Integer.MAX_VALUE;
449: _depth--;
450: }
451:
452: public void characters(char[] ch, int start, int length) {
453: if (_parseText && _depth <= _ignore) {
454: if (_text == null)
455: _text = new StringBuffer();
456: _text.append(ch, start, length);
457: }
458: }
459:
460: public void comment(char[] ch, int start, int length)
461: throws SAXException {
462: if (_parseComments && _depth <= _ignore) {
463: if (_comments == null)
464: _comments = new ArrayList(3);
465: _comments.add(String.valueOf(ch, start, length));
466: }
467: if (_lh != null)
468: _lh.comment(ch, start, length);
469: }
470:
471: public void startCDATA() throws SAXException {
472: if (_lh != null)
473: _lh.startCDATA();
474: }
475:
476: public void endCDATA() throws SAXException {
477: if (_lh != null)
478: _lh.endCDATA();
479: }
480:
481: public void startDTD(String name, String publicId, String systemId)
482: throws SAXException {
483: if (_lh != null)
484: _lh.startDTD(name, publicId, systemId);
485: }
486:
487: public void endDTD() throws SAXException {
488: if (_lh != null)
489: _lh.endDTD();
490: }
491:
492: public void startEntity(String name) throws SAXException {
493: if (_lh != null)
494: _lh.startEntity(name);
495: }
496:
497: public void endEntity(String name) throws SAXException {
498: if (_lh != null)
499: _lh.endEntity(name);
500: }
501:
502: /**
503: * Override this method marking the start of some element. If this method
504: * returns false, the content of the element and the end element event will
505: * be ignored.
506: */
507: protected abstract boolean startElement(String name,
508: Attributes attrs) throws SAXException;
509:
510: /**
511: * Override this method marking the end of some element.
512: */
513: protected abstract void endElement(String name) throws SAXException;
514:
515: /**
516: * Add a result to be returned from the current parse.
517: */
518: protected void addResult(Object result) {
519: if (_log != null && _log.isTraceEnabled())
520: _log.trace(_loc.get("add-result", result));
521: _curResults.add(result);
522: }
523:
524: /**
525: * Override this method to finish up after a parse; this is only
526: * called if no errors are encountered during parsing. Subclasses should
527: * call <code>super.finish()</code> to resolve superclass state.
528: */
529: protected void finish() {
530: if (_log != null && _log.isTraceEnabled())
531: _log.trace(_loc.get("end-parse", getSourceName()));
532: _results = new ArrayList(_curResults);
533: }
534:
535: /**
536: * Override this method to clear any state and ready the parser for
537: * a new document. Subclasses should call
538: * <code>super.reset()</code> to clear superclass state.
539: */
540: protected void reset() {
541: _curResults.clear();
542: _curLoader = null;
543: _sourceName = null;
544: _sourceFile = null;
545: _depth = -1;
546: _ignore = Integer.MAX_VALUE;
547: if (_comments != null)
548: _comments.clear();
549: }
550:
551: /**
552: * Implement to return the XML schema source for the document. Returns
553: * null by default. May return:
554: * <ul>
555: * <li><code>String</code> pointing to schema URI.</li>
556: * <li><code>InputStream</code> containing schema contents.</li>
557: * <li><code>InputSource</code> containing schema contents.</li>
558: * <li><code>File</code> containing schema contents.</li>
559: * <li>Array of any of the above elements.</li>
560: * </ul>
561: */
562: protected Object getSchemaSource() throws IOException {
563: return null;
564: }
565:
566: /**
567: * Override this method to return any <code>DOCTYPE</code> declaration
568: * that should be dynamically included in xml documents that will be
569: * validated. Returns null by default.
570: */
571: protected Reader getDocType() throws IOException {
572: return null;
573: }
574:
575: /**
576: * Return the name of the source file being parsed.
577: */
578: protected String getSourceName() {
579: return _sourceName;
580: }
581:
582: /**
583: * Return the file of the source being parsed.
584: */
585: protected File getSourceFile() {
586: return _sourceFile;
587: }
588:
589: /**
590: * Add current comments to the given entity. By default, assumes entity
591: * is {@link Commentable}.
592: */
593: protected void addComments(Object obj) {
594: String[] comments = currentComments();
595: if (comments.length > 0 && obj instanceof Commentable)
596: ((Commentable) obj).setComments(comments);
597: }
598:
599: /**
600: * Array of comments for the current node, or empty array if none.
601: */
602: protected String[] currentComments() {
603: if (_comments == null || _comments.isEmpty())
604: return Commentable.EMPTY_COMMENTS;
605: return (String[]) _comments
606: .toArray(new String[_comments.size()]);
607: }
608:
609: /**
610: * Return the text value within the current node.
611: */
612: protected String currentText() {
613: if (_text == null)
614: return "";
615: return _text.toString().trim();
616: }
617:
618: /**
619: * Return the current location within the source file.
620: */
621: protected String currentLocation() {
622: return " [" + _loc.get("loc-prefix") + _location.getLocation()
623: + "]";
624: }
625:
626: /**
627: * Return the parse depth. Within the root element, the depth is 0,
628: * within the first nested element, it is 1, and so forth.
629: */
630: protected int currentDepth() {
631: return _depth;
632: }
633:
634: /**
635: * Return the class loader to use when resolving resources and loading
636: * classes.
637: */
638: protected ClassLoader currentClassLoader() {
639: if (_loader != null)
640: return _loader;
641: if (_curLoader == null)
642: _curLoader = (ClassLoader) AccessController
643: .doPrivileged(J2DoPrivHelper
644: .getContextClassLoaderAction());
645: return _curLoader;
646: }
647:
648: /**
649: * Ignore all content below the current element.
650: *
651: * @param ignoreEnd whether to ignore the end element event
652: */
653: protected void ignoreContent(boolean ignoreEnd) {
654: _ignore = _depth;
655: if (!ignoreEnd)
656: _ignore++;
657: }
658:
659: /**
660: * Returns a SAXException with the source file name and the given error
661: * message.
662: */
663: protected SAXException getException(String msg) {
664: return new SAXException(getSourceName() + currentLocation()
665: + ": " + msg);
666: }
667:
668: /**
669: * Returns a SAXException with the source file name and the given error
670: * message.
671: */
672: protected SAXException getException(Message msg) {
673: return new SAXException(getSourceName() + currentLocation()
674: + ": " + msg.getMessage());
675: }
676:
677: /**
678: * Returns a SAXException with the source file name and the given error
679: * message.
680: */
681: protected SAXException getException(Message msg, Throwable cause) {
682: if (cause != null && _log != null && _log.isTraceEnabled())
683: _log.trace(_loc.get("sax-exception", getSourceName(),
684: _location.getLocation()), cause);
685: SAXException e = new SAXException(getSourceName()
686: + currentLocation() + ": " + msg + " [" + cause + "]");
687: e.initCause(cause);
688: return e;
689: }
690: }
|