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:10 UTC

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

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());
             }
         }
     }