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