| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.struts2.components; |
| |
| import com.opensymphony.xwork2.inject.Inject; |
| import com.opensymphony.xwork2.util.TextParseUtil; |
| import com.opensymphony.xwork2.util.ValueStack; |
| import org.apache.commons.lang3.BooleanUtils; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.commons.lang3.reflect.MethodUtils; |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| import org.apache.struts2.StrutsConstants; |
| import org.apache.struts2.StrutsException; |
| import org.apache.struts2.dispatcher.mapper.ActionMapper; |
| import org.apache.struts2.dispatcher.mapper.ActionMapping; |
| import org.apache.struts2.util.ComponentUtils; |
| import org.apache.struts2.util.FastByteArrayOutputStream; |
| import org.apache.struts2.views.TagAttribute; |
| import org.apache.struts2.views.annotations.StrutsTagAttribute; |
| import org.apache.struts2.views.jsp.TagUtils; |
| import org.apache.struts2.views.util.UrlHelper; |
| |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.io.Writer; |
| import java.lang.reflect.Method; |
| import java.util.*; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentMap; |
| |
| /** |
| * Base class to extend for UI components. |
| * <br> |
| * This class is a good extension point when building reusable UI components. |
| * |
| */ |
| public class Component { |
| |
| private static final Logger LOG = LogManager.getLogger(Component.class); |
| |
| public static final String COMPONENT_STACK = "__component_stack"; |
| |
| /** |
| * Caches information about common tag's attributes to reduce scanning for annotation @StrutsTagAttribute |
| */ |
| protected static ConcurrentMap<Class<?>, Collection<String>> standardAttributesMap = new ConcurrentHashMap<>(); |
| |
| protected boolean devMode = false; |
| protected ValueStack stack; |
| protected Map<String, Object> parameters; |
| protected ActionMapper actionMapper; |
| protected boolean throwExceptionOnELFailure; |
| private UrlHelper urlHelper; |
| |
| /** |
| * Constructor. |
| * |
| * @param stack OGNL value stack. |
| */ |
| public Component(ValueStack stack) { |
| this.stack = stack; |
| this.parameters = new LinkedHashMap<>(); |
| getComponentStack().push(this); |
| } |
| |
| /** |
| * Gets the name of this component. |
| * @return the name of this component. |
| */ |
| private String getComponentName() { |
| Class c = getClass(); |
| String name = c.getName(); |
| int dot = name.lastIndexOf('.'); |
| |
| return name.substring(dot + 1).toLowerCase(); |
| } |
| |
| @Inject(value = StrutsConstants.STRUTS_DEVMODE, required = false) |
| public void setDevMode(String devMode) { |
| this.devMode = BooleanUtils.toBoolean(devMode); |
| } |
| |
| @Inject |
| public void setActionMapper(ActionMapper mapper) { |
| this.actionMapper = mapper; |
| } |
| |
| @Inject(StrutsConstants.STRUTS_EL_THROW_EXCEPTION) |
| public void setThrowExceptionsOnELFailure(String throwException) { |
| this.throwExceptionOnELFailure = BooleanUtils.toBoolean(throwException); |
| } |
| |
| @Inject |
| public void setUrlHelper(UrlHelper urlHelper) { |
| this.urlHelper = urlHelper; |
| } |
| /** |
| * Gets the OGNL value stack associated with this component. |
| * @return the OGNL value stack associated with this component. |
| */ |
| public ValueStack getStack() { |
| return stack; |
| } |
| |
| /** |
| * Gets the component stack of this component. |
| * @return the component stack of this component, never <tt>null</tt>. |
| */ |
| public Stack<Component> getComponentStack() { |
| Stack<Component> componentStack = (Stack<Component>) stack.getContext().get(COMPONENT_STACK); |
| if (componentStack == null) { |
| componentStack = new Stack<>(); |
| stack.getContext().put(COMPONENT_STACK, componentStack); |
| } |
| return componentStack; |
| } |
| |
| /** |
| * Callback for the start tag of this component. |
| * Should the body be evaluated? |
| * |
| * @param writer the output writer. |
| * @return true if the body should be evaluated |
| */ |
| public boolean start(Writer writer) { |
| return true; |
| } |
| |
| /** |
| * Callback for the end tag of this component. |
| * Should the body be evaluated again? |
| * <br> |
| * <b>NOTE:</b> will pop component stack. |
| * @param writer the output writer. |
| * @param body the rendered body. |
| * @return true if the body should be evaluated again |
| */ |
| public boolean end(Writer writer, String body) { |
| return end(writer, body, true); |
| } |
| |
| /** |
| * Callback for the start tag of this component. |
| * Should the body be evaluated again? |
| * <br> |
| * <b>NOTE:</b> has a parameter to determine to pop the component stack. |
| * @param writer the output writer. |
| * @param body the rendered body. |
| * @param popComponentStack should the component stack be popped? |
| * @return true if the body should be evaluated again |
| */ |
| protected boolean end(Writer writer, String body, boolean popComponentStack) { |
| assert(body != null); |
| |
| try { |
| writer.write(body); |
| } catch (IOException e) { |
| throw new StrutsException("IOError while writing the body: " + e.getMessage(), e); |
| } |
| if (popComponentStack) { |
| popComponentStack(); |
| } |
| return false; |
| } |
| |
| /** |
| * Pops the component stack. |
| */ |
| protected void popComponentStack() { |
| getComponentStack().pop(); |
| } |
| |
| /** |
| * Finds the nearest ancestor of this component stack. |
| * @param clazz the class to look for, or if assignable from. |
| * @return the component if found, <tt>null</tt> if not. |
| */ |
| protected Component findAncestor(Class clazz) { |
| Stack componentStack = getComponentStack(); |
| int currPosition = componentStack.search(this); |
| if (currPosition >= 0) { |
| int start = componentStack.size() - currPosition - 1; |
| |
| //for (int i = componentStack.size() - 2; i >= 0; i--) { |
| for (int i = start; i >=0; i--) { |
| Component component = (Component) componentStack.get(i); |
| if (clazz.isAssignableFrom(component.getClass()) && component != this) { |
| return component; |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Evaluates the OGNL stack to find a String value. |
| * @param expr OGNL expression. |
| * @return the String value found. |
| */ |
| protected String findString(String expr) { |
| return (String) findValue(expr, String.class); |
| } |
| |
| protected TagAttribute findString(TagAttribute attribute) { |
| return findValue(attribute, String.class); |
| } |
| |
| /** |
| * Evaluates the OGNL stack to find a String value. |
| * <br> |
| * If the given expression is <tt>null</tt> a error is logged and a <code>RuntimeException</code> is thrown |
| * constructed with a messaged based on the given field and errorMsg parameter. |
| * |
| * @param expr OGNL expression. |
| * @param field field name used when throwing <code>RuntimeException</code>. |
| * @param errorMsg error message used when throwing <code>RuntimeException</code>. |
| * @return the String value found. |
| * @throws StrutsException is thrown in case of expression is null. |
| */ |
| protected String findString(String expr, String field, String errorMsg) { |
| if (expr == null) { |
| throw fieldError(field, errorMsg, null); |
| } else { |
| return findString(expr); |
| } |
| } |
| |
| /** |
| * Constructs a <code>RuntimeException</code> based on the given information. |
| * <br> |
| * A message is constructed and logged at ERROR level before being returned |
| * as a <code>RuntimeException</code>. |
| * @param field field name used when throwing <code>RuntimeException</code>. |
| * @param errorMsg error message used when throwing <code>RuntimeException</code>. |
| * @param e the caused exception, can be <tt>null</tt>. |
| * @return the constructed <code>StrutsException</code>. |
| */ |
| protected StrutsException fieldError(String field, String errorMsg, Exception e) { |
| String msg = "tag '" + getComponentName() + "', field '" + field + |
| ( parameters != null && parameters.containsKey("name")?"', name '" + parameters.get("name"):"") + |
| "': " + errorMsg; |
| throw new StrutsException(msg, e); |
| } |
| |
| /** |
| * Finds a value from the OGNL stack based on the given expression. |
| * Will always evaluate <code>expr</code> against stack except when <code>expr</code> |
| * is null. If altsyntax (%{...}) is applied, simply strip it off. |
| * |
| * @param expr the expression. Returns <tt>null</tt> if expr is null. |
| * @return the value, <tt>null</tt> if not found. |
| */ |
| protected Object findValue(String expr) { |
| if (expr == null) { |
| return null; |
| } |
| |
| expr = stripExpressionIfAltSyntax(expr); |
| |
| return getStack().findValue(expr, throwExceptionOnELFailure); |
| } |
| |
| /** |
| * If altsyntax (%{...}) is applied, simply strip the "%{" and "}" off. |
| * @param expr the expression (must be not null) |
| * @return the stripped expression if altSyntax is enabled. Otherwise |
| * the parameter expression is returned as is. |
| */ |
| protected String stripExpressionIfAltSyntax(String expr) { |
| return ComponentUtils.stripExpressionIfAltSyntax(stack, expr); |
| } |
| |
| /** |
| * See <code>struts.properties</code> where the altSyntax flag is defined. |
| * @return if the altSyntax enabled? [TRUE] |
| */ |
| public boolean altSyntax() { |
| return ComponentUtils.altSyntax(stack); |
| } |
| |
| /** |
| * Adds the surrounding %{ } to the expression for proper processing. |
| * @param expr the expression. |
| * @return the modified expression if altSyntax is enabled, or the parameter |
| * expression otherwise. |
| */ |
| protected String completeExpressionIfAltSyntax(String expr) { |
| if (altSyntax()) { |
| return "%{" + expr + "}"; |
| } |
| return expr; |
| } |
| |
| /** |
| * This check is needed for backwards compatibility with 2.1.x |
| * @param expr the expression. |
| * @return the found string if altSyntax is enabled. The parameter |
| * expression otherwise. |
| */ |
| protected String findStringIfAltSyntax(String expr) { |
| if (altSyntax()) { |
| return findString(expr); |
| } |
| return expr; |
| } |
| |
| protected TagAttribute findStringIfAltSyntax(TagAttribute attribute) { |
| if (altSyntax()) { |
| return findString(attribute); |
| } |
| return attribute; |
| } |
| |
| /** |
| * <p> |
| * Evaluates the OGNL stack to find an Object value. |
| * </p> |
| * |
| * <p> |
| * Function just like <code>findValue(String)</code> except that if the |
| * given expression is <tt>null</tt> a error is logged and |
| * a <code>RuntimeException</code> is thrown constructed with a |
| * messaged based on the given field and errorMsg parameter. |
| * </p> |
| * |
| * @param expr OGNL expression. |
| * @param field field name used when throwing <code>RuntimeException</code>. |
| * @param errorMsg error message used when throwing <code>RuntimeException</code>. |
| * @return the Object found, is never <tt>null</tt>. |
| * @throws StrutsException is thrown in case of not found in the OGNL stack, or expression is <tt>null</tt>. |
| */ |
| protected Object findValue(String expr, String field, String errorMsg) { |
| if (expr == null) { |
| throw fieldError(field, errorMsg, null); |
| } else { |
| Object value = null; |
| Exception problem = null; |
| try { |
| value = findValue(expr); |
| } catch (Exception e) { |
| problem = e; |
| } |
| |
| if (value == null) { |
| throw fieldError(field, errorMsg, problem); |
| } |
| |
| return value; |
| } |
| } |
| |
| /** |
| * Evaluates the OGNL stack to find an Object of the given type. Will evaluate |
| * <code>expr</code> the portion wrapped with altSyntax (%{...}) |
| * against stack when altSyntax is on, else the whole <code>expr</code> |
| * is evaluated against the stack. |
| * <br> |
| * This method only supports the altSyntax. So this should be set to true. |
| * @param expr OGNL expression. |
| * @param toType the type expected to find. |
| * @return the Object found, or <tt>null</tt> if not found. |
| */ |
| protected Object findValue(String expr, Class<?> toType) { |
| if (altSyntax() && toType == String.class) { |
| if (ComponentUtils.containsExpression(expr)) { |
| return TextParseUtil.translateVariables('%', expr, stack); |
| } else { |
| return expr; |
| } |
| } else { |
| expr = stripExpressionIfAltSyntax(expr); |
| |
| return getStack().findValue(expr, toType, throwExceptionOnELFailure); |
| } |
| } |
| |
| protected TagAttribute findValue(TagAttribute attribute, Class<?> toType) { |
| if (altSyntax() && toType == String.class) { |
| if (attribute.isExpression() && !attribute.isEvaluated()) { |
| String translateVariables = TextParseUtil.translateVariables('%', attribute.getValue(), stack); |
| return TagAttribute.evaluated(translateVariables); |
| } else { |
| return attribute; |
| } |
| } else { |
| Object value = getStack().findValue(attribute.stripedExpression(), toType, throwExceptionOnELFailure); |
| |
| if (value == null) { |
| return TagAttribute.NULL; |
| } else { |
| return TagAttribute.evaluated(String.valueOf(value)); |
| } |
| } |
| } |
| |
| /** |
| * Renders an action URL by consulting the {@link org.apache.struts2.dispatcher.mapper.ActionMapper}. |
| * @param action the action |
| * @param namespace the namespace |
| * @param method the method |
| * @param req HTTP request |
| * @param res HTTP response |
| * @param parameters parameters |
| * @param scheme http or https |
| * @param includeContext should the context path be included or not |
| * @param encodeResult should the url be encoded |
| * @param forceAddSchemeHostAndPort should the scheme host and port be forced |
| * @param escapeAmp should ampersand (&) be escaped to &amp; |
| * @return the action url. |
| */ |
| protected String determineActionURL(String action, String namespace, String method, |
| HttpServletRequest req, HttpServletResponse res, Map parameters, String scheme, |
| boolean includeContext, boolean encodeResult, boolean forceAddSchemeHostAndPort, |
| boolean escapeAmp) { |
| String finalAction = findString(action); |
| String finalMethod = method != null ? findString(method) : null; |
| String finalNamespace = determineNamespace(namespace, getStack(), req); |
| ActionMapping mapping = new ActionMapping(finalAction, finalNamespace, finalMethod, parameters); |
| String uri = actionMapper.getUriFromActionMapping(mapping); |
| return urlHelper.buildUrl(uri, req, res, parameters, scheme, includeContext, encodeResult, forceAddSchemeHostAndPort, escapeAmp); |
| } |
| |
| /** |
| * Determines the namespace of the current page being renderdd. Useful for Form, URL, and href generations. |
| * @param namespace the namespace |
| * @param stack OGNL value stack |
| * @param req HTTP request |
| * @return the namepsace of the current page being rendered, is never <tt>null</tt>. |
| */ |
| protected String determineNamespace(String namespace, ValueStack stack, HttpServletRequest req) { |
| String result; |
| |
| if (namespace == null) { |
| result = TagUtils.buildNamespace(actionMapper, stack, req); |
| } else { |
| result = findString(namespace); |
| } |
| |
| if (result == null) { |
| result = ""; |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Pushes this component's parameter Map as well as the component itself on to the stack |
| * and then copies the supplied parameters over. Because the component's parameter Map is |
| * pushed before the component itself, any key-value pair that can't be assigned to component |
| * will be set in the parameters Map. |
| * |
| * @param params the parameters to copy. |
| */ |
| public void copyParams(Map<String, ?> params) { |
| stack.push(parameters); |
| stack.push(this); |
| try { |
| for (Map.Entry<String, ?> entry : params.entrySet()) { |
| String key = entry.getKey(); |
| |
| if (key.indexOf('-') >= 0) { |
| // UI component attributes may contain hypens (e.g. data-ajax), but ognl |
| // can't handle that, and there can't be a component property with a hypen |
| // so into the parameters map it goes. See WW-4493 |
| parameters.put(key, entry.getValue()); |
| } else { |
| stack.setValue(key, entry.getValue()); |
| } |
| } |
| } finally { |
| stack.pop(); |
| stack.pop(); |
| } |
| } |
| |
| /** |
| * Constructs a string representation of the given exception. |
| * @param t the exception |
| * @return the exception as a string. |
| */ |
| protected String toString(Throwable t) { |
| try (FastByteArrayOutputStream bout = new FastByteArrayOutputStream(); |
| PrintWriter wrt = new PrintWriter(bout)) { |
| t.printStackTrace(wrt); |
| return bout.toString(); |
| } |
| } |
| |
| /** |
| * Gets the parameters. |
| * @return the parameters. Is never <tt>null</tt>. |
| */ |
| public Map<String, Object> getParameters() { |
| return parameters; |
| } |
| |
| /** |
| * Adds all the given parameters to this component's own parameters. |
| * @param params the parameters to add. |
| */ |
| public void addAllParameters(Map params) { |
| parameters.putAll(params); |
| } |
| |
| /** |
| * Adds the given key and value to this component's own parameter. |
| * <br> |
| * If the provided key is <tt>null</tt> nothing happens. |
| * If the provided value is <tt>null</tt> any existing parameter with |
| * the given key name is removed. |
| * @param key the key of the new parameter to add. |
| * @param value the value associated with the key. |
| */ |
| public void addParameter(String key, Object value) { |
| if (key != null) { |
| Map params = getParameters(); |
| |
| if (value == null) { |
| params.remove(key); |
| } else { |
| params.put(key, value); |
| } |
| } |
| } |
| |
| /** |
| * Overwrite to set if body should be used. |
| * @return always false for this component. |
| */ |
| public boolean usesBody() { |
| return false; |
| } |
| |
| /** |
| * Override to set if body content should be HTML-escaped. |
| * |
| * @return always true (default) for this component. |
| * |
| * @since 2.6 |
| */ |
| public boolean escapeHtmlBody() { |
| return true; |
| } |
| |
| /** |
| * Checks if provided name is a valid tag's attribute |
| * |
| * @param attrName String name of attribute |
| * @return true if attribute with the same name was already defined |
| */ |
| public boolean isValidTagAttribute(String attrName) { |
| return getStandardAttributes().contains(attrName); |
| } |
| |
| /** |
| * If needed caches all methods annotated by given annotation to avoid further scans |
| * |
| * @return list of attributes |
| */ |
| protected Collection<String> getStandardAttributes() { |
| Class clz = getClass(); |
| Collection<String> standardAttributes = standardAttributesMap.get(clz); |
| if (standardAttributes == null) { |
| Collection<Method> methods = MethodUtils.getMethodsListWithAnnotation(clz, StrutsTagAttribute.class, |
| true, true); |
| standardAttributes = new HashSet<>(methods.size()); |
| for(Method m : methods) { |
| standardAttributes.add(StringUtils.uncapitalize(m.getName().substring(3))); |
| } |
| standardAttributesMap.putIfAbsent(clz, standardAttributes); |
| } |
| return standardAttributes; |
| } |
| |
| } |