001: /*
002: * Copyright 2002,2004 The Apache Software Foundation.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.apache.commons.jelly.impl;
017:
018: import java.io.IOException;
019: import java.lang.reflect.InvocationTargetException;
020: import java.net.MalformedURLException;
021: import java.net.URL;
022: import java.util.Collections;
023: import java.util.Hashtable;
024: import java.util.Iterator;
025: import java.util.Map;
026: import java.util.WeakHashMap;
027:
028: import org.apache.commons.beanutils.ConvertingWrapDynaBean;
029: import org.apache.commons.beanutils.ConvertUtils;
030: import org.apache.commons.beanutils.DynaBean;
031: import org.apache.commons.beanutils.DynaProperty;
032:
033: import org.apache.commons.jelly.CompilableTag;
034: import org.apache.commons.jelly.JellyContext;
035: import org.apache.commons.jelly.JellyException;
036: import org.apache.commons.jelly.JellyTagException;
037: import org.apache.commons.jelly.DynaTag;
038: import org.apache.commons.jelly.LocationAware;
039: import org.apache.commons.jelly.NamespaceAwareTag;
040: import org.apache.commons.jelly.Script;
041: import org.apache.commons.jelly.Tag;
042: import org.apache.commons.jelly.XMLOutput;
043: import org.apache.commons.jelly.expression.Expression;
044:
045: import org.apache.commons.logging.Log;
046: import org.apache.commons.logging.LogFactory;
047:
048: import org.xml.sax.Attributes;
049: import org.xml.sax.Locator;
050: import org.xml.sax.SAXException;
051:
052: /**
053: * <p><code>TagScript</code> is a Script that evaluates a custom tag.</p>
054: *
055: * <b>Note</b> that this class should be re-entrant and used
056: * concurrently by multiple threads.
057: *
058: * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
059: * @version $Revision: 165507 $
060: */
061: public class TagScript implements Script {
062:
063: /** The Log to which logging calls will be made. */
064: private static final Log log = LogFactory.getLog(TagScript.class);
065:
066: /** The attribute expressions that are created */
067: protected Map attributes = new Hashtable();
068:
069: /** the optional namespaces Map of prefix -> URI of this single Tag */
070: private Map tagNamespacesMap;
071:
072: /**
073: * The optional namespace context mapping all prefixes -> URIs in scope
074: * at the point this tag is used.
075: * This Map is only created lazily if it is required by the NamespaceAwareTag.
076: */
077: private Map namespaceContext;
078:
079: /** the Jelly file which caused the problem */
080: private String fileName;
081:
082: /** the qualified element name which caused the problem */
083: private String elementName;
084:
085: /** the local (non-namespaced) tag name */
086: private String localName;
087:
088: /** the line number of the tag */
089: private int lineNumber = -1;
090:
091: /** the column number of the tag */
092: private int columnNumber = -1;
093:
094: /** the factory of Tag instances */
095: private TagFactory tagFactory;
096:
097: /** the body script used for this tag */
098: private Script tagBody;
099:
100: /** the parent TagScript */
101: private TagScript parent;
102:
103: /** the SAX attributes */
104: private Attributes saxAttributes;
105:
106: /** the url of the script when parsed */
107: private URL scriptURL = null;
108:
109: /** A synchronized WeakHashMap from the current Thread (key) to a Tag object (value).
110: */
111: private Map threadLocalTagCache = Collections
112: .synchronizedMap(new WeakHashMap());
113:
114: /**
115: * @return a new TagScript based on whether
116: * the given Tag class is a bean tag or DynaTag
117: */
118: public static TagScript newInstance(Class tagClass) {
119: TagFactory factory = new DefaultTagFactory(tagClass);
120: return new TagScript(factory);
121: }
122:
123: public TagScript() {
124: }
125:
126: public TagScript(TagFactory tagFactory) {
127: this .tagFactory = tagFactory;
128: }
129:
130: public String toString() {
131: return super .toString() + "[tag=" + elementName + ";at="
132: + lineNumber + ":" + columnNumber + "]";
133: }
134:
135: /**
136: * Compiles the tags body
137: */
138: public Script compile() throws JellyException {
139: if (tagBody != null) {
140: tagBody = tagBody.compile();
141: }
142: return this ;
143: }
144:
145: /**
146: * Sets the optional namespaces prefix -> URI map of
147: * the namespaces attached to this Tag
148: */
149: public void setTagNamespacesMap(Map tagNamespacesMap) {
150: // lets check that this is a thread-safe map
151: if (!(tagNamespacesMap instanceof Hashtable)) {
152: tagNamespacesMap = new Hashtable(tagNamespacesMap);
153: }
154: this .tagNamespacesMap = tagNamespacesMap;
155: }
156:
157: /**
158: * Configures this TagScript from the SAX Locator, setting the column
159: * and line numbers
160: */
161: public void setLocator(Locator locator) {
162: setLineNumber(locator.getLineNumber());
163: setColumnNumber(locator.getColumnNumber());
164: }
165:
166: /** Add an initialization attribute for the tag.
167: * This method must be called after the setTag() method
168: */
169: public void addAttribute(String name, Expression expression) {
170: if (log.isDebugEnabled()) {
171: log.debug("adding attribute name: " + name
172: + " expression: " + expression);
173: }
174: attributes.put(name, expression);
175: }
176:
177: /**
178: * Strips off the name of a script to create a new context URL
179: * FIXME: Copied from JellyContext
180: */
181: private URL getJellyContextURL(URL url)
182: throws MalformedURLException {
183: String text = url.toString();
184: int idx = text.lastIndexOf('/');
185: text = text.substring(0, idx + 1);
186: return new URL(text);
187: }
188:
189: // Script interface
190: //-------------------------------------------------------------------------
191:
192: /** Evaluates the body of a tag */
193: public void run(JellyContext context, XMLOutput output)
194: throws JellyTagException {
195: URL rootURL = context.getRootURL();
196: URL currentURL = context.getCurrentURL();
197: try {
198: Tag tag = getTag(context);
199: if (tag == null) {
200: return;
201: }
202: tag.setContext(context);
203: setContextURLs(context);
204:
205: if (tag instanceof DynaTag) {
206: DynaTag dynaTag = (DynaTag) tag;
207:
208: // ### probably compiling this to 2 arrays might be quicker and smaller
209: for (Iterator iter = attributes.entrySet().iterator(); iter
210: .hasNext();) {
211: Map.Entry entry = (Map.Entry) iter.next();
212: String name = (String) entry.getKey();
213: Expression expression = (Expression) entry
214: .getValue();
215:
216: Class type = dynaTag.getAttributeType(name);
217: Object value = null;
218: if (type != null
219: && type.isAssignableFrom(Expression.class)
220: && !type.isAssignableFrom(Object.class)) {
221: value = expression;
222: } else {
223: value = expression.evaluateRecurse(context);
224: }
225: dynaTag.setAttribute(name, value);
226: }
227: } else {
228: // treat the tag as a bean
229: DynaBean dynaBean = new ConvertingWrapDynaBean(tag);
230: for (Iterator iter = attributes.entrySet().iterator(); iter
231: .hasNext();) {
232: Map.Entry entry = (Map.Entry) iter.next();
233: String name = (String) entry.getKey();
234: Expression expression = (Expression) entry
235: .getValue();
236:
237: DynaProperty property = dynaBean.getDynaClass()
238: .getDynaProperty(name);
239: if (property == null) {
240: throw new JellyException(
241: "This tag does not understand the '"
242: + name + "' attribute");
243: }
244: Class type = property.getType();
245:
246: Object value = null;
247: if (type.isAssignableFrom(Expression.class)
248: && !type.isAssignableFrom(Object.class)) {
249: value = expression;
250: } else {
251: value = expression.evaluateRecurse(context);
252: }
253: dynaBean.set(name, value);
254: }
255: }
256:
257: tag.doTag(output);
258: if (output != null) {
259: output.flush();
260: }
261: } catch (JellyTagException e) {
262: handleException(e);
263: } catch (JellyException e) {
264: handleException(e);
265: } catch (IOException e) {
266: handleException(e);
267: } catch (RuntimeException e) {
268: handleException(e);
269: } catch (Error e) {
270: /*
271: * Not sure if we should be converting errors to exceptions,
272: * but not trivial to remove because JUnit tags throw
273: * Errors in the normal course of operation. Hmm...
274: */
275: handleException(e);
276: } finally {
277: context.setRootURL(rootURL);
278: context.setCurrentURL(currentURL);
279: }
280:
281: }
282:
283: /**
284: * Set the context's root and current URL if not present
285: * @param context
286: * @throws JellyTagException
287: */
288: protected void setContextURLs(JellyContext context)
289: throws JellyTagException {
290: if ((context.getCurrentURL() == null || context.getRootURL() == null)
291: && scriptURL != null) {
292: if (context.getRootURL() == null)
293: context.setRootURL(scriptURL);
294: if (context.getCurrentURL() == null)
295: context.setCurrentURL(scriptURL);
296: }
297: }
298:
299: // Properties
300: //-------------------------------------------------------------------------
301:
302: /**
303: * @return the tag to be evaluated, creating it lazily if required.
304: */
305: public Tag getTag(JellyContext context) throws JellyException {
306: Thread t = Thread.currentThread();
307: Tag tag = (Tag) threadLocalTagCache.get(t);
308: if (tag == null) {
309: tag = createTag();
310: if (tag != null) {
311: threadLocalTagCache.put(t, tag);
312: configureTag(tag, context);
313: }
314: }
315: return tag;
316: }
317:
318: /**
319: * Returns the Factory of Tag instances.
320: * @return the factory
321: */
322: public TagFactory getTagFactory() {
323: return tagFactory;
324: }
325:
326: /**
327: * Sets the Factory of Tag instances.
328: * @param tagFactory The factory to set
329: */
330: public void setTagFactory(TagFactory tagFactory) {
331: this .tagFactory = tagFactory;
332: }
333:
334: /**
335: * Returns the parent.
336: * @return TagScript
337: */
338: public TagScript getParent() {
339: return parent;
340: }
341:
342: /**
343: * Returns the tagBody.
344: * @return Script
345: */
346: public Script getTagBody() {
347: return tagBody;
348: }
349:
350: /**
351: * Sets the parent.
352: * @param parent The parent to set
353: */
354: public void setParent(TagScript parent) {
355: this .parent = parent;
356: }
357:
358: /**
359: * Sets the tagBody.
360: * @param tagBody The tagBody to set
361: */
362: public void setTagBody(Script tagBody) {
363: this .tagBody = tagBody;
364: }
365:
366: /**
367: * @return the Jelly file which caused the problem
368: */
369: public String getFileName() {
370: return fileName;
371: }
372:
373: /**
374: * Sets the Jelly file which caused the problem
375: */
376: public void setFileName(String fileName) {
377: this .fileName = fileName;
378: try {
379: this .scriptURL = getJellyContextURL(new URL(fileName));
380: } catch (MalformedURLException e) {
381: log.debug("error setting script url", e);
382: }
383: }
384:
385: /**
386: * @return the element name which caused the problem
387: */
388: public String getElementName() {
389: return elementName;
390: }
391:
392: /**
393: * Sets the element name which caused the problem
394: */
395: public void setElementName(String elementName) {
396: this .elementName = elementName;
397: }
398:
399: /**
400: * @return the line number of the tag
401: */
402: public int getLineNumber() {
403: return lineNumber;
404: }
405:
406: /**
407: * Sets the line number of the tag
408: */
409: public void setLineNumber(int lineNumber) {
410: this .lineNumber = lineNumber;
411: }
412:
413: /**
414: * @return the column number of the tag
415: */
416: public int getColumnNumber() {
417: return columnNumber;
418: }
419:
420: /**
421: * Sets the column number of the tag
422: */
423: public void setColumnNumber(int columnNumber) {
424: this .columnNumber = columnNumber;
425: }
426:
427: /**
428: * Returns the SAX attributes of this tag
429: * @return Attributes
430: */
431: public Attributes getSaxAttributes() {
432: return saxAttributes;
433: }
434:
435: /**
436: * Sets the SAX attributes of this tag
437: * @param saxAttributes The saxAttributes to set
438: */
439: public void setSaxAttributes(Attributes saxAttributes) {
440: this .saxAttributes = saxAttributes;
441: }
442:
443: /**
444: * Returns the local, non namespaced XML name of this tag
445: * @return String
446: */
447: public String getLocalName() {
448: return localName;
449: }
450:
451: /**
452: * Sets the local, non namespaced name of this tag.
453: * @param localName The localName to set
454: */
455: public void setLocalName(String localName) {
456: this .localName = localName;
457: }
458:
459: /**
460: * Returns the namespace context of this tag. This is all the prefixes
461: * in scope in the document where this tag is used which are mapped to
462: * their namespace URIs.
463: *
464: * @return a Map with the keys are namespace prefixes and the values are
465: * namespace URIs.
466: */
467: public synchronized Map getNamespaceContext() {
468: if (namespaceContext == null) {
469: if (parent != null) {
470: namespaceContext = getParent().getNamespaceContext();
471: if (tagNamespacesMap != null
472: && !tagNamespacesMap.isEmpty()) {
473: // create a new child context
474: Hashtable newContext = new Hashtable(
475: namespaceContext.size() + 1);
476: newContext.putAll(namespaceContext);
477: newContext.putAll(tagNamespacesMap);
478: namespaceContext = newContext;
479: }
480: } else {
481: namespaceContext = tagNamespacesMap;
482: if (namespaceContext == null) {
483: namespaceContext = new Hashtable();
484: }
485: }
486: }
487: return namespaceContext;
488: }
489:
490: // Implementation methods
491: //-------------------------------------------------------------------------
492:
493: /**
494: * Factory method to create a new Tag instance.
495: * The default implementation is to delegate to the TagFactory
496: */
497: protected Tag createTag() throws JellyException {
498: if (tagFactory != null) {
499: return tagFactory.createTag(localName, getSaxAttributes());
500: }
501: return null;
502: }
503:
504: /**
505: * Compiles a newly created tag if required, sets its parent and body.
506: */
507: protected void configureTag(Tag tag, JellyContext context)
508: throws JellyException {
509: if (tag instanceof CompilableTag) {
510: ((CompilableTag) tag).compile();
511: }
512: Tag parentTag = null;
513: if (parent != null) {
514: parentTag = parent.getTag(context);
515: }
516: tag.setParent(parentTag);
517: tag.setBody(tagBody);
518:
519: if (tag instanceof NamespaceAwareTag) {
520: NamespaceAwareTag naTag = (NamespaceAwareTag) tag;
521: naTag.setNamespaceContext(getNamespaceContext());
522: }
523: if (tag instanceof LocationAware) {
524: applyLocation((LocationAware) tag);
525: }
526: }
527:
528: /**
529: * Allows the script to set the tag instance to be used, such as in a StaticTagScript
530: * when a StaticTag is switched with a DynamicTag
531: */
532: protected void setTag(Tag tag, JellyContext context) {
533: Thread t = Thread.currentThread();
534: threadLocalTagCache.put(t, tag);
535: }
536:
537: /**
538: * Output the new namespace prefixes used for this element
539: */
540: protected void startNamespacePrefixes(XMLOutput output)
541: throws SAXException {
542: if (tagNamespacesMap != null) {
543: for (Iterator iter = tagNamespacesMap.entrySet().iterator(); iter
544: .hasNext();) {
545: Map.Entry entry = (Map.Entry) iter.next();
546: String prefix = (String) entry.getKey();
547: String uri = (String) entry.getValue();
548: output.startPrefixMapping(prefix, uri);
549: }
550: }
551: }
552:
553: /**
554: * End the new namespace prefixes mapped for the current element
555: */
556: protected void endNamespacePrefixes(XMLOutput output)
557: throws SAXException {
558: if (tagNamespacesMap != null) {
559: for (Iterator iter = tagNamespacesMap.keySet().iterator(); iter
560: .hasNext();) {
561: String prefix = (String) iter.next();
562: output.endPrefixMapping(prefix);
563: }
564: }
565: }
566:
567: /**
568: * Converts the given value to the required type.
569: *
570: * @param value is the value to be converted. This will not be null
571: * @param requiredType the type that the value should be converted to
572: */
573: protected Object convertType(Object value, Class requiredType)
574: throws JellyException {
575: if (requiredType.isInstance(value)) {
576: return value;
577: }
578: if (value instanceof String) {
579: return ConvertUtils.convert((String) value, requiredType);
580: }
581: return value;
582: }
583:
584: /**
585: * Creates a new Jelly exception, adorning it with location information
586: */
587: protected JellyException createJellyException(String reason) {
588: return new JellyException(reason, fileName, elementName,
589: columnNumber, lineNumber);
590: }
591:
592: /**
593: * Creates a new Jelly exception, adorning it with location information
594: */
595: protected JellyException createJellyException(String reason,
596: Exception cause) {
597: if (cause instanceof JellyException) {
598: return (JellyException) cause;
599: }
600:
601: if (cause instanceof InvocationTargetException) {
602: return new JellyException(reason,
603: ((InvocationTargetException) cause)
604: .getTargetException(), fileName,
605: elementName, columnNumber, lineNumber);
606: }
607: return new JellyException(reason, cause, fileName, elementName,
608: columnNumber, lineNumber);
609: }
610:
611: /**
612: * A helper method to handle this Jelly exception.
613: * This method adorns the JellyException with location information
614: * such as adding line number information etc.
615: */
616: protected void handleException(JellyTagException e)
617: throws JellyTagException {
618: if (log.isTraceEnabled()) {
619: log.trace("Caught exception: " + e, e);
620: }
621:
622: applyLocation(e);
623:
624: throw e;
625: }
626:
627: /**
628: * A helper method to handle this Jelly exception.
629: * This method adorns the JellyException with location information
630: * such as adding line number information etc.
631: */
632: protected void handleException(JellyException e)
633: throws JellyTagException {
634: if (log.isTraceEnabled()) {
635: log.trace("Caught exception: " + e, e);
636: }
637:
638: applyLocation(e);
639:
640: throw new JellyTagException(e);
641: }
642:
643: protected void applyLocation(LocationAware locationAware) {
644: if (locationAware.getLineNumber() == -1) {
645: locationAware.setColumnNumber(columnNumber);
646: locationAware.setLineNumber(lineNumber);
647: }
648: if (locationAware.getFileName() == null) {
649: locationAware.setFileName(fileName);
650: }
651: if (locationAware.getElementName() == null) {
652: locationAware.setElementName(elementName);
653: }
654: }
655:
656: /**
657: * A helper method to handle this non-Jelly exception.
658: * This method will rethrow the exception, wrapped in a JellyException
659: * while adding line number information etc.
660: */
661: protected void handleException(Exception e)
662: throws JellyTagException {
663: if (log.isTraceEnabled()) {
664: log.trace("Caught exception: " + e, e);
665: }
666:
667: if (e instanceof LocationAware) {
668: applyLocation((LocationAware) e);
669: }
670:
671: if (e instanceof JellyException) {
672: e.fillInStackTrace();
673: }
674:
675: if (e instanceof InvocationTargetException) {
676: throw new JellyTagException(((InvocationTargetException) e)
677: .getTargetException(), fileName, elementName,
678: columnNumber, lineNumber);
679: }
680:
681: throw new JellyTagException(e, fileName, elementName,
682: columnNumber, lineNumber);
683: }
684:
685: /**
686: * A helper method to handle this non-Jelly exception.
687: * This method will rethrow the exception, wrapped in a JellyException
688: * while adding line number information etc.
689: *
690: * Is this method wise?
691: */
692: protected void handleException(Error e) throws Error,
693: JellyTagException {
694: if (log.isTraceEnabled()) {
695: log.trace("Caught exception: " + e, e);
696: }
697:
698: if (e instanceof LocationAware) {
699: applyLocation((LocationAware) e);
700: }
701:
702: throw new JellyTagException(e, fileName, elementName,
703: columnNumber, lineNumber);
704: }
705: }
|