View Javadoc

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