You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by lu...@apache.org on 2020/10/15 05:22:09 UTC

[struts] branch tag-attribute created (now 5077c21)

This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a change to branch tag-attribute
in repository https://gitbox.apache.org/repos/asf/struts.git.


      at 5077c21  Introduces a TagAttribute class as a wrapper around raw String attributes

This branch includes the following new commits:

     new 5077c21  Introduces a TagAttribute class as a wrapper around raw String attributes

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[struts] 01/01: Introduces a TagAttribute class as a wrapper around raw String attributes

Posted by lu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch tag-attribute
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 5077c211187554b4e001f71a3b1c0d99bd4a2183
Author: Lukasz Lenart <lu...@apache.org>
AuthorDate: Thu Oct 15 07:21:57 2020 +0200

    Introduces a TagAttribute class as a wrapper around raw String attributes
---
 .../org/apache/struts2/components/Component.java   | 54 +++++++++++----
 .../apache/struts2/components/DoubleSelect.java    |  2 +-
 .../java/org/apache/struts2/components/Form.java   | 11 +--
 .../org/apache/struts2/components/FormButton.java  | 16 +++--
 .../struts2/components/ServletUrlRenderer.java     | 16 +++--
 .../java/org/apache/struts2/components/UIBean.java | 26 +++----
 .../org/apache/struts2/views/TagAttribute.java     | 79 ++++++++++++++++++++++
 .../views/freemarker/StrutsBeanWrapper.java        | 15 +++-
 .../views/freemarker/TagAttributeAdapter.java      | 47 +++++++++++++
 .../apache/struts2/components/FormButtonTest.java  | 29 ++++----
 .../org/apache/struts2/components/UIBeanTest.java  | 19 +++---
 .../org/apache/struts2/views/java/Attributes.java  |  4 ++
 .../struts2/views/java/simple/CheckboxHandler.java |  6 +-
 .../views/java/simple/DateTextFieldHandler.java    |  3 +-
 .../struts2/components/PortletUrlRenderer.java     | 12 ++--
 15 files changed, 261 insertions(+), 78 deletions(-)

diff --git a/core/src/main/java/org/apache/struts2/components/Component.java b/core/src/main/java/org/apache/struts2/components/Component.java
index e6c13d3..e09048c 100644
--- a/core/src/main/java/org/apache/struts2/components/Component.java
+++ b/core/src/main/java/org/apache/struts2/components/Component.java
@@ -22,8 +22,8 @@ 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.reflect.MethodUtils;
 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;
@@ -32,6 +32,7 @@ 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;
@@ -65,7 +66,7 @@ public class Component {
 
     protected boolean devMode = false;
     protected ValueStack stack;
-    protected Map parameters;
+    protected Map<String, Object> parameters;
     protected ActionMapper actionMapper;
     protected boolean throwExceptionOnELFailure;
     private UrlHelper urlHelper;
@@ -220,6 +221,10 @@ public class Component {
         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>
@@ -276,7 +281,7 @@ public class Component {
     }
 
     /**
-     * If altsyntax (%{...}) is applied, simply strip the "%{" and "}" off. 
+     * 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.
@@ -296,7 +301,7 @@ public class Component {
     /**
      * Adds the surrounding %{ } to the expression for proper processing.
      * @param expr the expression.
-     * @return the modified expression if altSyntax is enabled, or the parameter 
+     * @return the modified expression if altSyntax is enabled, or the parameter
      * expression otherwise.
      */
 	protected String completeExpressionIfAltSyntax(String expr) {
@@ -319,6 +324,13 @@ public class Component {
 		return expr;
 	}
 
+    protected TagAttribute findStringIfAltSyntax(TagAttribute attribute) {
+        if (altSyntax()) {
+            return findString(attribute);
+        }
+        return attribute;
+    }
+
     /**
      * <p>
      * Evaluates the OGNL stack to find an Object value.
@@ -368,7 +380,7 @@ public class Component {
      * @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) {
+    protected Object findValue(String expr, Class<?> toType) {
         if (altSyntax() && toType == String.class) {
             if (ComponentUtils.containsExpression(expr)) {
                 return TextParseUtil.translateVariables('%', expr, stack);
@@ -382,6 +394,25 @@ public class Component {
         }
     }
 
+    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
@@ -440,13 +471,12 @@ public class Component {
      *
      * @param params  the parameters to copy.
      */
-    public void copyParams(Map params) {
+    public void copyParams(Map<String, ?> params) {
         stack.push(parameters);
         stack.push(this);
         try {
-            for (Object o : params.entrySet()) {
-                Map.Entry entry = (Map.Entry) o;
-                String key = (String) entry.getKey();
+            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
@@ -480,7 +510,7 @@ public class Component {
      * Gets the parameters.
      * @return the parameters. Is never <tt>null</tt>.
      */
-    public Map getParameters() {
+    public Map<String, Object> getParameters() {
         return parameters;
     }
 
@@ -523,9 +553,9 @@ public class Component {
 
     /**
      * Override to set if body content should be HTML-escaped.
-     * 
+     *
      * @return always true (default) for this component.
-     * 
+     *
      * @since 2.6
      */
     public boolean escapeHtmlBody() {
diff --git a/core/src/main/java/org/apache/struts2/components/DoubleSelect.java b/core/src/main/java/org/apache/struts2/components/DoubleSelect.java
index a6f96b1..78a9eed 100644
--- a/core/src/main/java/org/apache/struts2/components/DoubleSelect.java
+++ b/core/src/main/java/org/apache/struts2/components/DoubleSelect.java
@@ -57,7 +57,7 @@ public class DoubleSelect extends DoubleListUIBean {
     public void evaluateExtraParams() {
         super.evaluateExtraParams();
         StringBuilder onchangeParam = new StringBuilder();
-        onchangeParam.append(getParameters().get("id")).append("Redirect(this.selectedIndex)");
+        onchangeParam.append(getId().getValue()).append("Redirect(this.selectedIndex)");
         if(StringUtils.isNotEmpty(this.onchange)) {
         	onchangeParam.append(";").append(this.onchange);
         }
diff --git a/core/src/main/java/org/apache/struts2/components/Form.java b/core/src/main/java/org/apache/struts2/components/Form.java
index 9758cf3..a5480dd 100644
--- a/core/src/main/java/org/apache/struts2/components/Form.java
+++ b/core/src/main/java/org/apache/struts2/components/Form.java
@@ -30,6 +30,7 @@ import com.opensymphony.xwork2.validator.*;
 import com.opensymphony.xwork2.validator.validators.VisitorFieldValidator;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.struts2.dispatcher.mapper.ActionMapping;
+import org.apache.struts2.views.TagAttribute;
 import org.apache.struts2.views.annotations.StrutsTag;
 import org.apache.struts2.views.annotations.StrutsTagAttribute;
 import org.apache.struts2.views.jsp.TagUtils;
@@ -169,9 +170,9 @@ public class Form extends ClosingUIBean {
 
         if (name == null) {
             //make the name the same as the id
-            String id = (String) getParameters().get("id");
-             if (StringUtils.isNotEmpty(id)) {
-                addParameter("name", id);
+            TagAttribute id = (TagAttribute) getParameters().get("id");
+             if (id != null && !id.isNull()) {
+                addParameter("name", id.getValue());
              }
         }
 
@@ -220,8 +221,8 @@ public class Form extends ClosingUIBean {
      */
     @Override
     protected void populateComponentHtmlId(Form form) {
-        if (id != null) {
-            addParameter("id", escape(id));
+        if (!id.isNull()) {
+            addParameter("id", id.escaped());
         }
 
         // if no id given, it will be tried to generate it from the action attribute
diff --git a/core/src/main/java/org/apache/struts2/components/FormButton.java b/core/src/main/java/org/apache/struts2/components/FormButton.java
index 7dcc7ae..4340b01 100644
--- a/core/src/main/java/org/apache/struts2/components/FormButton.java
+++ b/core/src/main/java/org/apache/struts2/components/FormButton.java
@@ -21,6 +21,7 @@ package org.apache.struts2.components;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.apache.struts2.views.TagAttribute;
 import org.apache.struts2.views.annotations.StrutsTagAttribute;
 import org.apache.struts2.dispatcher.mapper.ActionMapper;
 import org.apache.struts2.dispatcher.mapper.ActionMapping;
@@ -98,32 +99,33 @@ public abstract class FormButton extends ClosingUIBean {
      * </ol>
      */
     protected void populateComponentHtmlId(Form form) {
-        String _tmp_id = "";
-        if (id != null) {
+        TagAttribute _tmp_id = TagAttribute.EMPTY;
+        if (!id.isNull()) {
             // this check is needed for backwards compatibility with 2.1.x
         	_tmp_id = findStringIfAltSyntax(id);
         }
         else {
             if (form != null && form.getParameters().get("id") != null) {
-                _tmp_id = _tmp_id + form.getParameters().get("id").toString() + "_";
+                _tmp_id = _tmp_id.append(((TagAttribute)form.getParameters().get("id")).getValue() + "_");
             }
             if (name != null) {
-                _tmp_id = _tmp_id + escape(name);
+                _tmp_id = _tmp_id.append(escape(name));
             } else if (action != null || method != null){
                 if (action != null) {
-                    _tmp_id = _tmp_id + escape(action);
+                    _tmp_id = _tmp_id.append(escape(action));
                 }
                 if (method != null) {
-                    _tmp_id = _tmp_id + "_" + escape(method);
+                    _tmp_id = _tmp_id.append("_" + escape(method));
                 }
             } else {
                 // if form is null, this component is used, without a form, i guess
                 // there's not much we could do then.
                 if (form != null) {
-                    _tmp_id = _tmp_id + form.getSequence();
+                    _tmp_id = _tmp_id.append(String.valueOf(form.getSequence()));
                 }
             }
         }
+        this.id = _tmp_id;
         addParameter("id", _tmp_id);
     }
 
diff --git a/core/src/main/java/org/apache/struts2/components/ServletUrlRenderer.java b/core/src/main/java/org/apache/struts2/components/ServletUrlRenderer.java
index b8b7a3c..db7ca58 100644
--- a/core/src/main/java/org/apache/struts2/components/ServletUrlRenderer.java
+++ b/core/src/main/java/org/apache/struts2/components/ServletUrlRenderer.java
@@ -29,6 +29,7 @@ import org.apache.logging.log4j.Logger;
 import org.apache.struts2.StrutsException;
 import org.apache.struts2.dispatcher.mapper.ActionMapper;
 import org.apache.struts2.dispatcher.mapper.ActionMapping;
+import org.apache.struts2.views.TagAttribute;
 import org.apache.struts2.views.util.UrlHelper;
 
 import java.io.IOException;
@@ -186,8 +187,9 @@ public class ServletUrlRenderer implements UrlRenderer {
             }
 
             // if the id isn't specified, use the action name
-            if (formComponent.getId() == null && actionName != null) {
-                formComponent.addParameter("id", formComponent.escape(actionName));
+            TagAttribute id = formComponent.getId();
+            if (id.isNull() && actionName != null) {
+                formComponent.addParameter("id", TagAttribute.evaluated(actionName).escaped());
             }
         } else if (action != null) {
             // Since we can't find an action alias in the configuration, we just
@@ -213,16 +215,16 @@ public class ServletUrlRenderer implements UrlRenderer {
 
             // name/id: cut out anything between / and . should be the id and
             // name
-            String id = formComponent.getId();
-            if (id == null) {
+            TagAttribute id = formComponent.getId();
+            if (id == null || id.isNull()) {
                 slash = result.lastIndexOf('/');
                 int dot = result.indexOf('.', slash);
                 if (dot != -1) {
-                    id = result.substring(slash + 1, dot);
+                    id = TagAttribute.evaluated(result.substring(slash + 1, dot));
                 } else {
-                    id = result.substring(slash + 1);
+                    id = TagAttribute.evaluated(result.substring(slash + 1));
                 }
-                formComponent.addParameter("id", formComponent.escape(id));
+                formComponent.addParameter("id", id.escaped());
             }
         }
 
diff --git a/core/src/main/java/org/apache/struts2/components/UIBean.java b/core/src/main/java/org/apache/struts2/components/UIBean.java
index 0e44a3b..4ca5c00 100644
--- a/core/src/main/java/org/apache/struts2/components/UIBean.java
+++ b/core/src/main/java/org/apache/struts2/components/UIBean.java
@@ -31,6 +31,7 @@ import org.apache.struts2.components.template.TemplateEngine;
 import org.apache.struts2.components.template.TemplateEngineManager;
 import org.apache.struts2.components.template.TemplateRenderingContext;
 import org.apache.struts2.util.TextProviderHelper;
+import org.apache.struts2.views.TagAttribute;
 import org.apache.struts2.views.annotations.StrutsTagAttribute;
 import org.apache.struts2.views.util.ContextUtil;
 
@@ -456,7 +457,7 @@ public abstract class UIBean extends Component {
     // shortcut, sets label, name, and value
     protected String key;
 
-    protected String id;
+    protected TagAttribute id = TagAttribute.NULL;
     protected String cssClass;
     protected String cssStyle;
     protected String cssErrorClass;
@@ -989,25 +990,26 @@ public abstract class UIBean extends Component {
      * @param form enclosing form tag
      */
     protected void populateComponentHtmlId(Form form) {
-        String tryId;
+        TagAttribute tryId;
         String generatedId;
-        if (id != null) {
+        if (id != null && !id.isNull()) {
             // this check is needed for backwards compatibility with 2.1.x
             tryId = findStringIfAltSyntax(id);
         } else if (null == (generatedId = escape(name != null ? findString(name) : null))) {
             LOG.debug("Cannot determine id attribute for [{}], consider defining id, name or key attribute!", this);
-            tryId = null;
+            tryId = TagAttribute.NULL;
         } else if (form != null) {
-            tryId = form.getParameters().get("id") + "_" + generatedId;
+            tryId = TagAttribute.evaluated(((TagAttribute)form.getParameters().get("id")).getValue() + "_" + generatedId);
         } else {
-            tryId = generatedId;
+            tryId = TagAttribute.evaluated(generatedId);
         }
 
         //fix for https://issues.apache.org/jira/browse/WW-4299
         //do not assign value to id if tryId is null
-        if (tryId != null) {
-          addParameter("id", tryId);
-          addParameter("escapedId", escape(tryId));
+        if (!tryId.isNull()) {
+            id = tryId;
+            addParameter("id", tryId);
+            addParameter("escapedId", tryId.escaped());
         }
     }
 
@@ -1015,15 +1017,13 @@ public abstract class UIBean extends Component {
      * Get's the id for referencing element.
      * @return the id for referencing element.
      */
-    public String getId() {
+    public TagAttribute getId() {
         return id;
     }
 
     @StrutsTagAttribute(description="HTML id attribute")
     public void setId(String id) {
-        if (id != null) {
-            this.id = findString(id);
-        }
+        this.id = TagAttribute.raw(id);
     }
 
     @StrutsTagAttribute(description="The template directory.")
diff --git a/core/src/main/java/org/apache/struts2/views/TagAttribute.java b/core/src/main/java/org/apache/struts2/views/TagAttribute.java
new file mode 100644
index 0000000..42daa38
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/views/TagAttribute.java
@@ -0,0 +1,79 @@
+/*
+ * 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.views;
+
+public class TagAttribute {
+
+    public static final TagAttribute NULL = new TagAttribute(null, true);
+    public static final TagAttribute EMPTY = new TagAttribute("", true);
+
+    private final String value;
+    private final boolean evaluated;
+
+    private TagAttribute(String value, boolean evaluated) {
+        this.value = value;
+        this.evaluated = evaluated;
+    }
+
+    public static TagAttribute raw(String value) {
+        return new TagAttribute(value, false);
+    }
+
+    public static TagAttribute evaluated(String evaluatedValue) {
+        return new TagAttribute(evaluatedValue, true);
+    }
+
+    public boolean isExpression() {
+        return value != null && value.contains("%{") && value.contains("}");
+    }
+
+    public String stripedExpression() {
+        if (isExpression()) {
+            return value.substring(2, value.length() - 1);
+        } else {
+            return value;
+        }
+    }
+
+    public TagAttribute escaped(){
+        // escape any possible values that can make the ID painful to work with in JavaScript
+        if (value != null) {
+            return TagAttribute.evaluated(value.replaceAll("[\\/\\.\\[\\]\'\"]", "_"));
+        } else {
+            return null;
+        }
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public boolean isEvaluated() {
+        return evaluated;
+    }
+
+    public boolean isNull() {
+        return value == null;
+    }
+
+    public TagAttribute append(String appendString) {
+        return TagAttribute.evaluated(value + appendString);
+    }
+
+}
diff --git a/core/src/main/java/org/apache/struts2/views/freemarker/StrutsBeanWrapper.java b/core/src/main/java/org/apache/struts2/views/freemarker/StrutsBeanWrapper.java
index 3f94f96..dc038ce 100644
--- a/core/src/main/java/org/apache/struts2/views/freemarker/StrutsBeanWrapper.java
+++ b/core/src/main/java/org/apache/struts2/views/freemarker/StrutsBeanWrapper.java
@@ -30,7 +30,9 @@ import freemarker.template.SimpleSequence;
 import freemarker.template.TemplateCollectionModel;
 import freemarker.template.TemplateHashModelEx;
 import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
 import freemarker.template.Version;
+import org.apache.struts2.views.TagAttribute;
 
 /**
  * <!-- START SNIPPET: javadoc -->
@@ -49,7 +51,8 @@ import freemarker.template.Version;
  * <!-- END SNIPPET: javadoc -->
  */
 public class StrutsBeanWrapper extends BeansWrapper {
-    private boolean altMapWrapper;
+
+    private final boolean altMapWrapper;
 
     public StrutsBeanWrapper(boolean altMapWrapper, Version incompatibleImprovements) {
         super(incompatibleImprovements);
@@ -65,6 +68,16 @@ public class StrutsBeanWrapper extends BeansWrapper {
         return super.getModelFactory(clazz);
     }
 
+    @Override
+    public TemplateModel wrap(final Object obj) throws TemplateModelException {
+        if (obj instanceof TagAttribute) {
+            TagAttribute attribute = (TagAttribute) obj;
+            return new TagAttributeAdapter(attribute, this);
+        }
+
+        return super.wrap(obj);
+    }
+
     /**
      * Attempting to get the best of both worlds of FM's MapModel and SimpleMapModel, by reimplementing the isEmpty(),
      * keySet() and values() methods. ?keys and ?values built-ins are thus available, just as well as plain Map
diff --git a/core/src/main/java/org/apache/struts2/views/freemarker/TagAttributeAdapter.java b/core/src/main/java/org/apache/struts2/views/freemarker/TagAttributeAdapter.java
new file mode 100644
index 0000000..81686a5
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/views/freemarker/TagAttributeAdapter.java
@@ -0,0 +1,47 @@
+/*
+ * 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.views.freemarker;
+
+import freemarker.template.AdapterTemplateModel;
+import freemarker.template.ObjectWrapper;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateScalarModel;
+import freemarker.template.WrappingTemplateModel;
+import org.apache.struts2.views.TagAttribute;
+
+public class TagAttributeAdapter extends WrappingTemplateModel implements AdapterTemplateModel, TemplateScalarModel {
+
+    private final TagAttribute attribute;
+
+    public TagAttributeAdapter(TagAttribute attribute, ObjectWrapper ow) {
+        super(ow);
+        this.attribute = attribute;
+    }
+
+    @Override
+    public Object getAdaptedObject(Class<?> hint) {
+        return attribute;
+    }
+
+    @Override
+    public String getAsString() throws TemplateModelException {
+        return attribute.getValue();
+    }
+
+}
diff --git a/core/src/test/java/org/apache/struts2/components/FormButtonTest.java b/core/src/test/java/org/apache/struts2/components/FormButtonTest.java
index bf0fe95..2620542 100644
--- a/core/src/test/java/org/apache/struts2/components/FormButtonTest.java
+++ b/core/src/test/java/org/apache/struts2/components/FormButtonTest.java
@@ -19,6 +19,7 @@
 package org.apache.struts2.components;
 
 import org.apache.struts2.StrutsInternalTestCase;
+import org.apache.struts2.views.TagAttribute;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 
@@ -31,7 +32,7 @@ import com.opensymphony.xwork2.util.ValueStack;
  */
 public class FormButtonTest extends StrutsInternalTestCase {
 
-    public void testPopulateComponentHtmlId1() throws Exception {
+    public void testPopulateComponentHtmlId1() {
         MockHttpServletRequest req = new MockHttpServletRequest();
         MockHttpServletResponse res = new MockHttpServletResponse();
         ValueStack stack = ActionContext.getContext().getValueStack();
@@ -44,32 +45,32 @@ public class FormButtonTest extends StrutsInternalTestCase {
 
         submit.populateComponentHtmlId(form);
 
-        assertEquals("submitId", submit.getParameters().get("id"));
+        assertEquals("submitId", submit.getId().getValue());
     }
 
-    public void testPopulateComponentHtmlId2() throws Exception {
+    public void testPopulateComponentHtmlId2() {
         MockHttpServletRequest req = new MockHttpServletRequest();
         MockHttpServletResponse res = new MockHttpServletResponse();
         ValueStack stack = ActionContext.getContext().getValueStack();
 
         Form form = new Form(stack, req, res);
-        form.getParameters().put("id", "formId");
+        form.getParameters().put("id", TagAttribute.evaluated("formId"));
 
         Submit submit = new Submit(stack, req, res);
         submit.setName("submitName");
 
         submit.populateComponentHtmlId(form);
 
-        assertEquals("formId_submitName", submit.getParameters().get("id"));
+        assertEquals("formId_submitName", submit.getId().getValue());
     }
 
-    public void testPopulateComponentHtmlId3() throws Exception {
+    public void testPopulateComponentHtmlId3() {
         MockHttpServletRequest req = new MockHttpServletRequest();
         MockHttpServletResponse res = new MockHttpServletResponse();
         ValueStack stack = ActionContext.getContext().getValueStack();
 
         Form form = new Form(stack, req, res);
-        form.getParameters().put("id", "formId");
+        form.getParameters().put("id", TagAttribute.evaluated("formId"));
 
         Submit submit = new Submit(stack, req, res);
         submit.setAction("submitAction");
@@ -77,10 +78,10 @@ public class FormButtonTest extends StrutsInternalTestCase {
 
         submit.populateComponentHtmlId(form);
 
-        assertEquals("formId_submitAction_submitMethod", submit.getParameters().get("id"));
+        assertEquals("formId_submitAction_submitMethod", submit.getId().getValue());
     }
 
-    public void testPopulateComponentHtmlId4() throws Exception {
+    public void testPopulateComponentHtmlId4() {
         MockHttpServletRequest req = new MockHttpServletRequest();
         MockHttpServletResponse res = new MockHttpServletResponse();
         ValueStack stack = ActionContext.getContext().getValueStack();
@@ -90,10 +91,10 @@ public class FormButtonTest extends StrutsInternalTestCase {
 
         submit.populateComponentHtmlId(null);
 
-        assertEquals("submitId", submit.getParameters().get("id"));
+        assertEquals("submitId", submit.getId().getValue());
     }
 
-    public void testPopulateComponentHtmlId5() throws Exception {
+    public void testPopulateComponentHtmlId5() {
         MockHttpServletRequest req = new MockHttpServletRequest();
         MockHttpServletResponse res = new MockHttpServletResponse();
         ValueStack stack = ActionContext.getContext().getValueStack();
@@ -103,10 +104,10 @@ public class FormButtonTest extends StrutsInternalTestCase {
 
         submit.populateComponentHtmlId(null);
 
-        assertEquals("submitName", submit.getParameters().get("id"));
+        assertEquals("submitName", submit.getId().getValue());
     }
 
-    public void testPopulateComponentHtmlId6() throws Exception {
+    public void testPopulateComponentHtmlId6() {
         MockHttpServletRequest req = new MockHttpServletRequest();
         MockHttpServletResponse res = new MockHttpServletResponse();
         ValueStack stack = ActionContext.getContext().getValueStack();
@@ -117,6 +118,6 @@ public class FormButtonTest extends StrutsInternalTestCase {
 
         submit.populateComponentHtmlId(null);
 
-        assertEquals("submitAction_submitMethod", submit.getParameters().get("id"));
+        assertEquals("submitAction_submitMethod", submit.getId().getValue());
     }
 }
diff --git a/core/src/test/java/org/apache/struts2/components/UIBeanTest.java b/core/src/test/java/org/apache/struts2/components/UIBeanTest.java
index 9317397..5977f3a 100644
--- a/core/src/test/java/org/apache/struts2/components/UIBeanTest.java
+++ b/core/src/test/java/org/apache/struts2/components/UIBeanTest.java
@@ -25,6 +25,7 @@ import org.apache.struts2.StrutsInternalTestCase;
 import org.apache.struts2.components.template.Template;
 import org.apache.struts2.components.template.TemplateEngine;
 import org.apache.struts2.components.template.TemplateEngineManager;
+import org.apache.struts2.views.TagAttribute;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 
@@ -40,14 +41,14 @@ public class UIBeanTest extends StrutsInternalTestCase {
         MockHttpServletResponse res = new MockHttpServletResponse();
 
         Form form = new Form(stack, req, res);
-        form.getParameters().put("id", "formId");
+        form.getParameters().put("id", TagAttribute.evaluated("formId"));
 
         TextField txtFld = new TextField(stack, req, res);
         txtFld.setId("txtFldId");
 
         txtFld.populateComponentHtmlId(form);
 
-        assertEquals("txtFldId", txtFld.getParameters().get("id"));
+        assertEquals("txtFldId", txtFld.getId().getValue());
     }
 
     public void testPopulateComponentHtmlIdWithOgnl() throws Exception {
@@ -56,14 +57,14 @@ public class UIBeanTest extends StrutsInternalTestCase {
         MockHttpServletResponse res = new MockHttpServletResponse();
 
         Form form = new Form(stack, req, res);
-        form.getParameters().put("id", "formId");
+        form.getParameters().put("id", TagAttribute.evaluated("formId"));
 
         TextField txtFld = new TextField(stack, req, res);
         txtFld.setName("txtFldName%{'1'}");
 
         txtFld.populateComponentHtmlId(form);
 
-        assertEquals("formId_txtFldName1", txtFld.getParameters().get("id"));
+        assertEquals("formId_txtFldName1", txtFld.getId().getValue());
     }
 
     public void testPopulateComponentHtmlId2() throws Exception {
@@ -72,14 +73,14 @@ public class UIBeanTest extends StrutsInternalTestCase {
         MockHttpServletResponse res = new MockHttpServletResponse();
 
         Form form = new Form(stack, req, res);
-        form.getParameters().put("id", "formId");
+        form.getParameters().put("id", TagAttribute.evaluated("formId"));
 
         TextField txtFld = new TextField(stack, req, res);
         txtFld.setName("txtFldName");
 
         txtFld.populateComponentHtmlId(form);
 
-        assertEquals("formId_txtFldName", txtFld.getParameters().get("id"));
+        assertEquals("formId_txtFldName", txtFld.getId().getValue());
     }
 
     public void testPopulateComponentHtmlWithoutNameAndId() throws Exception {
@@ -94,7 +95,7 @@ public class UIBeanTest extends StrutsInternalTestCase {
 
         txtFld.populateComponentHtmlId(form);
 
-        assertEquals(null, txtFld.getParameters().get("id"));
+        assertNull(txtFld.getParameters().get("id"));
     }
 
     public void testEscape() throws Exception {
@@ -120,12 +121,12 @@ public class UIBeanTest extends StrutsInternalTestCase {
         MockHttpServletResponse res = new MockHttpServletResponse();
 
         Form form = new Form(stack, req, res);
-        form.getParameters().put("id", "formId");
+        form.getParameters().put("id", TagAttribute.evaluated("formId"));
 
         TextField txtFld = new TextField(stack, req, res);
         txtFld.setName("foo/bar");
         txtFld.populateComponentHtmlId(form);
-        assertEquals("formId_foo_bar", txtFld.getParameters().get("id"));
+        assertEquals("formId_foo_bar", txtFld.getId().getValue());
     }
 
     public void testGetThemeFromForm() throws Exception {
diff --git a/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/Attributes.java b/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/Attributes.java
index 3a60507..5e6f370 100644
--- a/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/Attributes.java
+++ b/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/Attributes.java
@@ -20,6 +20,7 @@ package org.apache.struts2.views.java;
 
 import org.apache.commons.text.StringEscapeUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.struts2.views.TagAttribute;
 
 import java.util.LinkedHashMap;
 
@@ -60,6 +61,9 @@ public class Attributes extends LinkedHashMap<String, String> {
     public Attributes addIfExists(String attrName, Object paramValue, boolean encode) {
         if (paramValue != null) {
             String val = paramValue.toString();
+            if (paramValue instanceof TagAttribute) {
+                val = ((TagAttribute) paramValue).getValue();
+            }
             if (StringUtils.isNotBlank(val))
                 put(attrName, (encode ? StringUtils.defaultString(StringEscapeUtils.escapeHtml4(val)) : val));
         }
diff --git a/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/CheckboxHandler.java b/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/CheckboxHandler.java
index c712dda..f01794c 100644
--- a/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/CheckboxHandler.java
+++ b/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/CheckboxHandler.java
@@ -18,6 +18,7 @@
  */
 package org.apache.struts2.views.java.simple;
 
+import org.apache.struts2.views.TagAttribute;
 import org.apache.struts2.views.java.Attributes;
 import org.apache.struts2.views.java.TagGenerator;
 import org.apache.commons.lang3.StringUtils;
@@ -32,7 +33,7 @@ public class CheckboxHandler extends AbstractTagHandler implements TagGenerator
         Attributes attrs = new Attributes();
 
         String fieldValue = (String) params.get("fieldValue");
-        String id = (String) params.get("id");
+        TagAttribute id = (TagAttribute) params.get("id");
         String name = (String) params.get("name");
         Object disabled = params.get("disabled");
 
@@ -52,8 +53,9 @@ public class CheckboxHandler extends AbstractTagHandler implements TagGenerator
 
         //hidden input
         attrs = new Attributes();
+        String idStr = id != null ? id.getValue() : null;
         attrs.add("type", "hidden")
-                .add("id", "__checkbox_" + StringUtils.defaultString(StringEscapeUtils.escapeHtml4(id)))
+                .add("id", "__checkbox_" + StringUtils.defaultString(StringEscapeUtils.escapeHtml4(idStr)))
                 .add("name", "__checkbox_" + StringUtils.defaultString(StringEscapeUtils.escapeHtml4(name)))
                 .add("value", "__checkbox_" + StringUtils.defaultString(StringEscapeUtils.escapeHtml4(fieldValue)))
                 .addIfTrue("disabled", disabled);
diff --git a/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/DateTextFieldHandler.java b/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/DateTextFieldHandler.java
index 7c93a6b..91963ba 100644
--- a/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/DateTextFieldHandler.java
+++ b/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/DateTextFieldHandler.java
@@ -24,6 +24,7 @@ import java.util.Date;
 import java.util.Map;
 
 import org.apache.struts2.interceptor.DateTextFieldInterceptor.DateWord;
+import org.apache.struts2.views.TagAttribute;
 import org.apache.struts2.views.java.Attributes;
 import org.apache.struts2.views.java.TagGenerator;
 
@@ -37,7 +38,7 @@ public class DateTextFieldHandler extends AbstractTagHandler implements TagGener
 
         // Get format
         String format = (String)params.get("format");
-        String id = (String)params.get("id");
+        String id = params.get("id") != null ? ((TagAttribute) params.get("id")).getValue() : null;
         String name = (String)params.get("name");
         if (id == null) {
         	id = name;
diff --git a/plugins/portlet/src/main/java/org/apache/struts2/components/PortletUrlRenderer.java b/plugins/portlet/src/main/java/org/apache/struts2/components/PortletUrlRenderer.java
index 4b51f7e..4783581 100644
--- a/plugins/portlet/src/main/java/org/apache/struts2/components/PortletUrlRenderer.java
+++ b/plugins/portlet/src/main/java/org/apache/struts2/components/PortletUrlRenderer.java
@@ -18,7 +18,6 @@
  */
 package org.apache.struts2.components;
 
-import com.opensymphony.xwork2.ActionContext;
 import com.opensymphony.xwork2.ActionInvocation;
 import com.opensymphony.xwork2.inject.Inject;
 import org.apache.commons.lang3.StringUtils;
@@ -27,6 +26,7 @@ import org.apache.struts2.dispatcher.mapper.ActionMapper;
 import org.apache.struts2.portlet.context.PortletActionContext;
 import org.apache.struts2.portlet.util.PortletUrlHelper;
 import org.apache.struts2.portlet.util.PortletUrlHelperJSR286;
+import org.apache.struts2.views.TagAttribute;
 import org.apache.struts2.views.util.UrlHelper;
 
 import javax.portlet.PortletMode;
@@ -183,16 +183,16 @@ public class PortletUrlRenderer implements UrlRenderer {
 
             // name/id: cut out anything between / and . should be the id and
             // name
-            String id = formComponent.getId();
-            if (id == null) {
+            TagAttribute id = formComponent.getId();
+            if (id.isNull()) {
                 int slash = action.lastIndexOf('/');
                 int dot = action.indexOf('.', slash);
                 if (dot != -1) {
-                    id = action.substring(slash + 1, dot);
+                    id = TagAttribute.evaluated(action.substring(slash + 1, dot));
                 } else {
-                    id = action.substring(slash + 1);
+                    id = TagAttribute.evaluated(action.substring(slash + 1));
                 }
-                formComponent.addParameter("id", formComponent.escape(id));
+                formComponent.addParameter("id", id.escaped());
             }
         }
     }