001: /* *****************************************************************************
002: * Parser.java
003: * ****************************************************************************/
004:
005: /* J_LZ_COPYRIGHT_BEGIN *******************************************************
006: * Copyright 2001-2007 Laszlo Systems, Inc. All Rights Reserved. *
007: * Use is subject to license terms. *
008: * J_LZ_COPYRIGHT_END *********************************************************/
009:
010: package org.openlaszlo.compiler;
011:
012: import java.io.*;
013: import java.lang.*;
014: import java.util.*;
015: import org.apache.log4j.Logger;
016: import org.jdom.Attribute;
017: import org.jdom.Document;
018: import org.jdom.Content;
019: import org.jdom.Element;
020: import org.jdom.JDOMException;
021: import org.jdom.Namespace;
022: import org.jdom.JDOMFactory;
023: import org.jdom.input.SAXBuilder;
024: import org.jdom.input.SAXHandler;
025: import org.jdom.output.SAXOutputter;
026: import org.jdom.output.XMLOutputter;
027: import org.xml.sax.InputSource;
028: import org.xml.sax.SAXException;
029: import org.xml.sax.XMLFilter;
030: import org.xml.sax.helpers.XMLFilterImpl;
031: import org.apache.commons.collections.LRUMap;
032: import org.openlaszlo.server.*;
033: import org.openlaszlo.utils.*;
034: import org.openlaszlo.xml.internal.*;
035:
036: import javax.xml.transform.TransformerConfigurationException;
037: import javax.xml.transform.TransformerFactory;
038: import javax.xml.transform.sax.SAXResult;
039: import javax.xml.transform.sax.SAXTransformerFactory;
040: import javax.xml.transform.sax.TransformerHandler;
041: import javax.xml.transform.stream.StreamSource;
042:
043: import org.xml.sax.SAXException;
044:
045: /** Parses and validates an XML file. XML elements are annotated with
046: * their source locations.
047: *
048: * A new parser should be used for each compilation, but shared across
049: * all XML file reads within a compilation. This assures that the
050: * parser caches are active during compilation, but are not reused
051: * for subsequent compilations when the file may have been modified.
052: */
053: public class Parser {
054: private static Logger mLogger = Logger.getLogger(Parser.class);
055: private static Logger mPostTransformationLogger = Logger
056: .getLogger("postTransformation");
057: private static Logger mPreValidationLogger = Logger
058: .getLogger("preValidation");
059: static public Namespace sNamespace = Namespace
060: .getNamespace("http://www.laszlosystems.com/2003/05/lzx");
061:
062: protected FileResolver resolver = FileResolver.DEFAULT_FILE_RESOLVER;
063:
064: /** Map(File, Document) */
065: protected final Map fileCache = new HashMap();
066: /** List<String>. Pathnames in messages are reported relative to
067: * one of these. */
068: List basePathnames = new Vector();
069:
070: // Stylesheet templates and generators for updating old
071: // namespaces, and adding namespace declarations.
072: private static javax.xml.transform.Templates sPreprocessorTemplates;
073: private static SAXTransformerFactory sSaxFactory;
074: private static long sPreprocessorLastModified;
075:
076: protected static synchronized SAXTransformerFactory getSaxFactory() {
077: String stylePath = LPS.HOME().replace('\\', '/') + "/"
078: + "WEB-INF" + "/" + "lps" + "/" + "schema" + "/"
079: + "preprocess.xsl";
080: File styleFile = new File(stylePath);
081: long lastModified = styleFile.lastModified();
082:
083: if (sSaxFactory != null
084: && sPreprocessorLastModified == lastModified)
085: return sSaxFactory;
086:
087: // name the class instead of using
088: // TransformerFactory.newInstance(), to insure that we get
089: // saxon and thereby work around a failure on Tomcat 5 w/ jdk
090: // 1.4.2 Linux, and w/ Sun 1.4.1_05
091: javax.xml.transform.TransformerFactory factory = new com.icl.saxon.TransformerFactoryImpl();
092:
093: javax.xml.transform.Templates templates = null;
094: try {
095: templates = factory.newTemplates(new StreamSource(
096: "file:///" + stylePath));
097: } catch (TransformerConfigurationException e) {
098: throw new ChainedException(e);
099: }
100:
101: if (!factory.getFeature(SAXTransformerFactory.FEATURE))
102: throw new RuntimeException(
103: /* (non-Javadoc)
104: * @i18n.test
105: * @org-mes="TransformerFactory doesn't implement SAXTransformerFactory"
106: */
107: org.openlaszlo.i18n.LaszloMessages.getMessage(Parser.class
108: .getName(), "051018-107"));
109:
110: SAXTransformerFactory saxFactory = (SAXTransformerFactory) factory;
111:
112: sSaxFactory = saxFactory;
113: sPreprocessorTemplates = templates;
114: sPreprocessorLastModified = lastModified;
115: return saxFactory;
116: }
117:
118: public Parser() {
119: }
120:
121: public void setResolver(FileResolver resolver) {
122: this .resolver = resolver;
123: }
124:
125: /** Returns the pathname to use in user error messages. This is
126: * the shortest pathname relative to a directory on the search
127: * path for this application.
128: */
129: public String getUserPathname(String pathname) {
130: String sourceDir = new File(pathname).getParent();
131: if (sourceDir == null)
132: sourceDir = "";
133: sourceDir = sourceDir.replace(File.separatorChar, '/');
134: String best = pathname.replace(File.separatorChar, '/');
135: int bestLength = StringUtils.split(best, "/").length;
136: for (Iterator iter = basePathnames.iterator(); iter.hasNext();) {
137: String item = (String) iter.next();
138: String base = item.replace(File.separatorChar, '/');
139: try {
140: String candidate = FileUtils.adjustRelativePath(
141: new File(pathname).getName(), base, sourceDir);
142: int candidateLength = StringUtils.split(candidate, "/").length;
143: if (candidateLength < bestLength) {
144: best = candidate;
145: bestLength = candidateLength;
146: }
147: } catch (FileUtils.RelativizationError e) {
148: // If it can't be relativized, it simply doesn't produce
149: // a candidate, and we do nothing.
150: }
151: }
152: return best;
153: }
154:
155: /** Reads an XML document and adds source location information to
156: * the elements. */
157: public Document read(File file) throws JDOMException, IOException {
158: // See if we've already read the file. This is an
159: // optimization, and also assures that the same content is
160: // used across passes. We don't need to (and shouldn't) check
161: // the date, since the cache is an instance variable, and each
162: // compiler invocation uses a fresh parser.
163: File key = file.getCanonicalFile();
164: if (fileCache.containsKey(key)) {
165: return (Document) fileCache.get(key);
166: }
167:
168: // Use a custom subclass of SAXBuilder to build a JDOM tree
169: // containing custom Elements which contain source file
170: // location info (ElementWithLocationInfo).
171:
172: // The following variables are used to add source location
173: // that reflects the input name, while the system identifier
174: // has been made absolute.
175: final String pathname = file.getPath();
176: final String messagePathname = getUserPathname(pathname);
177:
178: // This is a ContentHandler which adds source location info to
179: // our own custom class of jdom.Element
180: class SourceLocatorHandler extends org.jdom.input.SAXHandler {
181: org.xml.sax.Locator locator;
182: int startLineNumber;
183: int startColumnNumber;
184: Element currentElement;
185:
186: SourceLocatorHandler() throws IOException {
187: }
188:
189: SourceLocatorHandler(JDOMFactory factory)
190: throws IOException {
191: super (factory);
192: }
193:
194: public void characters(char[] ch, int start, int length)
195: throws SAXException {
196: startLineNumber = locator.getLineNumber();
197: startColumnNumber = locator.getColumnNumber();
198: super .characters(ch, start, length);
199: }
200:
201: public void endElement(String namespaceURI,
202: String localName, String qName) throws SAXException {
203: // You can only call this.getCurrentElement() before
204: // super.endElement
205:
206: // Save source location info for reporting compilation errors
207: saveEndLocation(this .getCurrentElement(), pathname,
208: messagePathname, locator.getLineNumber(),
209: locator.getColumnNumber());
210:
211: super .endElement(namespaceURI, localName, qName);
212: }
213:
214: public void setDocumentLocator(org.xml.sax.Locator locator) {
215: this .locator = locator;
216: }
217:
218: public void startElement(String namespaceURI,
219: String localName, String qName,
220: org.xml.sax.Attributes atts) throws SAXException {
221: super
222: .startElement(namespaceURI, localName, qName,
223: atts);
224:
225: // You can only call this.getCurrentElement() after
226: // super.startElement
227:
228: // Save source location info for reporting compilation errors
229: saveStartLocation(this .getCurrentElement(), pathname,
230: messagePathname, locator.getLineNumber(),
231: locator.getColumnNumber());
232: }
233: }
234:
235: /* We need a SAXBuilder that uses our custom Factory and
236: ContentHandler classes, but the stock
237: org.jdom.input.SAXBuilder has no API for setting which
238: ContentHandler is used.
239:
240: To get what we need, we create a subclass of SAXBuilder
241: and override the createContentHandler method to
242: instantiate our custom handler with our custom factory .
243: */
244: class SourceLocatorSAXBuilder extends SAXBuilder {
245: SourceLocatorSAXBuilder(String saxDriverClass) {
246: super (saxDriverClass);
247: }
248:
249: SourceLocatorSAXBuilder() {
250: super ();
251: }
252:
253: // We need to create our own special contentHandler,
254: // and you *must* pass the factory to the SaxHandler
255: // constructor, or else you get a default JDOM
256: // factory, which is not what you want!
257: protected org.jdom.input.SAXHandler createContentHandler() {
258: try {
259: return new SourceLocatorHandler(getFactory());
260: } catch (IOException e) {
261: throw new ChainedException(e);
262: }
263: }
264: }
265:
266: //SAXBuilder builder = new SourceLocatorSAXBuilder("org.apache.crimson.parser.XMLReaderImpl");
267: SAXBuilder builder = new SourceLocatorSAXBuilder();
268: builder.setFactory(new SourceLocatorJDOMFactory());
269:
270: // ignore DOCTYPE declarations TODO [2004-25-05 ows]: parse
271: // entity references from internal declarations, and either
272: // warn about external declarations or add them to the
273: // dependency information. If the latter, use a library to
274: // cache and resolve non-file sources against a catalog file.
275: builder
276: .setEntityResolver(new org.xml.sax.helpers.DefaultHandler() {
277: public InputSource resolveEntity(String publicId,
278: String systemId) {
279: return new InputSource(new StringReader(""));
280: }
281: });
282:
283: // Parse the document
284: java.io.Reader reader = FileUtils.makeXMLReaderForFile(key
285: .getPath(), "UTF-8");
286: InputSource source = new InputSource(reader);
287: source.setPublicId(messagePathname);
288: source.setSystemId(key.getPath());
289: Document doc = builder.build(source);
290: reader.close();
291: fileCache.put(key, doc);
292: return doc;
293: }
294:
295: protected Document preprocess(final Document sourceDoc)
296: throws java.io.IOException, org.jdom.JDOMException {
297: // Fills location information from the metasource attribute.
298: class SourceLocatorHandler extends org.jdom.input.SAXHandler {
299: SourceLocatorHandler() throws IOException {
300: }
301:
302: SourceLocatorHandler(JDOMFactory factory)
303: throws IOException {
304: super (factory);
305: }
306:
307: public void endElement(String namespaceURI,
308: String localName, String qName) throws SAXException {
309: ElementWithLocationInfo element = (ElementWithLocationInfo) this
310: .getCurrentElement();
311: Attribute attr = element
312: .getAttribute(SourceLocatorSAXOutputter.SOURCEINFO_ATTRIBUTE_NAME);
313: if (attr != null) {
314: SourceLocator locator = SourceLocator
315: .fromString(attr.getValue());
316: element.initSourceLocator(locator);
317: element.removeAttribute(attr);
318: }
319: super .endElement(namespaceURI, localName, qName);
320: }
321: }
322:
323: // Create a transformer that implements the 'preprocess'
324: // transformation.
325: TransformerHandler handler;
326: try {
327: handler = getSaxFactory().newTransformerHandler(
328: sPreprocessorTemplates);
329: } catch (TransformerConfigurationException e) {
330: throw new ChainedException(e);
331: }
332:
333: SAXHandler resultHandler = new SourceLocatorHandler(
334: new SourceLocatorJDOMFactory());
335: SAXResult result = new javax.xml.transform.sax.SAXResult(
336: resultHandler);
337: handler.setResult(result);
338:
339: SourceLocatorSAXOutputter outputter = new SourceLocatorSAXOutputter();
340: outputter.setWriteMetaData(true);
341: outputter.setContentHandler(handler);
342: outputter.output(sourceDoc);
343:
344: Document resultDoc = resultHandler.getDocument();
345:
346: if (mPostTransformationLogger.isDebugEnabled()) {
347: org.jdom.output.XMLOutputter xmloutputter = new org.jdom.output.XMLOutputter();
348: mPostTransformationLogger.debug(xmloutputter
349: .outputString(resultDoc));
350: }
351:
352: return resultDoc;
353: }
354:
355: /** Reads the XML document, and modifies it by replacing each
356: * include element by the root of the expanded document named by
357: * the include's href attribute. Pathames are resolved relative to
358: * the element's source location. If a pathname resolves to the
359: * current file or a file in the set that's passed as the second
360: * argument, a compilation error is thrown. A copy of the set of
361: * pathnames that additionally includes the current file is passed
362: * to each recursive call. (Since the set has stacklike behavior,
363: * a cons list would be appropriate, but I don't think Java has
364: * one.)
365: *
366: * This is a helper function for parse(), which is factored out
367: * so that expandIncludes can be apply it recursively to included
368: * files. */
369: protected Document readExpanded(File file, Set currentFiles,
370: CompilationEnvironment env) throws IOException,
371: JDOMException {
372: File key = file.getCanonicalFile();
373: if (currentFiles.contains(key)) {
374: throw new CompilationError(
375: /* (non-Javadoc)
376: * @i18n.test
377: * @org-mes=p[0] + " includes itself."
378: */
379: org.openlaszlo.i18n.LaszloMessages.getMessage(Parser.class
380: .getName(), "051018-394", new Object[] { file }));
381: }
382: Set newCurrentFiles = new HashSet(currentFiles);
383: newCurrentFiles.add(key);
384: Document doc = read(file);
385: Element root = doc.getRootElement();
386: expandIncludes(root, newCurrentFiles, env);
387: return doc;
388: }
389:
390: static final String WHEN = "when";
391: static final String OTHERWISE = "otherwise";
392:
393: protected boolean evaluateConditions(Element element,
394: CompilationEnvironment env) {
395: if (element.getAttribute("runtime") != null
396: && !element.getAttributeValue("runtime").equals(
397: env.getRuntime())) {
398: return false;
399: }
400: return true;
401: }
402:
403: public static String xmltostring(Element e) {
404: org.jdom.output.XMLOutputter outputter = new org.jdom.output.XMLOutputter();
405: return outputter.outputString(e);
406: }
407:
408: protected void expandChildrenIncludes(Element element,
409: Set currentFiles, CompilationEnvironment env)
410: throws IOException, JDOMException {
411: for (Iterator iter = element.getChildren().iterator(); iter
412: .hasNext();) {
413: Element child = (Element) iter.next();
414: expandIncludes(child, currentFiles, env);
415: }
416: }
417:
418: /*
419: <switch>
420: <when runtime="swf8">
421: <view .../>
422: </when>
423: <otherwise >
424: <view .../>
425: </otherwise>
426: </switch>
427:
428: returns the child list of the active arm of the <switch>
429:
430: */
431: protected List evaluateSwitchStatement(Element elt,
432: CompilationEnvironment env) {
433: Element selected = null;
434: for (Iterator iter = elt.getChildren(WHEN, elt.getNamespace())
435: .iterator(); iter.hasNext();) {
436: Element when = (Element) iter.next();
437: if (evaluateConditions(when, env)) {
438: selected = when;
439: break;
440: }
441: }
442: if (selected == null) {
443: for (Iterator iter = elt.getChildren(OTHERWISE,
444: elt.getNamespace()).iterator(); iter.hasNext();) {
445: Element other = (Element) iter.next();
446: selected = other;
447: break;
448: }
449: }
450: if (selected == null) {
451: return new ArrayList();
452: } else {
453: return selected.cloneContent();
454: }
455: }
456:
457: // Makes a copy of the element's child list.
458: protected List copyChildList(Element element) {
459: ArrayList children = new ArrayList();
460: for (Iterator iter = element.getChildren().iterator(); iter
461: .hasNext();) {
462: Element child = (Element) iter.next();
463: children.add(child);
464: }
465: return children;
466: }
467:
468: /** Replaces include statements by the content of the included
469: * file.
470: *
471: * This is a helper function for readExpanded, which is factored
472: * out so that readExpanded can apply it recursively to included
473: * files. */
474: protected void expandIncludes(Element element, Set currentFiles,
475: CompilationEnvironment env) throws IOException,
476: JDOMException {
477: // Copy the child list, to avoid the concurrentModificationError problem
478: // we get if we use the getChildren list directly.
479: List children = copyChildList(element);
480:
481: // expand (replace) any <switch> elements with only the selected arm of the
482: // <switch>
483: for (Iterator iter = children.iterator(); iter.hasNext();) {
484: Element child = (Element) iter.next();
485: if (child.getName().equals("switch")) {
486: List goodies = evaluateSwitchStatement(child, env);
487: // splice these in place of the <switch>
488: int index = element.indexOf(child);
489: element.setContent(index, goodies);
490: }
491: }
492:
493: // Get a new copy of the children list, with expanded <switch> elements from above.
494: children = copyChildList(element);
495:
496: for (Iterator iter = children.iterator(); iter.hasNext();) {
497: Element child = (Element) iter.next();
498:
499: if (child.getName().equals("include")) {
500: String base = new File(getSourcePathname(element))
501: .getParent();
502: String type = XMLUtils.getAttributeValue(child, "type",
503: "xml");
504: String href = child.getAttributeValue("href");
505: if (href == null) {
506: throw new CompilationError(
507: /* (non-Javadoc)
508: * @i18n.test
509: * @org-mes="The <include> element requires an \"href\" attribute."
510: */
511: org.openlaszlo.i18n.LaszloMessages.getMessage(
512: Parser.class.getName(), "051018-438"),
513: child);
514: }
515: File target = resolver.resolve(href, base, true);
516: // If this file is already implicitly included, just
517: // leave it, don't try to expand it. It will be
518: // skipped at the compilation stage.
519: if (resolver.getBinaryIncludes().contains(target)) {
520: continue;
521: }
522: if (type.equals("text")) {
523: List content = element.getContent();
524: int index = content.indexOf(child);
525: content.set(index, new org.jdom.Text(FileUtils
526: .readFileString(target, "UTF-8")));
527: } else if (type.equals("xml")) {
528: // Pass the target, not the key, so that source
529: // location information is correct.
530: //System.err.println("including xml "+target);
531: Document doc = read(target);
532: // If it's a top-level library, the compiler will
533: // process it during the compilation phase. In
534: // that case change it to a <library href=""/>
535: // element, where the href is the <include>'s
536: // href, so that LibraryCompiler can resolve it.
537: //
538: // Otherwise replace the <include> element with
539: // the included file.
540: if (CompilerUtils.isAtToplevel(child)
541: && LibraryCompiler.isElement(doc
542: .getRootElement())) {
543: // Modify the existing element instead of
544: // creating a new one, so that source location
545: // information is preserved.
546: child.setName(doc.getRootElement().getName());
547: } else {
548: doc = readExpanded(target, currentFiles, env);
549:
550: // replace the <include> child element with the expanded file contents
551: List content = element.getContent();
552: int index = content.indexOf(child);
553: Element root = doc.getRootElement();
554: root.detach();
555: content.set(index, root);
556:
557: File key = target.getCanonicalFile();
558: fileCache.remove(key);
559: }
560: } else {
561: throw new CompilationError(
562: /* (non-Javadoc)
563: * @i18n.test
564: * @org-mes="include type must be xml or text"
565: */
566: org.openlaszlo.i18n.LaszloMessages.getMessage(
567: Parser.class.getName(), "051018-485"));
568: }
569: } else {
570: expandIncludes(child, currentFiles, env);
571: }
572: }
573: }
574:
575: /** Reads an XML file, expands includes, and validates it.
576: */
577: public Document parse(File file, CompilationEnvironment env)
578: throws CompilationError {
579: String pathname = file.getPath();
580: try {
581: Document doc = readExpanded(file, new HashSet(), env);
582: // Apply the stylesheet
583: doc = preprocess(doc);
584: return doc;
585: } catch (IOException e) {
586: CompilationError ce = new CompilationError(e);
587: ce.initPathname(pathname);
588: throw ce;
589: } catch (JDOMException e) {
590: String solution = SolutionMessages.findSolution(e
591: .getMessage(), SolutionMessages.PARSER);
592: CompilationError ce = new CompilationError(e, solution);
593: throw ce;
594:
595: }
596: }
597:
598: /** Cache of compiled schema verifiers. Type Map<String,
599: * org.iso_relax.verifier.Schema>, where the key is the string
600: * serialization of the schema. */
601: private static LRUMap mSchemaCache = new LRUMap(1);
602:
603: void saveStartLocation(Element elt, String pathname,
604: String messagePathname, int startLineNumber,
605: int startColumnNumber) {
606: SourceLocator info = ((ElementWithLocationInfo) elt).locator;
607: info.setPathname(pathname, messagePathname);
608: info.startLineNumber = startLineNumber;
609: info.startColumnNumber = startColumnNumber;
610: }
611:
612: void saveEndLocation(Element elt, String pathname,
613: String messagePathname, int endLineNumber,
614: int endColumnNumber) {
615: SourceLocator info = ((ElementWithLocationInfo) elt).locator;
616: info.setPathname(pathname, messagePathname);
617: info.endLineNumber = endLineNumber;
618: info.endColumnNumber = endColumnNumber;
619: }
620:
621: /* Implement source location, on top of metadata */
622: static final int LINENO = 1;
623: static final int COLNO = 2;
624:
625: static String getSourcePathname(Element elt) {
626: SourceLocator info = ((ElementWithLocationInfo) elt).locator;
627: return info.pathname;
628: }
629:
630: static String getSourceMessagePathname(Element elt) {
631: SourceLocator info = ((ElementWithLocationInfo) elt).locator;
632: return info.messagePathname;
633: }
634:
635: static Integer getSourceLocation(Element elt, int coord,
636: boolean start) {
637: if (elt == null) {
638: return null;
639: // +++ should we throw an error if elt == null?
640: }
641:
642: SourceLocator info = ((ElementWithLocationInfo) elt).locator;
643:
644: if (coord == LINENO) {
645: return new Integer(start ? info.startLineNumber
646: : info.endLineNumber);
647: } else {
648: return new Integer(start ? info.startColumnNumber
649: : info.endColumnNumber);
650: }
651: }
652:
653: static Integer getSourceLocation(Element elt, int coord) {
654: return getSourceLocation(elt, coord, true);
655: }
656:
657: class SourceLocatorJDOMFactory extends org.jdom.DefaultJDOMFactory {
658:
659: public SourceLocatorJDOMFactory() {
660: super ();
661: }
662:
663: public Element element(String name) {
664: return new ElementWithLocationInfo(name);
665: }
666:
667: public Element element(String name, Namespace namespace) {
668: return new ElementWithLocationInfo(name, namespace);
669: }
670:
671: public Element element(String name, String uri) {
672: return new ElementWithLocationInfo(name, uri);
673: }
674:
675: public Element element(String name, String prefix, String uri) {
676: return new ElementWithLocationInfo(name, prefix, uri);
677: }
678: }
679: }
|