View Javadoc

1   /*
2    * $Id: Component.java 768855 2009-04-27 02:09:35Z wesw $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  package org.apache.struts2.components;
23  
24  import java.io.IOException;
25  import java.io.PrintWriter;
26  import java.io.Writer;
27  import java.util.Iterator;
28  import java.util.LinkedHashMap;
29  import java.util.Map;
30  import java.util.Stack;
31  
32  import javax.servlet.http.HttpServletRequest;
33  import javax.servlet.http.HttpServletResponse;
34  
35  import org.apache.struts2.StrutsException;
36  import org.apache.struts2.StrutsConstants;
37  import org.apache.struts2.dispatcher.mapper.ActionMapper;
38  import org.apache.struts2.dispatcher.mapper.ActionMapping;
39  import org.apache.struts2.util.FastByteArrayOutputStream;
40  import org.apache.struts2.views.jsp.TagUtils;
41  import org.apache.struts2.views.util.ContextUtil;
42  import org.apache.struts2.views.util.UrlHelper;
43  
44  import com.opensymphony.xwork2.inject.Inject;
45  import com.opensymphony.xwork2.util.ValueStack;
46  import com.opensymphony.xwork2.util.TextParseUtil;
47  
48  /***
49   * Base class to extend for UI components.
50   * <p/>
51   * This class is a good extension point when building reuseable UI components.
52   *
53   */
54  public class Component {
55      public static final String COMPONENT_STACK = "__component_stack";
56  
57      protected ValueStack stack;
58      protected Map parameters;
59      protected ActionMapper actionMapper;
60      protected boolean throwExceptionOnELFailure;
61  
62      /***
63       * Constructor.
64       *
65       * @param stack  OGNL value stack.
66       */
67      public Component(ValueStack stack) {
68          this.stack = stack;
69          this.parameters = new LinkedHashMap();
70          getComponentStack().push(this);
71      }
72  
73      /***
74       * Gets the name of this component.
75       * @return the name of this component.
76       */
77      private String getComponentName() {
78          Class c = getClass();
79          String name = c.getName();
80          int dot = name.lastIndexOf('.');
81  
82          return name.substring(dot + 1).toLowerCase();
83      }
84      
85      @Inject
86      public void setActionMapper(ActionMapper mapper) {
87          this.actionMapper = mapper;
88      }
89  
90      @Inject(StrutsConstants.STRUTS_EL_THROW_EXCEPTION)
91      public void setThrowExceptionsOnELFailure(String throwException) {
92          this.throwExceptionOnELFailure = "true".equals(throwException);
93      }
94      
95      /***
96       * Gets the OGNL value stack assoicated with this component.
97       * @return the OGNL value stack assoicated with this component.
98       */
99      public ValueStack getStack() {
100         return stack;
101     }
102 
103     /***
104      * Gets the component stack of this component.
105      * @return the component stack of this component, never <tt>null</tt>.
106      */
107     public Stack getComponentStack() {
108         Stack componentStack = (Stack) stack.getContext().get(COMPONENT_STACK);
109         if (componentStack == null) {
110             componentStack = new Stack();
111             stack.getContext().put(COMPONENT_STACK, componentStack);
112         }
113         return componentStack;
114     }
115 
116     /***
117      * Callback for the start tag of this component.
118      * Should the body be evaluated?
119      *
120      * @param writer  the output writer.
121      * @return true if the body should be evaluated
122      */
123     public boolean start(Writer writer) {
124         return true;
125     }
126 
127     /***
128      * Callback for the end tag of this component.
129      * Should the body be evaluated again?
130      * <p/>
131      * <b>NOTE:</b> will pop component stack.
132      * @param writer  the output writer.
133      * @param body    the rendered body.
134      * @return true if the body should be evaluated again
135      */
136     public boolean end(Writer writer, String body) {
137         return end(writer, body, true);
138     }
139 
140     /***
141      * Callback for the start tag of this component.
142      * Should the body be evaluated again?
143      * <p/>
144      * <b>NOTE:</b> has a parameter to determine to pop the component stack.
145      * @param writer  the output writer.
146      * @param body    the rendered body.
147      * @param popComponentStack  should the component stack be popped?
148      * @return true if the body should be evaluated again
149      */
150     protected boolean end(Writer writer, String body, boolean popComponentStack) {
151         assert(body != null);
152 
153         try {
154             writer.write(body);
155         } catch (IOException e) {
156             throw new StrutsException("IOError while writing the body: " + e.getMessage(), e);
157         }
158         if (popComponentStack) {
159             popComponentStack();
160         }
161         return false;
162     }
163 
164     /***
165      * Pops the component stack.
166      */
167     protected void popComponentStack() {
168         getComponentStack().pop();
169     }
170 
171     /***
172      * Finds the nearest ancestor of this component stack.
173      * @param clazz the class to look for, or if assignable from.
174      * @return  the component if found, <tt>null</tt> if not.
175      */
176     protected Component findAncestor(Class clazz) {
177         Stack componentStack = getComponentStack();
178         int currPosition = componentStack.search(this);
179         if (currPosition >= 0) {
180             int start = componentStack.size() - currPosition - 1;
181 
182             //for (int i = componentStack.size() - 2; i >= 0; i--) {
183             for (int i = start; i >=0; i--) {
184                 Component component = (Component) componentStack.get(i);
185                 if (clazz.isAssignableFrom(component.getClass()) && component != this) {
186                     return component;
187                 }
188             }
189         }
190 
191         return null;
192     }
193 
194     /***
195      * Evaluates the OGNL stack to find a String value.
196      * @param expr  OGNL expression.
197      * @return  the String value found.
198      */
199     protected String findString(String expr) {
200         return (String) findValue(expr, String.class);
201     }
202 
203     /***
204      * Evaluates the OGNL stack to find a String value.
205      * <p/>
206      * If the given expression is <tt>null</tt/> a error is logged and a <code>RuntimeException</code> is thrown
207      * constructed with a messaged based on the given field and errorMsg paramter.
208      *
209      * @param expr  OGNL expression.
210      * @param field   field name used when throwing <code>RuntimeException</code>.
211      * @param errorMsg  error message used when throwing <code>RuntimeException</code>.
212      * @return  the String value found.
213      * @throws StrutsException is thrown in case of expression is <tt>null</tt>.
214      */
215     protected String findString(String expr, String field, String errorMsg) {
216         if (expr == null) {
217             throw fieldError(field, errorMsg, null);
218         } else {
219             return findString(expr);
220         }
221     }
222 
223     /***
224      * Constructs a <code>RuntimeException</code> based on the given information.
225      * <p/>
226      * A message is constructed and logged at ERROR level before being returned
227      * as a <code>RuntimeException</code>.
228      * @param field   field name used when throwing <code>RuntimeException</code>.
229      * @param errorMsg  error message used when throwing <code>RuntimeException</code>.
230      * @param e  the caused exception, can be <tt>null</tt>.
231      * @return  the constructed <code>StrutsException</code>.
232      */
233     protected StrutsException fieldError(String field, String errorMsg, Exception e) {
234         String msg = "tag '" + getComponentName() + "', field '" + field +
235                 ( parameters != null && parameters.containsKey("name")?"', name '" + parameters.get("name"):"") +
236                 "': " + errorMsg;
237         throw new StrutsException(msg, e);
238     }
239 
240     /***
241      * Finds a value from the OGNL stack based on the given expression.
242      * Will always evaluate <code>expr</code> against stack except when <code>expr</code>
243      * is null. If altsyntax (%{...}) is applied, simply strip it off.
244      *
245      * @param expr  the expression. Returns <tt>null</tt> if expr is null.
246      * @return the value, <tt>null</tt> if not found.
247      */
248     protected Object findValue(String expr) {
249         if (expr == null) {
250             return null;
251         }
252 
253         expr = stripExpressionIfAltSyntax(expr);
254 
255         return getStack().findValue(expr, throwExceptionOnELFailure);
256     }
257 
258     /***
259      * If altsyntax (%{...}) is applied, simply strip the "%{" and "}" off. 
260      * @param expr the expression (must be not null)
261      * @return the stripped expression if altSyntax is enabled. Otherwise
262      * the parameter expression is returned as is.
263      */
264 	protected String stripExpressionIfAltSyntax(String expr) {
265 		return stripExpressionIfAltSyntax(stack, expr);
266 	}
267 	
268     /***
269      * If altsyntax (%{...}) is applied, simply strip the "%{" and "}" off.
270      * @param stack the ValueStack where the context value is searched for. 
271      * @param expr the expression (must be not null)
272      * @return the stripped expression if altSyntax is enabled. Otherwise
273      * the parameter expression is returned as is.
274      */
275 	public static String stripExpressionIfAltSyntax(ValueStack stack, String expr) {
276 		if (altSyntax(stack)) {
277             // does the expression start with %{ and end with }? if so, just cut it off!
278             if (expr.startsWith("%{") && expr.endsWith("}")) {
279                 return expr.substring(2, expr.length() - 1);
280             }
281         }
282 		return expr;
283 	}
284 
285     /***
286      * Is the altSyntax enabled? [TRUE]
287      * <p/>
288      * @param stack the ValueStack where the context value is searched for.
289      * @return true if altSyntax is activated. False otherwise. 
290      * See <code>struts.properties</code> where the altSyntax flag is defined.
291      */
292 	public static boolean altSyntax(ValueStack stack)  {
293         return ContextUtil.isUseAltSyntax(stack.getContext());
294 	}
295 
296     /***
297      * Is the altSyntax enabled? [TRUE]
298      * <p/>
299      * See <code>struts.properties</code> where the altSyntax flag is defined.
300      */
301     public boolean altSyntax() {
302         return altSyntax(stack);
303     }
304 
305     /***
306      * Adds the sorrounding %{ } to the expression for proper processing.
307      * @param expr the expression.
308      * @return the modified expression if altSyntax is enabled, or the parameter 
309      * expression otherwise.
310      */
311 	protected String completeExpressionIfAltSyntax(String expr) {
312 		if (altSyntax()) {
313 			return "%{" + expr + "}";
314 		}
315 		return expr;
316 	}
317 
318     /***
319      * This check is needed for backwards compatibility with 2.1.x
320      * @param expr the expression.
321      * @return the found string if altSyntax is enabled. The parameter
322      * expression otherwise.
323      */
324 	protected String findStringIfAltSyntax(String expr) {
325 		if (altSyntax()) {
326 		    return findString(expr);
327 		}
328 		return expr;
329 	}
330 
331     /***
332      * Evaluates the OGNL stack to find an Object value.
333      * <p/>
334      * Function just like <code>findValue(String)</code> except that if the
335      * given expression is <tt>null</tt/> a error is logged and
336      * a <code>RuntimeException</code> is thrown constructed with a
337      * messaged based on the given field and errorMsg paramter.
338      *
339      * @param expr  OGNL expression.
340      * @param field   field name used when throwing <code>RuntimeException</code>.
341      * @param errorMsg  error message used when throwing <code>RuntimeException</code>.
342      * @return  the Object found, is never <tt>null</tt>.
343      * @throws StrutsException is thrown in case of not found in the OGNL stack, or expression is <tt>null</tt>.
344      */
345     protected Object findValue(String expr, String field, String errorMsg) {
346         if (expr == null) {
347             throw fieldError(field, errorMsg, null);
348         } else {
349             Object value = null;
350             Exception problem = null;
351             try {
352                 value = findValue(expr);
353             } catch (Exception e) {
354                 problem = e;
355             }
356 
357             if (value == null) {
358                 throw fieldError(field, errorMsg, problem);
359             }
360 
361             return value;
362         }
363     }
364 
365     /***
366      * Evaluates the OGNL stack to find an Object of the given type. Will evaluate
367      * <code>expr</code> the portion wrapped with altSyntax (%{...})
368      * against stack when altSyntax is on, else the whole <code>expr</code>
369      * is evaluated against the stack.
370      * <p/>
371      * This method only supports the altSyntax. So this should be set to true.
372      * @param expr  OGNL expression.
373      * @param toType  the type expected to find.
374      * @return  the Object found, or <tt>null</tt> if not found.
375      */
376     protected Object findValue(String expr, Class toType) {
377         if (altSyntax() && toType == String.class) {
378         	return TextParseUtil.translateVariables('%', expr, stack);
379         } else {
380             expr = stripExpressionIfAltSyntax(expr);
381 
382             return getStack().findValue(expr, toType, throwExceptionOnELFailure);
383         }
384     }
385 
386     /***
387      * Renders an action URL by consulting the {@link org.apache.struts2.dispatcher.mapper.ActionMapper}.
388      * @param action      the action
389      * @param namespace   the namespace
390      * @param method      the method
391      * @param req         HTTP request
392      * @param res         HTTP response
393      * @param parameters  parameters
394      * @param scheme      http or https
395      * @param includeContext  should the context path be included or not
396      * @param encodeResult    should the url be encoded
397      * @param forceAddSchemeHostAndPort    should the scheme host and port be forced
398      * @param escapeAmp    should ampersand (&) be escaped to &amp;
399      * @return the action url.
400      */
401     protected String determineActionURL(String action, String namespace, String method,
402                                         HttpServletRequest req, HttpServletResponse res, Map parameters, String scheme,
403                                         boolean includeContext, boolean encodeResult, boolean forceAddSchemeHostAndPort,
404                                         boolean escapeAmp) {
405         String finalAction = findString(action);
406         String finalMethod = method != null ? findString(method) : null;
407         String finalNamespace = determineNamespace(namespace, getStack(), req);
408         ActionMapping mapping = new ActionMapping(finalAction, finalNamespace, finalMethod, parameters);
409         String uri = actionMapper.getUriFromActionMapping(mapping);
410         return UrlHelper.buildUrl(uri, req, res, parameters, scheme, includeContext, encodeResult, forceAddSchemeHostAndPort, escapeAmp);
411     }
412 
413     /***
414      * Determines the namespace of the current page being renderdd. Useful for Form, URL, and href generations.
415      * @param namespace  the namespace
416      * @param stack      OGNL value stack
417      * @param req        HTTP request
418      * @return  the namepsace of the current page being rendered, is never <tt>null</tt>.
419      */
420     protected String determineNamespace(String namespace, ValueStack stack, HttpServletRequest req) {
421         String result;
422 
423         if (namespace == null) {
424             result = TagUtils.buildNamespace(actionMapper, stack, req);
425         } else {
426             result = findString(namespace);
427         }
428 
429         if (result == null) {
430             result = "";
431         }
432 
433         return result;
434     }
435 
436     /***
437      * Pushes this component's parameter Map as well as the component itself on to the stack
438      * and then copies the supplied parameters over. Because the component's parameter Map is
439      * pushed before the component itself, any key-value pair that can't be assigned to componet
440      * will be set in the parameters Map.
441      *
442      * @param params  the parameters to copy.
443      */
444     public void copyParams(Map params) {
445         stack.push(parameters);
446         stack.push(this);
447         try {
448             for (Iterator iterator = params.entrySet().iterator(); iterator.hasNext();) {
449                 Map.Entry entry = (Map.Entry) iterator.next();
450                 String key = (String) entry.getKey();
451                 stack.setValue(key, entry.getValue());
452             }
453         } finally {
454             stack.pop();
455             stack.pop();
456         }
457     }
458 
459     /***
460      * Constructs a string representation of the given exception.
461      * @param t  the exception
462      * @return the exception as a string.
463      */
464     protected String toString(Throwable t) {
465         FastByteArrayOutputStream bout = new FastByteArrayOutputStream();
466         PrintWriter wrt = new PrintWriter(bout);
467         t.printStackTrace(wrt);
468         wrt.close();
469 
470         return bout.toString();
471     }
472 
473     /***
474      * Gets the parameters.
475      * @return the parameters. Is never <tt>null</tt>.
476      */
477     public Map getParameters() {
478         return parameters;
479     }
480 
481     /***
482      * Adds all the given parameters to this component's own parameters.
483      * @param params the parameters to add.
484      */
485     public void addAllParameters(Map params) {
486         parameters.putAll(params);
487     }
488 
489     /***
490      * Adds the given key and value to this component's own parameter.
491      * <p/>
492      * If the provided key is <tt>null</tt> nothing happens.
493      * If the provided value is <tt>null</tt> any existing parameter with
494      * the given key name is removed.
495      * @param key  the key of the new parameter to add.
496      * @param value the value assoicated with the key.
497      */
498     public void addParameter(String key, Object value) {
499         if (key != null) {
500             Map params = getParameters();
501 
502             if (value == null) {
503                 params.remove(key);
504             } else {
505                 params.put(key, value);
506             }
507         }
508     }
509 
510     /***
511      * Overwrite to set if body shold be used.
512      * @return always false for this component.
513      */
514     public boolean usesBody() {
515         return false;
516     }
517 }