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