1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.apache.commons.jelly.impl;
17
18 import java.io.IOException;
19 import java.lang.reflect.InvocationTargetException;
20 import java.net.MalformedURLException;
21 import java.net.URL;
22 import java.util.Collections;
23 import java.util.Hashtable;
24 import java.util.Iterator;
25 import java.util.Map;
26 import java.util.WeakHashMap;
27
28 import org.apache.commons.beanutils.ConvertingWrapDynaBean;
29 import org.apache.commons.beanutils.ConvertUtils;
30 import org.apache.commons.beanutils.DynaBean;
31 import org.apache.commons.beanutils.DynaProperty;
32
33 import org.apache.commons.jelly.CompilableTag;
34 import org.apache.commons.jelly.JellyContext;
35 import org.apache.commons.jelly.JellyException;
36 import org.apache.commons.jelly.JellyTagException;
37 import org.apache.commons.jelly.DynaTag;
38 import org.apache.commons.jelly.LocationAware;
39 import org.apache.commons.jelly.NamespaceAwareTag;
40 import org.apache.commons.jelly.Script;
41 import org.apache.commons.jelly.Tag;
42 import org.apache.commons.jelly.XMLOutput;
43 import org.apache.commons.jelly.expression.Expression;
44
45 import org.apache.commons.logging.Log;
46 import org.apache.commons.logging.LogFactory;
47
48 import org.xml.sax.Attributes;
49 import org.xml.sax.Locator;
50 import org.xml.sax.SAXException;
51
52 /***
53 * <p><code>TagScript</code> is a Script that evaluates a custom tag.</p>
54 *
55 * <b>Note</b> that this class should be re-entrant and used
56 * concurrently by multiple threads.
57 *
58 * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
59 * @version $Revision: 227285 $
60 */
61 public class TagScript implements Script {
62
63 /*** The Log to which logging calls will be made. */
64 private static final Log log = LogFactory.getLog(TagScript.class);
65
66
67 /*** The attribute expressions that are created */
68 protected Map attributes = new Hashtable();
69
70 /*** the optional namespaces Map of prefix -> URI of this single Tag */
71 private Map tagNamespacesMap;
72
73 /***
74 * The optional namespace context mapping all prefixes -> URIs in scope
75 * at the point this tag is used.
76 * This Map is only created lazily if it is required by the NamespaceAwareTag.
77 */
78 private Map namespaceContext;
79
80 /*** the Jelly file which caused the problem */
81 private String fileName;
82
83 /*** the qualified element name which caused the problem */
84 private String elementName;
85
86 /*** the local (non-namespaced) tag name */
87 private String localName;
88
89 /*** the line number of the tag */
90 private int lineNumber = -1;
91
92 /*** the column number of the tag */
93 private int columnNumber = -1;
94
95 /*** the factory of Tag instances */
96 private TagFactory tagFactory;
97
98 /*** the body script used for this tag */
99 private Script tagBody;
100
101 /*** the parent TagScript */
102 private TagScript parent;
103
104 /*** the SAX attributes */
105 private Attributes saxAttributes;
106
107 /*** the url of the script when parsed */
108 private URL scriptURL = null;
109
110 /*** A synchronized WeakHashMap from the current Thread (key) to a Tag object (value).
111 */
112 private Map threadLocalTagCache = Collections.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=" + lineNumber + ":" + columnNumber + "]";
132 }
133
134 /***
135 * Compiles the tags body
136 */
137 public Script compile() throws JellyException {
138 if (tagBody != null) {
139 tagBody = tagBody.compile();
140 }
141 return this;
142 }
143
144 /***
145 * Sets the optional namespaces prefix -> URI map of
146 * the namespaces attached to this Tag
147 */
148 public void setTagNamespacesMap(Map tagNamespacesMap) {
149
150 if ( ! (tagNamespacesMap instanceof Hashtable) ) {
151 tagNamespacesMap = new Hashtable( tagNamespacesMap );
152 }
153 this.tagNamespacesMap = tagNamespacesMap;
154 }
155
156 /***
157 * Configures this TagScript from the SAX Locator, setting the column
158 * and line numbers
159 */
160 public void setLocator(Locator locator) {
161 setLineNumber( locator.getLineNumber() );
162 setColumnNumber( locator.getColumnNumber() );
163 }
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 + " expression: " + expression);
172 }
173 attributes.put(name, new ExpressionAttribute(name,expression));
174 }
175
176 /*** Add an initialization attribute for the tag.
177 * This method must be called after the setTag() method
178 */
179 public void addAttribute(String name, String prefix, String nsURI, Expression expression) {
180 if (log.isDebugEnabled()) {
181 log.debug("adding attribute name: " + name + " expression: " + expression);
182 }
183 if(name.indexOf(':')==-1)
184 name = prefix + ':' + name;
185 attributes.put(name, new ExpressionAttribute(name,prefix,nsURI,expression));
186 }
187
188 /***
189 * Strips off the name of a script to create a new context URL
190 * FIXME: Copied from JellyContext
191 */
192 private URL getJellyContextURL(URL url) throws MalformedURLException {
193 String text = url.toString();
194 int idx = text.lastIndexOf('/');
195 text = text.substring(0, idx + 1);
196 return new URL(text);
197 }
198
199
200
201
202 /*** Evaluates the body of a tag */
203 public void run(JellyContext context, XMLOutput output) throws JellyTagException {
204 URL rootURL = context.getRootURL();
205 URL currentURL = context.getCurrentURL();
206 if ( ! context.isCacheTags() ) {
207 clearTag();
208 }
209 try {
210 Tag tag = getTag(context);
211 if ( tag == null ) {
212 return;
213 }
214 tag.setContext(context);
215 setContextURLs(context);
216
217 if ( tag instanceof DynaTag ) {
218 DynaTag dynaTag = (DynaTag) tag;
219
220
221 for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) {
222 Map.Entry entry = (Map.Entry) iter.next();
223 String name = (String) entry.getKey();
224 Expression expression = ((ExpressionAttribute) entry.getValue()).exp;
225
226 Class type = dynaTag.getAttributeType(name);
227 Object value = null;
228 if (type != null && type.isAssignableFrom(Expression.class) && !type.isAssignableFrom(Object.class)) {
229 value = expression;
230 }
231 else {
232 value = expression.evaluateRecurse(context);
233 }
234 dynaTag.setAttribute(name, value);
235 }
236 }
237 else {
238
239 DynaBean dynaBean = new ConvertingWrapDynaBean( tag );
240 for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) {
241 Map.Entry entry = (Map.Entry) iter.next();
242 String name = (String) entry.getKey();
243 Expression expression = ((ExpressionAttribute) entry.getValue()).exp;
244
245 DynaProperty property = dynaBean.getDynaClass().getDynaProperty(name);
246 if (property == null) {
247 throw new JellyException("This tag does not understand the '" + name + "' attribute" );
248 }
249 Class type = property.getType();
250
251 Object value = null;
252 if (type.isAssignableFrom(Expression.class) && !type.isAssignableFrom(Object.class)) {
253 value = expression;
254 }
255 else {
256 value = expression.evaluateRecurse(context);
257 }
258 dynaBean.set(name, value);
259 }
260 }
261
262 tag.doTag(output);
263 if (output != null) {
264 output.flush();
265 }
266 }
267 catch (JellyTagException e) {
268 handleException(e);
269 } catch (JellyException e) {
270 handleException(e);
271 } catch (IOException e) {
272 handleException(e);
273 } catch (RuntimeException e) {
274 handleException(e);
275 }
276 catch (Error e) {
277
278
279
280
281
282 handleException(e);
283 } finally {
284 context.setRootURL(rootURL);
285 context.setCurrentURL(currentURL);
286 }
287
288 }
289
290 /***
291 * Set the context's root and current URL if not present
292 * @param context
293 * @throws JellyTagException
294 */
295 protected void setContextURLs(JellyContext context) throws JellyTagException {
296 if ((context.getCurrentURL() == null || context.getRootURL() == null) && scriptURL != null)
297 {
298 if (context.getRootURL() == null) context.setRootURL(scriptURL);
299 if (context.getCurrentURL() == null) context.setCurrentURL(scriptURL);
300 }
301 }
302
303
304
305
306 /***
307 * @return the tag to be evaluated, creating it lazily if required.
308 */
309 public Tag getTag(JellyContext context) throws JellyException {
310 Thread t = Thread.currentThread();
311 Tag tag = (Tag) threadLocalTagCache.get(t);
312 if ( tag == null ) {
313 tag = createTag();
314 if ( tag != null ) {
315 threadLocalTagCache.put(t,tag);
316 configureTag(tag,context);
317 }
318 }
319 return tag;
320 }
321
322 /***
323 * Returns the Factory of Tag instances.
324 * @return the factory
325 */
326 public TagFactory getTagFactory() {
327 return tagFactory;
328 }
329
330 /***
331 * Sets the Factory of Tag instances.
332 * @param tagFactory The factory to set
333 */
334 public void setTagFactory(TagFactory tagFactory) {
335 this.tagFactory = tagFactory;
336 }
337
338 /***
339 * Returns the parent.
340 * @return TagScript
341 */
342 public TagScript getParent() {
343 return parent;
344 }
345
346 /***
347 * Returns the tagBody.
348 * @return Script
349 */
350 public Script getTagBody() {
351 return tagBody;
352 }
353
354 /***
355 * Sets the parent.
356 * @param parent The parent to set
357 */
358 public void setParent(TagScript parent) {
359 this.parent = parent;
360 }
361
362 /***
363 * Sets the tagBody.
364 * @param tagBody The tagBody to set
365 */
366 public void setTagBody(Script tagBody) {
367 this.tagBody = tagBody;
368 }
369
370 /***
371 * @return the Jelly file which caused the problem
372 */
373 public String getFileName() {
374 return fileName;
375 }
376
377 /***
378 * Sets the Jelly file which caused the problem
379 */
380 public void setFileName(String fileName) {
381 this.fileName = fileName;
382 try
383 {
384 this.scriptURL = getJellyContextURL(new URL(fileName));
385 } catch (MalformedURLException e) {
386 log.debug("error setting script url", e);
387 }
388 }
389
390
391 /***
392 * @return the element name which caused the problem
393 */
394 public String getElementName() {
395 return elementName;
396 }
397
398 /***
399 * Sets the element name which caused the problem
400 */
401 public void setElementName(String elementName) {
402 this.elementName = elementName;
403 }
404 /***
405 * @return the line number of the tag
406 */
407 public int getLineNumber() {
408 return lineNumber;
409 }
410
411 /***
412 * Sets the line number of the tag
413 */
414 public void setLineNumber(int lineNumber) {
415 this.lineNumber = lineNumber;
416 }
417
418 /***
419 * @return the column number of the tag
420 */
421 public int getColumnNumber() {
422 return columnNumber;
423 }
424
425 /***
426 * Sets the column number of the tag
427 */
428 public void setColumnNumber(int columnNumber) {
429 this.columnNumber = columnNumber;
430 }
431
432 /***
433 * Returns the SAX attributes of this tag
434 * @return Attributes
435 */
436 public Attributes getSaxAttributes() {
437 return saxAttributes;
438 }
439
440 /***
441 * Sets the SAX attributes of this tag
442 * @param saxAttributes The saxAttributes to set
443 */
444 public void setSaxAttributes(Attributes saxAttributes) {
445 this.saxAttributes = saxAttributes;
446 }
447
448 /***
449 * Returns the local, non namespaced XML name of this tag
450 * @return String
451 */
452 public String getLocalName() {
453 return localName;
454 }
455
456 /***
457 * Sets the local, non namespaced name of this tag.
458 * @param localName The localName to set
459 */
460 public void setLocalName(String localName) {
461 this.localName = localName;
462 }
463
464
465 /***
466 * Returns the namespace context of this tag. This is all the prefixes
467 * in scope in the document where this tag is used which are mapped to
468 * their namespace URIs.
469 *
470 * @return a Map with the keys are namespace prefixes and the values are
471 * namespace URIs.
472 */
473 public synchronized Map getNamespaceContext() {
474 if (namespaceContext == null) {
475 if (parent != null) {
476 namespaceContext = getParent().getNamespaceContext();
477 if (tagNamespacesMap != null && !tagNamespacesMap.isEmpty()) {
478
479 Hashtable newContext = new Hashtable(namespaceContext.size()+1);
480 newContext.putAll(namespaceContext);
481 newContext.putAll(tagNamespacesMap);
482 namespaceContext = newContext;
483 }
484 }
485 else {
486 namespaceContext = tagNamespacesMap;
487 if (namespaceContext == null) {
488 namespaceContext = new Hashtable();
489 }
490 }
491 }
492 return namespaceContext;
493 }
494
495
496
497
498 /***
499 * Factory method to create a new Tag instance.
500 * The default implementation is to delegate to the TagFactory
501 */
502 protected Tag createTag() throws JellyException {
503 if ( tagFactory != null) {
504 return tagFactory.createTag(localName, getSaxAttributes());
505 }
506 return null;
507 }
508
509
510 /***
511 * Compiles a newly created tag if required, sets its parent and body.
512 */
513 protected void configureTag(Tag tag, JellyContext context) throws JellyException {
514 if (tag instanceof CompilableTag) {
515 ((CompilableTag) tag).compile();
516 }
517 Tag parentTag = null;
518 if ( parent != null ) {
519 parentTag = parent.getTag(context);
520 }
521 tag.setParent( parentTag );
522 tag.setBody( tagBody );
523
524 if (tag instanceof NamespaceAwareTag) {
525 NamespaceAwareTag naTag = (NamespaceAwareTag) tag;
526 naTag.setNamespaceContext(getNamespaceContext());
527 }
528 if (tag instanceof LocationAware) {
529 applyLocation((LocationAware) tag);
530 }
531 }
532
533 /***
534 * Flushes the current cached tag so that it will be created, lazily, next invocation
535 */
536 protected void clearTag() {
537 Thread t = Thread.currentThread();
538 threadLocalTagCache.put(t,null);
539 }
540
541 /***
542 * Allows the script to set the tag instance to be used, such as in a StaticTagScript
543 * when a StaticTag is switched with a DynamicTag
544 */
545 protected void setTag(Tag tag, JellyContext context) {
546 Thread t = Thread.currentThread();
547 threadLocalTagCache.put(t,tag);
548 }
549
550 /***
551 * Output the new namespace prefixes used for this element
552 */
553 protected void startNamespacePrefixes(XMLOutput output) throws SAXException {
554 if ( tagNamespacesMap != null ) {
555 for ( Iterator iter = tagNamespacesMap.entrySet().iterator(); iter.hasNext(); ) {
556 Map.Entry entry = (Map.Entry) iter.next();
557 String prefix = (String) entry.getKey();
558 String uri = (String) entry.getValue();
559 output.startPrefixMapping(prefix, uri);
560 }
561 }
562 }
563
564 /***
565 * End the new namespace prefixes mapped for the current element
566 */
567 protected void endNamespacePrefixes(XMLOutput output) throws SAXException {
568 if ( tagNamespacesMap != null ) {
569 for ( Iterator iter = tagNamespacesMap.keySet().iterator(); iter.hasNext(); ) {
570 String prefix = (String) iter.next();
571 output.endPrefixMapping(prefix);
572 }
573 }
574 }
575
576 /***
577 * Converts the given value to the required type.
578 *
579 * @param value is the value to be converted. This will not be null
580 * @param requiredType the type that the value should be converted to
581 */
582 protected Object convertType(Object value, Class requiredType)
583 throws JellyException {
584 if (requiredType.isInstance(value)) {
585 return value;
586 }
587 if (value instanceof String) {
588 return ConvertUtils.convert((String) value, requiredType);
589 }
590 return value;
591 }
592
593 /***
594 * Creates a new Jelly exception, adorning it with location information
595 */
596 protected JellyException createJellyException(String reason) {
597 return new JellyException(
598 reason, fileName, elementName, columnNumber, lineNumber
599 );
600 }
601
602 /***
603 * Creates a new Jelly exception, adorning it with location information
604 */
605 protected JellyException createJellyException(String reason, Exception cause) {
606 if (cause instanceof JellyException) {
607 return (JellyException) cause;
608 }
609
610 if (cause instanceof InvocationTargetException) {
611 return new JellyException(
612 reason,
613 ((InvocationTargetException) cause).getTargetException(),
614 fileName,
615 elementName,
616 columnNumber,
617 lineNumber);
618 }
619 return new JellyException(
620 reason, cause, fileName, elementName, columnNumber, lineNumber
621 );
622 }
623
624 /***
625 * A helper method to handle this Jelly exception.
626 * This method adorns the JellyException with location information
627 * such as adding line number information etc.
628 */
629 protected void handleException(JellyTagException e) throws JellyTagException {
630 if (log.isTraceEnabled()) {
631 log.trace( "Caught exception: " + e, e );
632 }
633
634 applyLocation(e);
635
636 throw e;
637 }
638
639 /***
640 * A helper method to handle this Jelly exception.
641 * This method adorns the JellyException with location information
642 * such as adding line number information etc.
643 */
644 protected void handleException(JellyException e) throws JellyTagException {
645 if (log.isTraceEnabled()) {
646 log.trace( "Caught exception: " + e, e );
647 }
648
649 applyLocation(e);
650
651 throw new JellyTagException(e);
652 }
653
654 protected void applyLocation(LocationAware locationAware) {
655 if (locationAware.getLineNumber() == -1) {
656 locationAware.setColumnNumber(columnNumber);
657 locationAware.setLineNumber(lineNumber);
658 }
659 if ( locationAware.getFileName() == null ) {
660 locationAware.setFileName( fileName );
661 }
662 if ( locationAware.getElementName() == null ) {
663 locationAware.setElementName( elementName );
664 }
665 }
666
667 /***
668 * A helper method to handle this non-Jelly exception.
669 * This method will rethrow the exception, wrapped in a JellyException
670 * while adding line number information etc.
671 */
672 protected void handleException(Exception e) throws JellyTagException {
673 if (log.isTraceEnabled()) {
674 log.trace( "Caught exception: " + e, e );
675 }
676
677 if (e instanceof LocationAware) {
678 applyLocation((LocationAware) e);
679 }
680
681 if ( e instanceof JellyException ) {
682 e.fillInStackTrace();
683 }
684
685 if ( e instanceof InvocationTargetException) {
686 throw new JellyTagException( ((InvocationTargetException)e).getTargetException(),
687 fileName,
688 elementName,
689 columnNumber,
690 lineNumber );
691 }
692
693 throw new JellyTagException(e, fileName, elementName, columnNumber, lineNumber);
694 }
695
696 /***
697 * A helper method to handle this non-Jelly exception.
698 * This method will rethrow the exception, wrapped in a JellyException
699 * while adding line number information etc.
700 *
701 * Is this method wise?
702 */
703 protected void handleException(Error e) throws Error, JellyTagException {
704 if (log.isTraceEnabled()) {
705 log.trace( "Caught exception: " + e, e );
706 }
707
708 if (e instanceof LocationAware) {
709 applyLocation((LocationAware) e);
710 }
711
712 throw new JellyTagException(e, fileName, elementName, columnNumber, lineNumber);
713 }
714 }
715
716
717 class ExpressionAttribute {
718 public ExpressionAttribute(String name, Expression exp) {
719 this(name,"","",exp);
720 }
721 public ExpressionAttribute(String name, String prefix, String nsURI, Expression exp) {
722 this.name = name;
723 this.prefix = prefix;
724 this.nsURI = nsURI;
725 this.exp = exp;
726 }
727
728 String name;
729 String prefix;
730 String nsURI;
731 Expression exp;
732 }