View Javadoc

1   /*
2    * $Id: Component.java 651946 2008-04-27 13:41:38Z apetrelli $
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         if (altSyntax()) {
247             // does the expression start with %{ and end with }? if so, just cut it off!
248             if (expr.startsWith("%{") && expr.endsWith("}")) {
249                 expr = expr.substring(2, expr.length() - 1);
250             }
251         }
252 
253         return getStack().findValue(expr);
254     }
255 
256     /***
257      * Is the altSyntax enabled? [TRUE]
258      * <p/>
259      * See <code>struts.properties</code> where the altSyntax flag is defined.
260      */
261     public boolean altSyntax() {
262         return ContextUtil.isUseAltSyntax(stack.getContext());
263     }
264 
265     /***
266      * Evaluates the OGNL stack to find an Object value.
267      * <p/>
268      * Function just like <code>findValue(String)</code> except that if the
269      * given expression is <tt>null</tt/> a error is logged and
270      * a <code>RuntimeException</code> is thrown constructed with a
271      * messaged based on the given field and errorMsg paramter.
272      *
273      * @param expr  OGNL expression.
274      * @param field   field name used when throwing <code>RuntimeException</code>.
275      * @param errorMsg  error message used when throwing <code>RuntimeException</code>.
276      * @return  the Object found, is never <tt>null</tt>.
277      * @throws StrutsException is thrown in case of not found in the OGNL stack, or expression is <tt>null</tt>.
278      */
279     protected Object findValue(String expr, String field, String errorMsg) {
280         if (expr == null) {
281             throw fieldError(field, errorMsg, null);
282         } else {
283             Object value = null;
284             Exception problem = null;
285             try {
286                 value = findValue(expr);
287             } catch (Exception e) {
288                 problem = e;
289             }
290 
291             if (value == null) {
292                 throw fieldError(field, errorMsg, problem);
293             }
294 
295             return value;
296         }
297     }
298 
299     /***
300      * Evaluates the OGNL stack to find an Object of the given type. Will evaluate
301      * <code>expr</code> the portion wrapped with altSyntax (%{...})
302      * against stack when altSyntax is on, else the whole <code>expr</code>
303      * is evaluated against the stack.
304      * <p/>
305      * This method only supports the altSyntax. So this should be set to true.
306      * @param expr  OGNL expression.
307      * @param toType  the type expected to find.
308      * @return  the Object found, or <tt>null</tt> if not found.
309      */
310     protected Object findValue(String expr, Class toType) {
311         if (altSyntax() && toType == String.class) {
312             return TextParseUtil.translateVariables('%', expr, stack);
313         } else {
314             if (altSyntax()) {
315                 // does the expression start with %{ and end with }? if so, just cut it off!
316                 if (expr.startsWith("%{") && expr.endsWith("}")) {
317                     expr = expr.substring(2, expr.length() - 1);
318                 }
319             }
320 
321             return getStack().findValue(expr, toType);
322         }
323     }
324 
325     /***
326      * Renders an action URL by consulting the {@link org.apache.struts2.dispatcher.mapper.ActionMapper}.
327      * @param action      the action
328      * @param namespace   the namespace
329      * @param method      the method
330      * @param req         HTTP request
331      * @param res         HTTP response
332      * @param parameters  parameters
333      * @param scheme      http or https
334      * @param includeContext  should the context path be included or not
335      * @param encodeResult    should the url be encoded
336      * @param forceAddSchemeHostAndPort    should the scheme host and port be forced
337      * @param escapeAmp    should ampersand (&) be escaped to &amp;
338      * @return the action url.
339      */
340     protected String determineActionURL(String action, String namespace, String method,
341                                         HttpServletRequest req, HttpServletResponse res, Map parameters, String scheme,
342                                         boolean includeContext, boolean encodeResult, boolean forceAddSchemeHostAndPort,
343                                         boolean escapeAmp) {
344         String finalAction = findString(action);
345         String finalMethod = method != null ? findString(method) : method;
346         String finalNamespace = determineNamespace(namespace, getStack(), req);
347         ActionMapping mapping = new ActionMapping(finalAction, finalNamespace, finalMethod, parameters);
348         String uri = actionMapper.getUriFromActionMapping(mapping);
349         return UrlHelper.buildUrl(uri, req, res, parameters, scheme, includeContext, encodeResult, forceAddSchemeHostAndPort, escapeAmp);
350     }
351 
352     /***
353      * Determines the namespace of the current page being renderdd. Useful for Form, URL, and href generations.
354      * @param namespace  the namespace
355      * @param stack      OGNL value stack
356      * @param req        HTTP request
357      * @return  the namepsace of the current page being rendered, is never <tt>null</tt>.
358      */
359     protected String determineNamespace(String namespace, ValueStack stack, HttpServletRequest req) {
360         String result;
361 
362         if (namespace == null) {
363             result = TagUtils.buildNamespace(actionMapper, stack, req);
364         } else {
365             result = findString(namespace);
366         }
367 
368         if (result == null) {
369             result = "";
370         }
371 
372         return result;
373     }
374 
375     /***
376      * Pushes this component's parameter Map as well as the component itself on to the stack
377      * and then copies the supplied parameters over. Because the component's parameter Map is
378      * pushed before the component itself, any key-value pair that can't be assigned to componet
379      * will be set in the parameters Map.
380      *
381      * @param params  the parameters to copy.
382      */
383     public void copyParams(Map params) {
384         stack.push(parameters);
385         stack.push(this);
386         try {
387             for (Iterator iterator = params.entrySet().iterator(); iterator.hasNext();) {
388                 Map.Entry entry = (Map.Entry) iterator.next();
389                 String key = (String) entry.getKey();
390                 stack.setValue(key, entry.getValue());
391             }
392         } finally {
393             stack.pop();
394             stack.pop();
395         }
396     }
397 
398     /***
399      * Constructs a string representation of the given exception.
400      * @param t  the exception
401      * @return the exception as a string.
402      */
403     protected String toString(Throwable t) {
404         FastByteArrayOutputStream bout = new FastByteArrayOutputStream();
405         PrintWriter wrt = new PrintWriter(bout);
406         t.printStackTrace(wrt);
407         wrt.close();
408 
409         return bout.toString();
410     }
411 
412     /***
413      * Gets the parameters.
414      * @return the parameters. Is never <tt>null</tt>.
415      */
416     public Map getParameters() {
417         return parameters;
418     }
419 
420     /***
421      * Adds all the given parameters to this component's own parameters.
422      * @param params the parameters to add.
423      */
424     public void addAllParameters(Map params) {
425         parameters.putAll(params);
426     }
427 
428     /***
429      * Adds the given key and value to this component's own parameter.
430      * <p/>
431      * If the provided key is <tt>null</tt> nothing happens.
432      * If the provided value is <tt>null</tt> any existing parameter with
433      * the given key name is removed.
434      * @param key  the key of the new parameter to add.
435      * @param value the value assoicated with the key.
436      */
437     public void addParameter(String key, Object value) {
438         if (key != null) {
439             Map params = getParameters();
440 
441             if (value == null) {
442                 params.remove(key);
443             } else {
444                 params.put(key, value);
445             }
446         }
447     }
448 
449     /***
450      * Overwrite to set if body shold be used.
451      * @return always false for this component.
452      */
453     public boolean usesBody() {
454         return false;
455     }
456 }