View Javadoc

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