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: 165507 $
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, expression);
174 }
175
176 /***
177 * Strips off the name of a script to create a new context URL
178 * FIXME: Copied from JellyContext
179 */
180 private URL getJellyContextURL(URL url) throws MalformedURLException {
181 String text = url.toString();
182 int idx = text.lastIndexOf('/');
183 text = text.substring(0, idx + 1);
184 return new URL(text);
185 }
186
187
188
189
190 /*** Evaluates the body of a tag */
191 public void run(JellyContext context, XMLOutput output) throws JellyTagException {
192 URL rootURL = context.getRootURL();
193 URL currentURL = context.getCurrentURL();
194 try {
195 Tag tag = getTag(context);
196 if ( tag == null ) {
197 return;
198 }
199 tag.setContext(context);
200 setContextURLs(context);
201
202 if ( tag instanceof DynaTag ) {
203 DynaTag dynaTag = (DynaTag) tag;
204
205
206 for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) {
207 Map.Entry entry = (Map.Entry) iter.next();
208 String name = (String) entry.getKey();
209 Expression expression = (Expression) entry.getValue();
210
211 Class type = dynaTag.getAttributeType(name);
212 Object value = null;
213 if (type != null && type.isAssignableFrom(Expression.class) && !type.isAssignableFrom(Object.class)) {
214 value = expression;
215 }
216 else {
217 value = expression.evaluateRecurse(context);
218 }
219 dynaTag.setAttribute(name, value);
220 }
221 }
222 else {
223
224 DynaBean dynaBean = new ConvertingWrapDynaBean( tag );
225 for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) {
226 Map.Entry entry = (Map.Entry) iter.next();
227 String name = (String) entry.getKey();
228 Expression expression = (Expression) entry.getValue();
229
230 DynaProperty property = dynaBean.getDynaClass().getDynaProperty(name);
231 if (property == null) {
232 throw new JellyException("This tag does not understand the '" + name + "' attribute" );
233 }
234 Class type = property.getType();
235
236 Object value = null;
237 if (type.isAssignableFrom(Expression.class) && !type.isAssignableFrom(Object.class)) {
238 value = expression;
239 }
240 else {
241 value = expression.evaluateRecurse(context);
242 }
243 dynaBean.set(name, value);
244 }
245 }
246
247 tag.doTag(output);
248 if (output != null) {
249 output.flush();
250 }
251 }
252 catch (JellyTagException e) {
253 handleException(e);
254 } catch (JellyException e) {
255 handleException(e);
256 } catch (IOException e) {
257 handleException(e);
258 } catch (RuntimeException e) {
259 handleException(e);
260 }
261 catch (Error e) {
262
263
264
265
266
267 handleException(e);
268 } finally {
269 context.setRootURL(rootURL);
270 context.setCurrentURL(currentURL);
271 }
272
273 }
274
275 /***
276 * Set the context's root and current URL if not present
277 * @param context
278 * @throws JellyTagException
279 */
280 protected void setContextURLs(JellyContext context) throws JellyTagException {
281 if ((context.getCurrentURL() == null || context.getRootURL() == null) && scriptURL != null)
282 {
283 if (context.getRootURL() == null) context.setRootURL(scriptURL);
284 if (context.getCurrentURL() == null) context.setCurrentURL(scriptURL);
285 }
286 }
287
288
289
290
291 /***
292 * @return the tag to be evaluated, creating it lazily if required.
293 */
294 public Tag getTag(JellyContext context) throws JellyException {
295 Thread t = Thread.currentThread();
296 Tag tag = (Tag) threadLocalTagCache.get(t);
297 if ( tag == null ) {
298 tag = createTag();
299 if ( tag != null ) {
300 threadLocalTagCache.put(t,tag);
301 configureTag(tag,context);
302 }
303 }
304 return tag;
305 }
306
307 /***
308 * Returns the Factory of Tag instances.
309 * @return the factory
310 */
311 public TagFactory getTagFactory() {
312 return tagFactory;
313 }
314
315 /***
316 * Sets the Factory of Tag instances.
317 * @param tagFactory The factory to set
318 */
319 public void setTagFactory(TagFactory tagFactory) {
320 this.tagFactory = tagFactory;
321 }
322
323 /***
324 * Returns the parent.
325 * @return TagScript
326 */
327 public TagScript getParent() {
328 return parent;
329 }
330
331 /***
332 * Returns the tagBody.
333 * @return Script
334 */
335 public Script getTagBody() {
336 return tagBody;
337 }
338
339 /***
340 * Sets the parent.
341 * @param parent The parent to set
342 */
343 public void setParent(TagScript parent) {
344 this.parent = parent;
345 }
346
347 /***
348 * Sets the tagBody.
349 * @param tagBody The tagBody to set
350 */
351 public void setTagBody(Script tagBody) {
352 this.tagBody = tagBody;
353 }
354
355 /***
356 * @return the Jelly file which caused the problem
357 */
358 public String getFileName() {
359 return fileName;
360 }
361
362 /***
363 * Sets the Jelly file which caused the problem
364 */
365 public void setFileName(String fileName) {
366 this.fileName = fileName;
367 try
368 {
369 this.scriptURL = getJellyContextURL(new URL(fileName));
370 } catch (MalformedURLException e) {
371 log.debug("error setting script url", e);
372 }
373 }
374
375
376 /***
377 * @return the element name which caused the problem
378 */
379 public String getElementName() {
380 return elementName;
381 }
382
383 /***
384 * Sets the element name which caused the problem
385 */
386 public void setElementName(String elementName) {
387 this.elementName = elementName;
388 }
389 /***
390 * @return the line number of the tag
391 */
392 public int getLineNumber() {
393 return lineNumber;
394 }
395
396 /***
397 * Sets the line number of the tag
398 */
399 public void setLineNumber(int lineNumber) {
400 this.lineNumber = lineNumber;
401 }
402
403 /***
404 * @return the column number of the tag
405 */
406 public int getColumnNumber() {
407 return columnNumber;
408 }
409
410 /***
411 * Sets the column number of the tag
412 */
413 public void setColumnNumber(int columnNumber) {
414 this.columnNumber = columnNumber;
415 }
416
417 /***
418 * Returns the SAX attributes of this tag
419 * @return Attributes
420 */
421 public Attributes getSaxAttributes() {
422 return saxAttributes;
423 }
424
425 /***
426 * Sets the SAX attributes of this tag
427 * @param saxAttributes The saxAttributes to set
428 */
429 public void setSaxAttributes(Attributes saxAttributes) {
430 this.saxAttributes = saxAttributes;
431 }
432
433 /***
434 * Returns the local, non namespaced XML name of this tag
435 * @return String
436 */
437 public String getLocalName() {
438 return localName;
439 }
440
441 /***
442 * Sets the local, non namespaced name of this tag.
443 * @param localName The localName to set
444 */
445 public void setLocalName(String localName) {
446 this.localName = localName;
447 }
448
449
450 /***
451 * Returns the namespace context of this tag. This is all the prefixes
452 * in scope in the document where this tag is used which are mapped to
453 * their namespace URIs.
454 *
455 * @return a Map with the keys are namespace prefixes and the values are
456 * namespace URIs.
457 */
458 public synchronized Map getNamespaceContext() {
459 if (namespaceContext == null) {
460 if (parent != null) {
461 namespaceContext = getParent().getNamespaceContext();
462 if (tagNamespacesMap != null && !tagNamespacesMap.isEmpty()) {
463
464 Hashtable newContext = new Hashtable(namespaceContext.size()+1);
465 newContext.putAll(namespaceContext);
466 newContext.putAll(tagNamespacesMap);
467 namespaceContext = newContext;
468 }
469 }
470 else {
471 namespaceContext = tagNamespacesMap;
472 if (namespaceContext == null) {
473 namespaceContext = new Hashtable();
474 }
475 }
476 }
477 return namespaceContext;
478 }
479
480
481
482
483 /***
484 * Factory method to create a new Tag instance.
485 * The default implementation is to delegate to the TagFactory
486 */
487 protected Tag createTag() throws JellyException {
488 if ( tagFactory != null) {
489 return tagFactory.createTag(localName, getSaxAttributes());
490 }
491 return null;
492 }
493
494
495 /***
496 * Compiles a newly created tag if required, sets its parent and body.
497 */
498 protected void configureTag(Tag tag, JellyContext context) throws JellyException {
499 if (tag instanceof CompilableTag) {
500 ((CompilableTag) tag).compile();
501 }
502 Tag parentTag = null;
503 if ( parent != null ) {
504 parentTag = parent.getTag(context);
505 }
506 tag.setParent( parentTag );
507 tag.setBody( tagBody );
508
509 if (tag instanceof NamespaceAwareTag) {
510 NamespaceAwareTag naTag = (NamespaceAwareTag) tag;
511 naTag.setNamespaceContext(getNamespaceContext());
512 }
513 if (tag instanceof LocationAware) {
514 applyLocation((LocationAware) tag);
515 }
516 }
517
518
519 /***
520 * Allows the script to set the tag instance to be used, such as in a StaticTagScript
521 * when a StaticTag is switched with a DynamicTag
522 */
523 protected void setTag(Tag tag, JellyContext context) {
524 Thread t = Thread.currentThread();
525 threadLocalTagCache.put(t,tag);
526 }
527
528 /***
529 * Output the new namespace prefixes used for this element
530 */
531 protected void startNamespacePrefixes(XMLOutput output) throws SAXException {
532 if ( tagNamespacesMap != null ) {
533 for ( Iterator iter = tagNamespacesMap.entrySet().iterator(); iter.hasNext(); ) {
534 Map.Entry entry = (Map.Entry) iter.next();
535 String prefix = (String) entry.getKey();
536 String uri = (String) entry.getValue();
537 output.startPrefixMapping(prefix, uri);
538 }
539 }
540 }
541
542 /***
543 * End the new namespace prefixes mapped for the current element
544 */
545 protected void endNamespacePrefixes(XMLOutput output) throws SAXException {
546 if ( tagNamespacesMap != null ) {
547 for ( Iterator iter = tagNamespacesMap.keySet().iterator(); iter.hasNext(); ) {
548 String prefix = (String) iter.next();
549 output.endPrefixMapping(prefix);
550 }
551 }
552 }
553
554 /***
555 * Converts the given value to the required type.
556 *
557 * @param value is the value to be converted. This will not be null
558 * @param requiredType the type that the value should be converted to
559 */
560 protected Object convertType(Object value, Class requiredType)
561 throws JellyException {
562 if (requiredType.isInstance(value)) {
563 return value;
564 }
565 if (value instanceof String) {
566 return ConvertUtils.convert((String) value, requiredType);
567 }
568 return value;
569 }
570
571 /***
572 * Creates a new Jelly exception, adorning it with location information
573 */
574 protected JellyException createJellyException(String reason) {
575 return new JellyException(
576 reason, fileName, elementName, columnNumber, lineNumber
577 );
578 }
579
580 /***
581 * Creates a new Jelly exception, adorning it with location information
582 */
583 protected JellyException createJellyException(String reason, Exception cause) {
584 if (cause instanceof JellyException) {
585 return (JellyException) cause;
586 }
587
588 if (cause instanceof InvocationTargetException) {
589 return new JellyException(
590 reason,
591 ((InvocationTargetException) cause).getTargetException(),
592 fileName,
593 elementName,
594 columnNumber,
595 lineNumber);
596 }
597 return new JellyException(
598 reason, cause, fileName, elementName, columnNumber, lineNumber
599 );
600 }
601
602 /***
603 * A helper method to handle this Jelly exception.
604 * This method adorns the JellyException with location information
605 * such as adding line number information etc.
606 */
607 protected void handleException(JellyTagException e) throws JellyTagException {
608 if (log.isTraceEnabled()) {
609 log.trace( "Caught exception: " + e, e );
610 }
611
612 applyLocation(e);
613
614 throw e;
615 }
616
617 /***
618 * A helper method to handle this Jelly exception.
619 * This method adorns the JellyException with location information
620 * such as adding line number information etc.
621 */
622 protected void handleException(JellyException e) throws JellyTagException {
623 if (log.isTraceEnabled()) {
624 log.trace( "Caught exception: " + e, e );
625 }
626
627 applyLocation(e);
628
629 throw new JellyTagException(e);
630 }
631
632 protected void applyLocation(LocationAware locationAware) {
633 if (locationAware.getLineNumber() == -1) {
634 locationAware.setColumnNumber(columnNumber);
635 locationAware.setLineNumber(lineNumber);
636 }
637 if ( locationAware.getFileName() == null ) {
638 locationAware.setFileName( fileName );
639 }
640 if ( locationAware.getElementName() == null ) {
641 locationAware.setElementName( elementName );
642 }
643 }
644
645 /***
646 * A helper method to handle this non-Jelly exception.
647 * This method will rethrow the exception, wrapped in a JellyException
648 * while adding line number information etc.
649 */
650 protected void handleException(Exception e) throws JellyTagException {
651 if (log.isTraceEnabled()) {
652 log.trace( "Caught exception: " + e, e );
653 }
654
655 if (e instanceof LocationAware) {
656 applyLocation((LocationAware) e);
657 }
658
659 if ( e instanceof JellyException ) {
660 e.fillInStackTrace();
661 }
662
663 if ( e instanceof InvocationTargetException) {
664 throw new JellyTagException( ((InvocationTargetException)e).getTargetException(),
665 fileName,
666 elementName,
667 columnNumber,
668 lineNumber );
669 }
670
671 throw new JellyTagException(e, fileName, elementName, columnNumber, lineNumber);
672 }
673
674 /***
675 * A helper method to handle this non-Jelly exception.
676 * This method will rethrow the exception, wrapped in a JellyException
677 * while adding line number information etc.
678 *
679 * Is this method wise?
680 */
681 protected void handleException(Error e) throws Error, JellyTagException {
682 if (log.isTraceEnabled()) {
683 log.trace( "Caught exception: " + e, e );
684 }
685
686 if (e instanceof LocationAware) {
687 applyLocation((LocationAware) e);
688 }
689
690 throw new JellyTagException(e, fileName, elementName, columnNumber, lineNumber);
691 }
692 }