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.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
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
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 &
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 }