001: // Copyright 2006, 2007 The Apache Software Foundation
002: //
003: // Licensed under the Apache License, Version 2.0 (the "License");
004: // you may not use this file except in compliance with the License.
005: // You may obtain a copy of the License at
006: //
007: // http://www.apache.org/licenses/LICENSE-2.0
008: //
009: // Unless required by applicable law or agreed to in writing, software
010: // distributed under the License is distributed on an "AS IS" BASIS,
011: // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: // See the License for the specific language governing permissions and
013: // limitations under the License.
014:
015: package org.apache.tapestry.internal.services;
016:
017: import static org.apache.tapestry.ioc.IOCConstants.PERTHREAD_SCOPE;
018: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newList;
019: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newSet;
020:
021: import java.io.IOException;
022: import java.net.URL;
023: import java.util.List;
024: import java.util.Map;
025: import java.util.Set;
026: import java.util.regex.Matcher;
027: import java.util.regex.Pattern;
028:
029: import org.apache.commons.logging.Log;
030: import org.apache.tapestry.internal.parser.AttributeToken;
031: import org.apache.tapestry.internal.parser.BlockToken;
032: import org.apache.tapestry.internal.parser.BodyToken;
033: import org.apache.tapestry.internal.parser.CDATAToken;
034: import org.apache.tapestry.internal.parser.CommentToken;
035: import org.apache.tapestry.internal.parser.ComponentTemplate;
036: import org.apache.tapestry.internal.parser.ComponentTemplateImpl;
037: import org.apache.tapestry.internal.parser.DTDToken;
038: import org.apache.tapestry.internal.parser.EndElementToken;
039: import org.apache.tapestry.internal.parser.ExpansionToken;
040: import org.apache.tapestry.internal.parser.ParameterToken;
041: import org.apache.tapestry.internal.parser.StartComponentToken;
042: import org.apache.tapestry.internal.parser.StartElementToken;
043: import org.apache.tapestry.internal.parser.TemplateToken;
044: import org.apache.tapestry.internal.parser.TextToken;
045: import org.apache.tapestry.ioc.Location;
046: import org.apache.tapestry.ioc.Resource;
047: import org.apache.tapestry.ioc.annotations.Scope;
048: import org.apache.tapestry.ioc.internal.util.InternalUtils;
049: import org.apache.tapestry.ioc.internal.util.LocationImpl;
050: import org.apache.tapestry.ioc.internal.util.TapestryException;
051: import org.xml.sax.Attributes;
052: import org.xml.sax.ContentHandler;
053: import org.xml.sax.EntityResolver;
054: import org.xml.sax.InputSource;
055: import org.xml.sax.Locator;
056: import org.xml.sax.SAXException;
057: import org.xml.sax.XMLReader;
058: import org.xml.sax.ext.LexicalHandler;
059: import org.xml.sax.helpers.XMLReaderFactory;
060:
061: /**
062: * Non-threadsafe implementation; the IOC service uses the perthread lifecycle.
063: */
064: @Scope(PERTHREAD_SCOPE)
065: public class TemplateParserImpl implements TemplateParser,
066: LexicalHandler, ContentHandler, EntityResolver {
067: private static final String MIXINS_ATTRIBUTE_NAME = "mixins";
068:
069: private static final String TYPE_ATTRIBUTE_NAME = "type";
070:
071: private static final String ID_ATTRIBUTE_NAME = "id";
072:
073: public static final String TAPESTRY_SCHEMA_5_0_0 = "http://tapestry.apache.org/schema/tapestry_5_0_0.xsd";
074:
075: private XMLReader _reader;
076:
077: // Resource being parsed
078: private Resource _templateResource;
079:
080: private Locator _locator;
081:
082: private final List<TemplateToken> _tokens = newList();
083:
084: // Non-blank ids from start component (<comp>) elements
085:
086: private final Set<String> _componentIds = newSet();
087:
088: // Used to accumulate text provided by the characters(). Even contiguous characters may be
089: // broken up across multiple invocations due to parser internals. We accumulate those together
090: // before forming a text token.
091:
092: private final StringBuilder _textBuffer = new StringBuilder();
093:
094: private Location _textStartLocation;
095:
096: private boolean _textIsCData;
097:
098: private boolean _insideBody;
099:
100: private boolean _insideBodyErrorLogged;
101:
102: private boolean _ignoreEvents;
103:
104: private final Log _log;
105:
106: private final Map<String, URL> _configuration;
107:
108: // Note the use of the non-greedy modifier; this prevents the pattern from merging multiple
109: // expansions on the same text line into a single large
110: // but invalid expansion.
111:
112: private final Pattern EXPANSION_PATTERN = Pattern.compile(
113: "\\$\\{\\s*(.*?)\\s*}", Pattern.MULTILINE);
114:
115: public TemplateParserImpl(Log log, Map<String, URL> configuration) {
116: _log = log;
117: _configuration = configuration;
118:
119: reset();
120: }
121:
122: private void reset() {
123: _tokens.clear();
124: _componentIds.clear();
125: _templateResource = null;
126: _locator = null;
127: _textBuffer.setLength(0);
128: _textStartLocation = null;
129: _textIsCData = false;
130: _insideBody = false;
131: _insideBodyErrorLogged = false;
132: _ignoreEvents = true;
133: }
134:
135: public ComponentTemplate parseTemplate(Resource templateResource) {
136: if (_reader == null) {
137: try {
138: _reader = XMLReaderFactory.createXMLReader();
139:
140: _reader.setContentHandler(this );
141:
142: _reader.setEntityResolver(this );
143:
144: _reader
145: .setFeature(
146: "http://xml.org/sax/features/namespace-prefixes",
147: true);
148:
149: _reader
150: .setProperty(
151: "http://xml.org/sax/properties/lexical-handler",
152: this );
153: } catch (Exception ex) {
154: throw new RuntimeException(ServicesMessages
155: .newParserError(templateResource, ex), ex);
156: }
157: }
158:
159: URL resourceURL = templateResource.toURL();
160:
161: if (resourceURL == null)
162: throw new RuntimeException(ServicesMessages
163: .missingTemplateResource(templateResource));
164:
165: _templateResource = templateResource;
166:
167: try {
168: InputSource source = new InputSource(resourceURL
169: .openStream());
170:
171: _reader.parse(source);
172:
173: return new ComponentTemplateImpl(_templateResource,
174: _tokens, _componentIds);
175: } catch (Exception ex) {
176: // Some parsers get in an unknown state when an error occurs, and are are not
177: // subsequently useable.
178:
179: _reader = null;
180:
181: throw new TapestryException(ServicesMessages
182: .templateParseError(templateResource, ex),
183: getCurrentLocation(), ex);
184: } finally {
185: reset();
186: }
187: }
188:
189: public void setDocumentLocator(Locator locator) {
190: _locator = locator;
191: }
192:
193: /** Accumulates the characters into a text buffer. */
194: public void characters(char[] ch, int start, int length)
195: throws SAXException {
196: if (_ignoreEvents)
197: return;
198:
199: if (insideBody())
200: return;
201:
202: if (_textBuffer.length() == 0)
203: _textStartLocation = getCurrentLocation();
204:
205: _textBuffer.append(ch, start, length);
206: }
207:
208: /**
209: * Adds tokens corresponding to the content in the text buffer. For a non-CDATA section, we also
210: * search for expansions (thus we may add more than one token). Clears the text buffer.
211: */
212: private void processTextBuffer() {
213: if (_textBuffer.length() == 0)
214: return;
215:
216: String text = _textBuffer.toString();
217:
218: if (_textIsCData) {
219: _tokens.add(new CDATAToken(text, _textStartLocation));
220: } else {
221: addTokensForText(text);
222: }
223:
224: _textBuffer.setLength(0);
225: }
226:
227: /**
228: * Scans the text, using a regular expression pattern, for expansion patterns, and adds
229: * appropriate tokens for what it finds.
230: *
231: * @param text
232: */
233: private void addTokensForText(String text) {
234: Matcher matcher = EXPANSION_PATTERN.matcher(text);
235:
236: int startx = 0;
237:
238: // The big problem with all this code is that everything gets assigned to the
239: // start of the text block, even if there are line breaks leading up to it.
240: // That's going to take a lot more work and there are bigger fish to fry.
241:
242: while (matcher.find()) {
243: int matchStart = matcher.start();
244:
245: if (matchStart != startx) {
246: String prefix = text.substring(startx, matchStart);
247:
248: _tokens.add(new TextToken(prefix, _textStartLocation));
249: }
250:
251: // Group 1 includes the real text of the expansion, which whitespace around the
252: // expression (but inside the curly
253: // braces) excluded.
254:
255: String expression = matcher.group(1);
256:
257: _tokens.add(new ExpansionToken(expression,
258: _textStartLocation));
259:
260: startx = matcher.end();
261: }
262:
263: // Catch anything after the final regexp match.
264:
265: if (startx < text.length())
266: _tokens.add(new TextToken(text.substring(startx, text
267: .length()), _textStartLocation));
268: }
269:
270: public void startElement(String uri, String localName,
271: String qName, Attributes attributes) throws SAXException {
272: _ignoreEvents = false;
273:
274: if (_insideBody)
275: throw new IllegalStateException(ServicesMessages
276: .mayNotNestElementsInsideBody(localName));
277:
278: // Add any accumulated text into a text token
279: processTextBuffer();
280:
281: if (TAPESTRY_SCHEMA_5_0_0.equals(uri)) {
282: startTapestryElement(qName, localName, attributes);
283: return;
284: }
285:
286: // TODO: Handle interpolations inside attributes?
287:
288: startPossibleComponent(attributes, localName, null);
289: }
290:
291: /**
292: * Checks to see if currently inside a t:body element (which should always be empty). Content is
293: * ignored inside a body. If inside a body, then a warning is logged (but only one warning per
294: * body element).
295: *
296: * @return true if inside t:body, false otherwise
297: */
298: private boolean insideBody() {
299: if (_insideBody) {
300: // Limit to one logged error per infraction.
301:
302: if (!_insideBodyErrorLogged)
303: _log
304: .error(ServicesMessages
305: .contentInsideBodyNotAllowed(getCurrentLocation()));
306:
307: _insideBodyErrorLogged = true;
308: }
309:
310: return _insideBody;
311: }
312:
313: private void startTapestryElement(String qname, String localName,
314: Attributes attributes) {
315: if (localName.equalsIgnoreCase("body")) {
316: startBody();
317: return;
318: }
319:
320: if (localName.equalsIgnoreCase("parameter")) {
321: startParameter(attributes);
322: return;
323: }
324:
325: if (localName.equalsIgnoreCase("block")) {
326: startBlock(attributes);
327: return;
328: }
329:
330: // The component type is derived from the element name. Since element names may not contain
331: // slashes, we convert periods to slashes. Later down the pipeline, they'll probably be
332: // converted back into periods, as part of a fully qualified class name.
333:
334: String componentType = localName.replace('.', '/');
335:
336: // With a component type specified, it's not just possibly a component ...
337: startPossibleComponent(attributes, null, componentType);
338: }
339:
340: private void startBlock(Attributes attributes) {
341: String blockId = findSingleParameter("block", "id", attributes);
342:
343: // null is ok for blockId
344:
345: _tokens.add(new BlockToken(blockId, getCurrentLocation()));
346: }
347:
348: private void startParameter(Attributes attributes) {
349: String parameterName = findSingleParameter("parameter", "name",
350: attributes);
351:
352: if (InternalUtils.isBlank(parameterName))
353: throw new TapestryException(ServicesMessages
354: .parameterElementNameRequired(),
355: getCurrentLocation(), null);
356:
357: _tokens.add(new ParameterToken(parameterName,
358: getCurrentLocation()));
359: }
360:
361: private String findSingleParameter(String elementName,
362: String attributeName, Attributes attributes) {
363: String result = null;
364:
365: for (int i = 0; i < attributes.getLength(); i++) {
366: String name = attributes.getLocalName(i);
367:
368: if (name.equals(attributeName)) {
369: result = attributes.getValue(i);
370: continue;
371: }
372:
373: // Only the name attribute is allowed.
374:
375: throw new TapestryException(ServicesMessages
376: .undefinedTapestryAttribute(elementName, name,
377: attributeName), getCurrentLocation(), null);
378: }
379:
380: return result;
381: }
382:
383: private String nullForBlank(String input) {
384: return InternalUtils.isBlank(input) ? null : input;
385: }
386:
387: /**
388: * @param attributes
389: * the attributes for the element
390: * @param elementName
391: * the name of the element (to be assigned to the new token), may be null for a
392: * component in the Tapestry namespace
393: * @param identifiedType
394: * the type of the element, usually null, but may be the component type derived from
395: * the element name (for an element in the Tapestry namespace)
396: */
397: private void startPossibleComponent(Attributes attributes,
398: String elementName, String identifiedType) {
399: String id = null;
400: String type = identifiedType;
401: String mixins = null;
402: int count = attributes.getLength();
403: Location location = getCurrentLocation();
404: List<TemplateToken> attributeTokens = newList();
405:
406: for (int i = 0; i < count; i++) {
407: String name = attributes.getLocalName(i);
408:
409: // The name will be blank for an xmlns: attribute
410:
411: if (InternalUtils.isBlank(name))
412: continue;
413:
414: String uri = attributes.getURI(i);
415:
416: String value = attributes.getValue(i);
417:
418: if (TAPESTRY_SCHEMA_5_0_0.equals(uri)) {
419: if (name.equalsIgnoreCase(ID_ATTRIBUTE_NAME)) {
420: id = nullForBlank(value);
421: continue;
422: }
423:
424: if (type == null
425: && name.equalsIgnoreCase(TYPE_ATTRIBUTE_NAME)) {
426: type = nullForBlank(value);
427: continue;
428: }
429:
430: if (name.equalsIgnoreCase(MIXINS_ATTRIBUTE_NAME)) {
431: mixins = nullForBlank(value);
432: continue;
433: }
434:
435: // Anything else is the name of a Tapestry component parameter that is simply
436: // not part of the template's doctype for the element being instrumented.
437: }
438:
439: attributeTokens.add(new AttributeToken(name, value,
440: location));
441: }
442:
443: boolean isComponent = (id != null || type != null);
444:
445: // If provided t:mixins but not t:id or t:type, then its not quite a component
446:
447: if (mixins != null && !isComponent)
448: throw new TapestryException(ServicesMessages
449: .mixinsInvalidWithoutIdOrType(elementName),
450: location, null);
451:
452: if (isComponent) {
453: _tokens.add(new StartComponentToken(elementName, id, type,
454: mixins, location));
455: } else {
456: _tokens.add(new StartElementToken(elementName, location));
457: }
458:
459: _tokens.addAll(attributeTokens);
460:
461: if (id != null)
462: _componentIds.add(id);
463: }
464:
465: private void startBody() {
466: _tokens.add(new BodyToken(getCurrentLocation()));
467:
468: _insideBody = true;
469: _insideBodyErrorLogged = false;
470: }
471:
472: public void endElement(String uri, String localName, String qName)
473: throws SAXException {
474: processTextBuffer();
475:
476: // TODO: Handle tapestry namespace elements?
477:
478: // Because XML tags are always balanced, we don't even need to know what element just closed
479: // when we assemble things later.
480:
481: if (!_insideBody)
482: _tokens.add(new EndElementToken(getCurrentLocation()));
483:
484: _insideBody = false;
485: }
486:
487: private Location getCurrentLocation() {
488: if (_locator == null)
489: return null;
490:
491: return new LocationImpl(_templateResource, _locator
492: .getLineNumber(), _locator.getColumnNumber());
493: }
494:
495: public void comment(char[] ch, int start, int length)
496: throws SAXException {
497: if (_ignoreEvents || insideBody())
498: return;
499:
500: processTextBuffer();
501:
502: // Remove excess whitespace. The Comment DOM node will add a leadig and trailing space.
503:
504: String comment = new String(ch, start, length).trim();
505:
506: // TODO: Perhaps comments need to be "aggregated" the same way we aggregate text and CDATA.
507: // Hm. Probably not. Any whitespace between one comment and the next will become a
508: // TextToken.
509: // Unless we trim whitespace between consecutive comments ... and on down the rabbit hole.
510: // Oops -- unless a single comment may be passed into this method as multiple calls
511: // (have to check how multiline comments are handled).
512: // Tests against Sun's built in parser does show that multiline comments are still
513: // provided as a single call to comment(), so we're good for the meantime (until we find
514: // out some parsers aren't so compliant).
515:
516: _tokens.add(new CommentToken(comment, getCurrentLocation()));
517: }
518:
519: public void endCDATA() throws SAXException {
520: // Add a token for any accumulated CDATA.
521:
522: processTextBuffer();
523:
524: // Again, CDATA doesn't nest, so we know we're back to ordinary markup.
525:
526: _textIsCData = false;
527: }
528:
529: public void startCDATA() throws SAXException {
530: if (_ignoreEvents || insideBody())
531: return;
532:
533: processTextBuffer();
534:
535: // Because CDATA doesn't mix with any other SAX/lexical events, we can simply turn on a flag
536: // here and turn it off when we see the end.
537:
538: _textIsCData = true;
539: }
540:
541: // Empty methods defined by the various interfaces.
542:
543: public void endDTD() throws SAXException {
544: }
545:
546: public void endEntity(String name) throws SAXException {
547: }
548:
549: public void startDTD(String name, String publicId, String systemId)
550: throws SAXException {
551: // notes:
552: // 1) a DTD has to occur at the very start of a document. Since we don't start
553: // recording characters until we hit the first element of a document (see
554: // characters and startElement), there should be no text to process.
555: // It's worth noting that the sax parser will puke if any of the following
556: // occur:
557: // 1) a doctype is encountered multiple times in the same document
558: // 2) a doctype is encountered anywhere other than the very first item
559: // in a document.
560: // Hence, the assumption made in 1 should hold.
561: // Since an exception is thrown for case #1 above, we can just add the DTDToken.
562: // When we go to process the token (in PageLoaderProcessor), we can make sure
563: // that the final page has only a single DTDToken (the first one).
564: _tokens.add(new DTDToken(name, publicId, systemId,
565: getCurrentLocation()));
566: }
567:
568: public void startEntity(String name) throws SAXException {
569: }
570:
571: public void endDocument() throws SAXException {
572: }
573:
574: public void endPrefixMapping(String prefix) throws SAXException {
575: }
576:
577: public void ignorableWhitespace(char[] ch, int start, int length)
578: throws SAXException {
579: }
580:
581: public void processingInstruction(String target, String data)
582: throws SAXException {
583: }
584:
585: public void skippedEntity(String name) throws SAXException {
586: }
587:
588: public void startDocument() throws SAXException {
589: }
590:
591: public void startPrefixMapping(String prefix, String uri)
592: throws SAXException {
593: }
594:
595: public InputSource resolveEntity(String publicId, String systemId)
596: throws SAXException, IOException {
597: URL url = _configuration.get(publicId);
598:
599: if (url != null)
600: return new InputSource(url.openStream());
601:
602: return null;
603: }
604:
605: }
|