1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
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
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 &
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 }