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 2022/11/06 18:04:57 UTC

[struts] branch WW-5259-parser updated (998991eaa -> 47e65e819)

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

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


 discard 998991eaa WW-5259 Extracts UrlHelper#parseQueryString into a dedicated bean
     new 47e65e819 WW-5259 Extracts UrlHelper#parseQueryString into a dedicated bean

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (998991eaa)
            \
             N -- N -- N   refs/heads/WW-5259-parser (47e65e819)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

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.


Summary of changes:
 .../main/java/org/apache/struts2/url/QueryStringBuilder.java |  3 ++-
 .../main/java/org/apache/struts2/url/QueryStringParser.java  |  3 ++-
 .../java/org/apache/struts2/url/StrutsQueryStringParser.java |  3 +--
 core/src/main/java/org/apache/struts2/url/UrlDecoder.java    |  4 +++-
 core/src/main/java/org/apache/struts2/url/UrlEncoder.java    |  4 +++-
 .../org/apache/struts2/url/StrutsQueryStringParserTest.java  | 12 ++++++------
 .../struts2/portlet/result/PortletActionRedirectResult.java  |  2 +-
 7 files changed, 18 insertions(+), 13 deletions(-)


[struts] 01/01: WW-5259 Extracts UrlHelper#parseQueryString into a dedicated bean

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

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

commit 47e65e819eaee5ff475bba0070940173942bd562
Author: Lukasz Lenart <lu...@apache.org>
AuthorDate: Sun Nov 6 18:50:43 2022 +0100

    WW-5259 Extracts UrlHelper#parseQueryString into a dedicated bean
---
 .../StrutsDefaultConfigurationProvider.java        |   9 +-
 .../java/org/apache/struts2/StrutsConstants.java   |   3 +-
 .../struts2/components/ExtraParameterProvider.java |   4 +-
 .../struts2/components/ServletUrlRenderer.java     |  29 ++-
 .../org/apache/struts2/components/UrlProvider.java |  22 +-
 .../config/StrutsBeanSelectionProvider.java        |   6 +-
 .../struts2/result/ServletDispatcherResult.java    |  14 +-
 .../struts2/result/ServletRedirectResult.java      |  19 +-
 ...sStringBuilder.java => QueryStringBuilder.java} |  13 +-
 ...rsStringBuilder.java => QueryStringParser.java} |   7 +-
 ...gBuilder.java => StrutsQueryStringBuilder.java} |   8 +-
 .../struts2/url/StrutsQueryStringParser.java       |  98 ++++++++
 .../java/org/apache/struts2/url/UrlDecoder.java    |   8 +-
 .../java/org/apache/struts2/url/UrlEncoder.java    |   8 +-
 .../struts2/views/util/DefaultUrlHelper.java       |  73 ++----
 .../org/apache/struts2/views/util/UrlHelper.java   |  18 +-
 .../org/apache/struts2/default.properties          |  13 +-
 core/src/main/resources/struts-default.xml         |   6 +-
 .../result/ServletActionRedirectResultTest.java    |  11 +-
 .../result/ServletDispatcherResultTest.java        |  21 +-
 .../struts2/result/ServletRedirectResultTest.java  |  10 +-
 ...Test.java => StrutsQueryStringBuilderTest.java} |  14 +-
 .../struts2/url/StrutsQueryStringParserTest.java   |  84 +++++++
 .../struts2/views/util/DefaultUrlHelperTest.java   |  36 +--
 .../main/java/org/apache/struts2/JSPRuntime.java   |  13 +-
 .../org/apache/struts2/EmbeddedJSPResultTest.java  |  24 +-
 .../struts2/json/JSONActionRedirectResultTest.java |  19 +-
 .../struts2/components/PortletUrlRenderer.java     |  77 ++++---
 .../result/PortletActionRedirectResult.java        | 247 +++++++++++----------
 29 files changed, 537 insertions(+), 377 deletions(-)

diff --git a/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java b/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java
index d193c4d24..1f49cc11b 100644
--- a/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java
+++ b/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java
@@ -118,8 +118,10 @@ import org.apache.struts2.conversion.StrutsTypeConverterCreator;
 import org.apache.struts2.conversion.StrutsTypeConverterHolder;
 import org.apache.struts2.dispatcher.HttpParameters;
 import org.apache.struts2.dispatcher.Parameter;
-import org.apache.struts2.url.ParametersStringBuilder;
-import org.apache.struts2.url.StrutsParametersStringBuilder;
+import org.apache.struts2.url.QueryStringBuilder;
+import org.apache.struts2.url.QueryStringParser;
+import org.apache.struts2.url.StrutsQueryStringBuilder;
+import org.apache.struts2.url.StrutsQueryStringParser;
 import org.apache.struts2.url.StrutsUrlDecoder;
 import org.apache.struts2.url.StrutsUrlEncoder;
 import org.apache.struts2.url.UrlDecoder;
@@ -236,7 +238,8 @@ public class StrutsDefaultConfigurationProvider implements ConfigurationProvider
 
             .factory(ValueSubstitutor.class, EnvsValueSubstitutor.class, Scope.SINGLETON)
 
-            .factory(ParametersStringBuilder.class, StrutsParametersStringBuilder.class, Scope.SINGLETON)
+            .factory(QueryStringBuilder.class, StrutsQueryStringBuilder.class, Scope.SINGLETON)
+            .factory(QueryStringParser.class, StrutsQueryStringParser.class, Scope.SINGLETON)
             .factory(UrlEncoder.class, StrutsUrlEncoder.class, Scope.SINGLETON)
             .factory(UrlDecoder.class, StrutsUrlDecoder.class, Scope.SINGLETON)
         ;
diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java b/core/src/main/java/org/apache/struts2/StrutsConstants.java
index 7f6955648..b7dec53b2 100644
--- a/core/src/main/java/org/apache/struts2/StrutsConstants.java
+++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java
@@ -458,7 +458,8 @@ public final class StrutsConstants {
     /** See {@link org.apache.struts2.components.Date#setDateFormatter(DateFormatter)} */
     public static final String STRUTS_DATE_FORMATTER = "struts.date.formatter";
 
-    public static final String STRUTS_URL_PARAMETERS_STRING_BUILDER = "struts.url.parametersStringBuilder";
+    public static final String STRUTS_URL_QUERY_STRING_BUILDER = "struts.url.queryStringBuilder";
+    public static final String STRUTS_URL_QUERY_STRING_PARSER = "struts.url.queryStringParser";
     public static final String STRUTS_URL_ENCODER = "struts.url.encoder";
     public static final String STRUTS_URL_DECODER = "struts.url.decoder";
 }
diff --git a/core/src/main/java/org/apache/struts2/components/ExtraParameterProvider.java b/core/src/main/java/org/apache/struts2/components/ExtraParameterProvider.java
index 9734c6b6b..1d3719550 100644
--- a/core/src/main/java/org/apache/struts2/components/ExtraParameterProvider.java
+++ b/core/src/main/java/org/apache/struts2/components/ExtraParameterProvider.java
@@ -21,5 +21,7 @@ package org.apache.struts2.components;
 import java.util.Map;
 
 public interface ExtraParameterProvider {
-    public Map getExtraParameters();
+
+    Map<String, Object> getExtraParameters();
+
 }
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 18831e9ce..d5281a8a0 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.url.QueryStringParser;
 import org.apache.struts2.views.util.UrlHelper;
 
 import java.io.IOException;
@@ -48,6 +49,7 @@ public class ServletUrlRenderer implements UrlRenderer {
 
     private ActionMapper actionMapper;
     private UrlHelper urlHelper;
+    private QueryStringParser queryStringParser;
 
     @Override
     @Inject
@@ -60,6 +62,11 @@ public class ServletUrlRenderer implements UrlRenderer {
         this.urlHelper = urlHelper;
     }
 
+    @Inject
+    public void setQueryStringParser(QueryStringParser queryStringParser) {
+        this.queryStringParser = queryStringParser;
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -152,10 +159,10 @@ public class ServletUrlRenderer implements UrlRenderer {
             }
         }
 
-        Map actionParams = null;
+        Map<String, Object> actionParams = null;
         if (action != null && action.indexOf('?') > 0) {
             String queryString = action.substring(action.indexOf('?') + 1);
-            actionParams = urlHelper.parseQueryString(queryString, false);
+            actionParams = queryStringParser.parse(queryString, false);
             action = action.substring(0, action.indexOf('?'));
         }
 
@@ -164,19 +171,19 @@ public class ServletUrlRenderer implements UrlRenderer {
         String actionMethod = nameMapping.getMethod();
 
         final ActionConfig actionConfig = formComponent.configuration.getRuntimeConfiguration().getActionConfig(
-                namespace, actionName);
+            namespace, actionName);
         if (actionConfig != null) {
 
             ActionMapping mapping = new ActionMapping(actionName, namespace, actionMethod, formComponent.parameters);
             String result = urlHelper.buildUrl(formComponent.actionMapper.getUriFromActionMapping(mapping),
-                    formComponent.request, formComponent.response, actionParams, scheme, formComponent.includeContext, true, false, false);
+                formComponent.request, formComponent.response, actionParams, scheme, formComponent.includeContext, true, false, false);
             formComponent.addParameter("action", result);
 
             // let's try to get the actual action class and name
             // this can be used for getting the list of validators
             formComponent.addParameter("actionName", actionName);
             try {
-                Class clazz = formComponent.objectFactory.getClassInstance(actionConfig.getClassName());
+                Class<?> clazz = formComponent.objectFactory.getClassInstance(actionConfig.getClassName());
                 formComponent.addParameter("actionClass", clazz);
             } catch (ClassNotFoundException e) {
                 // this is OK, we'll just move on
@@ -258,7 +265,7 @@ public class ServletUrlRenderer implements UrlRenderer {
             }
 
             if (UrlProvider.NONE.equalsIgnoreCase(includeParams)) {
-                mergeRequestParameters(urlComponent.getValue(), urlComponent.getParameters(), Collections.<String, Object>emptyMap());
+                mergeRequestParameters(urlComponent.getValue(), urlComponent.getParameters(), Collections.emptyMap());
             } else if (UrlProvider.ALL.equalsIgnoreCase(includeParams)) {
                 mergeRequestParameters(urlComponent.getValue(), urlComponent.getParameters(), urlComponent.getHttpServletRequest().getParameterMap());
 
@@ -284,7 +291,7 @@ public class ServletUrlRenderer implements UrlRenderer {
 
     private void includeGetParameters(UrlProvider urlComponent) {
         String query = extractQueryString(urlComponent);
-        mergeRequestParameters(urlComponent.getValue(), urlComponent.getParameters(), urlHelper.parseQueryString(query, false));
+        mergeRequestParameters(urlComponent.getValue(), urlComponent.getParameters(), queryStringParser.parse(query, false));
     }
 
     private String extractQueryString(UrlProvider urlComponent) {
@@ -309,7 +316,7 @@ public class ServletUrlRenderer implements UrlRenderer {
      * Merge request parameters into current parameters. If a parameter is
      * already present, than the request parameter in the current request and value attribute
      * will not override its value.
-     *
+     * <p>
      * The priority is as follows:-
      * <ul>
      *  <li>parameter from the current request (least priority)</li>
@@ -317,8 +324,8 @@ public class ServletUrlRenderer implements UrlRenderer {
      *  <li>parameter from the param tag (most priority)</li>
      * </ul>
      *
-     * @param value the value attribute (URL to be generated by this component)
-     * @param parameters component parameters
+     * @param value             the value attribute (URL to be generated by this component)
+     * @param parameters        component parameters
      * @param contextParameters request parameters
      */
     protected void mergeRequestParameters(String value, Map<String, Object> parameters, Map<String, ?> contextParameters) {
@@ -332,7 +339,7 @@ public class ServletUrlRenderer implements UrlRenderer {
         if (StringUtils.contains(value, "?")) {
             String queryString = value.substring(value.indexOf('?') + 1);
 
-            mergedParams = urlHelper.parseQueryString(queryString, false);
+            mergedParams = queryStringParser.parse(queryString, false);
             for (Map.Entry<String, ?> entry : contextParameters.entrySet()) {
                 if (!mergedParams.containsKey(entry.getKey())) {
                     mergedParams.put(entry.getKey(), entry.getValue());
diff --git a/core/src/main/java/org/apache/struts2/components/UrlProvider.java b/core/src/main/java/org/apache/struts2/components/UrlProvider.java
index abb47c925..8c3b79040 100644
--- a/core/src/main/java/org/apache/struts2/components/UrlProvider.java
+++ b/core/src/main/java/org/apache/struts2/components/UrlProvider.java
@@ -25,7 +25,7 @@ import javax.servlet.http.HttpServletResponse;
 import java.util.Map;
 
 /**
- * Implemntations of this interface can be used to build a URL
+ * Implementations of this interface can be used to build a URL
  */
 public interface UrlProvider {
     /**
@@ -37,9 +37,9 @@ public interface UrlProvider {
      * get  - include only GET parameters in the URL (default)
      * all  - include both GET and POST parameters in the URL
      */
-    public static final String NONE = "none";
-    public static final String GET = "get";
-    public static final String ALL = "all";
+    String NONE = "none";
+    String GET = "get";
+    String ALL = "all";
 
     boolean isPutInContext();
 
@@ -55,7 +55,7 @@ public interface UrlProvider {
 
     String getIncludeParams();
 
-    Map getParameters();
+    Map<String, Object> getParameters();
 
     HttpServletRequest getHttpServletRequest();
 
@@ -78,19 +78,19 @@ public interface UrlProvider {
     boolean isForceAddSchemeHostAndPort();
 
     boolean isEscapeAmp();
-    
+
     String getPortletMode();
-    
+
     String getWindowState();
 
-    String determineActionURL(String action, String namespace, String method, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Map parameters, String scheme, boolean includeContext, boolean encode, boolean forceAddSchemeHostAndPort, boolean escapeAmp);
-    
+    String determineActionURL(String action, String namespace, String method, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Map<String, ?> parameters, String scheme, boolean includeContext, boolean encode, boolean forceAddSchemeHostAndPort, boolean escapeAmp);
+
     String determineNamespace(String namespace, ValueStack stack, HttpServletRequest req);
 
     String getAnchor();
-    
+
     String getPortletUrlType();
-    
+
     ValueStack getStack();
 
     void setUrlIncludeParams(String urlIncludeParams);
diff --git a/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java b/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java
index 15b79f09b..06385f28f 100644
--- a/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java
+++ b/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java
@@ -66,7 +66,8 @@ import org.apache.struts2.dispatcher.DispatcherErrorHandler;
 import org.apache.struts2.dispatcher.StaticContentLoader;
 import org.apache.struts2.dispatcher.mapper.ActionMapper;
 import org.apache.struts2.dispatcher.multipart.MultiPartRequest;
-import org.apache.struts2.url.ParametersStringBuilder;
+import org.apache.struts2.url.QueryStringBuilder;
+import org.apache.struts2.url.QueryStringParser;
 import org.apache.struts2.url.UrlDecoder;
 import org.apache.struts2.url.UrlEncoder;
 import org.apache.struts2.util.ContentTypeMatcher;
@@ -432,7 +433,8 @@ public class StrutsBeanSelectionProvider extends AbstractBeanSelectionProvider {
         alias(ExpressionCacheFactory.class, StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_FACTORY, builder, props, Scope.SINGLETON);
         alias(BeanInfoCacheFactory.class, StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_FACTORY, builder, props, Scope.SINGLETON);
 
-        alias(ParametersStringBuilder.class, StrutsConstants.STRUTS_URL_PARAMETERS_STRING_BUILDER, builder, props, Scope.SINGLETON);
+        alias(QueryStringBuilder.class, StrutsConstants.STRUTS_URL_QUERY_STRING_BUILDER, builder, props, Scope.SINGLETON);
+        alias(QueryStringParser.class, StrutsConstants.STRUTS_URL_QUERY_STRING_PARSER, builder, props, Scope.SINGLETON);
         alias(UrlEncoder.class, StrutsConstants.STRUTS_URL_ENCODER, builder, props, Scope.SINGLETON);
         alias(UrlDecoder.class, StrutsConstants.STRUTS_URL_DECODER, builder, props, Scope.SINGLETON);
 
diff --git a/core/src/main/java/org/apache/struts2/result/ServletDispatcherResult.java b/core/src/main/java/org/apache/struts2/result/ServletDispatcherResult.java
index f29a771ed..a3bbfaf97 100644
--- a/core/src/main/java/org/apache/struts2/result/ServletDispatcherResult.java
+++ b/core/src/main/java/org/apache/struts2/result/ServletDispatcherResult.java
@@ -27,7 +27,7 @@ import org.apache.logging.log4j.Logger;
 import org.apache.struts2.ServletActionContext;
 import org.apache.struts2.StrutsStatics;
 import org.apache.struts2.dispatcher.HttpParameters;
-import org.apache.struts2.views.util.UrlHelper;
+import org.apache.struts2.url.QueryStringParser;
 
 import javax.servlet.RequestDispatcher;
 import javax.servlet.http.HttpServletRequest;
@@ -66,7 +66,7 @@ import java.util.Map;
  * <!-- END SNIPPET: description -->
  *
  * <p><b>This result type takes the following parameters:</b></p>
- *
+ * <p>
  * <!-- START SNIPPET: params -->
  *
  * <ul>
@@ -76,7 +76,7 @@ import java.util.Map;
  * <li><b>parse</b> - true by default. If set to false, the location param will not be parsed for Ognl expressions.</li>
  *
  * </ul>
- *
+ * <p>
  * <!-- END SNIPPET: params -->
  *
  * <p><b>Example:</b></p>
@@ -99,7 +99,7 @@ public class ServletDispatcherResult extends StrutsResultSupport {
 
     private static final Logger LOG = LogManager.getLogger(ServletDispatcherResult.class);
 
-    private UrlHelper urlHelper;
+    private QueryStringParser queryStringParser;
 
     public ServletDispatcherResult() {
         super();
@@ -110,8 +110,8 @@ public class ServletDispatcherResult extends StrutsResultSupport {
     }
 
     @Inject
-    public void setUrlHelper(UrlHelper urlHelper) {
-        this.urlHelper = urlHelper;
+    public void setQueryStringParser(QueryStringParser queryStringParser) {
+        this.queryStringParser = queryStringParser;
     }
 
     /**
@@ -140,7 +140,7 @@ public class ServletDispatcherResult extends StrutsResultSupport {
             if (StringUtils.isNotEmpty(finalLocation) && finalLocation.indexOf('?') > 0) {
                 String queryString = finalLocation.substring(finalLocation.indexOf('?') + 1);
                 HttpParameters parameters = getParameters(invocation);
-                Map<String, Object> queryParams = urlHelper.parseQueryString(queryString, true);
+                Map<String, Object> queryParams = queryStringParser.parse(queryString, true);
                 if (queryParams != null && !queryParams.isEmpty()) {
                     parameters = HttpParameters.create(queryParams).withParent(parameters).build();
                     invocation.getInvocationContext().setParameters(parameters);
diff --git a/core/src/main/java/org/apache/struts2/result/ServletRedirectResult.java b/core/src/main/java/org/apache/struts2/result/ServletRedirectResult.java
index d59214b23..00d5b5203 100644
--- a/core/src/main/java/org/apache/struts2/result/ServletRedirectResult.java
+++ b/core/src/main/java/org/apache/struts2/result/ServletRedirectResult.java
@@ -29,7 +29,7 @@ import org.apache.logging.log4j.Logger;
 import org.apache.struts2.dispatcher.Dispatcher;
 import org.apache.struts2.dispatcher.mapper.ActionMapper;
 import org.apache.struts2.dispatcher.mapper.ActionMapping;
-import org.apache.struts2.views.util.UrlHelper;
+import org.apache.struts2.url.QueryStringBuilder;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -54,7 +54,7 @@ import static javax.servlet.http.HttpServletResponse.SC_FOUND;
  * available. This is because actions are built on a single-thread model. The
  * only way to pass data is through the session or with web parameters
  * (url?name=value) which can be OGNL expressions.
- *
+ * <p>
  * <b>This result type takes the following parameters:</b>
  *
  * <ul>
@@ -65,7 +65,7 @@ import static javax.servlet.http.HttpServletResponse.SC_FOUND;
  * "hash".  You can specify an anchor for a result.</li>
  * </ul>
  * This result follows the same rules from {@link StrutsResultSupport}.
- *
+ * <p>
  * <b>Example:</b>
  * <pre>
  * <!-- START SNIPPET: example -->
@@ -94,7 +94,7 @@ public class ServletRedirectResult extends StrutsResultSupport implements Reflec
     protected Map<String, Object> requestParameters = new LinkedHashMap<>();
     protected String anchor;
 
-    private UrlHelper urlHelper;
+    private QueryStringBuilder queryStringBuilder;
 
     public ServletRedirectResult() {
         super();
@@ -115,8 +115,8 @@ public class ServletRedirectResult extends StrutsResultSupport implements Reflec
     }
 
     @Inject
-    public void setUrlHelper(UrlHelper urlHelper) {
-        this.urlHelper = urlHelper;
+    public void setQueryStringBuilder(QueryStringBuilder queryStringBuilder) {
+        this.queryStringBuilder = queryStringBuilder;
     }
 
     public void setStatusCode(int code) {
@@ -202,7 +202,7 @@ public class ServletRedirectResult extends StrutsResultSupport implements Reflec
         }
 
         StringBuilder tmpLocation = new StringBuilder(finalLocation);
-        urlHelper.buildParametersString(requestParameters, tmpLocation, "&");
+        queryStringBuilder.build(requestParameters, tmpLocation, "&");
 
         // add the anchor
         if (anchor != null) {
@@ -283,10 +283,7 @@ public class ServletRedirectResult extends StrutsResultSupport implements Reflec
                 LOG.debug("[{}] isn't absolute URI, assuming it's a path", url);
                 return true;
             }
-        } catch (IllegalArgumentException e) {
-            LOG.debug("[{}] isn't a valid URL, assuming it's a path", url, e);
-            return true;
-        } catch (MalformedURLException e) {
+        } catch (IllegalArgumentException | MalformedURLException e) {
             LOG.debug("[{}] isn't a valid URL, assuming it's a path", url, e);
             return true;
         }
diff --git a/core/src/main/java/org/apache/struts2/url/ParametersStringBuilder.java b/core/src/main/java/org/apache/struts2/url/QueryStringBuilder.java
similarity index 63%
copy from core/src/main/java/org/apache/struts2/url/ParametersStringBuilder.java
copy to core/src/main/java/org/apache/struts2/url/QueryStringBuilder.java
index 651c46ddb..17e2775c2 100644
--- a/core/src/main/java/org/apache/struts2/url/ParametersStringBuilder.java
+++ b/core/src/main/java/org/apache/struts2/url/QueryStringBuilder.java
@@ -18,14 +18,21 @@
  */
 package org.apache.struts2.url;
 
+import java.io.Serializable;
 import java.util.Map;
 
 /**
- * A builder used to create a proper query string out of a set of parameters
+ * A builder used to create a proper Query String out of a set of parameters
  * @since Struts 6.1.0
  */
-public interface ParametersStringBuilder {
+public interface QueryStringBuilder extends Serializable {
 
-    void buildParametersString(Map<String, Object> params, StringBuilder link, String paramSeparator);
+    /**
+     * Builds a Query String with defined separator and appends it to the provided link
+     * @param params a Map used to build a Query String
+     * @param link to which the Query String should be added
+     * @param paramSeparator used to separate parameters in query string
+     */
+    void build(Map<String, Object> params, StringBuilder link, String paramSeparator);
 
 }
diff --git a/core/src/main/java/org/apache/struts2/url/ParametersStringBuilder.java b/core/src/main/java/org/apache/struts2/url/QueryStringParser.java
similarity index 80%
rename from core/src/main/java/org/apache/struts2/url/ParametersStringBuilder.java
rename to core/src/main/java/org/apache/struts2/url/QueryStringParser.java
index 651c46ddb..0f48cd293 100644
--- a/core/src/main/java/org/apache/struts2/url/ParametersStringBuilder.java
+++ b/core/src/main/java/org/apache/struts2/url/QueryStringParser.java
@@ -18,14 +18,15 @@
  */
 package org.apache.struts2.url;
 
+import java.io.Serializable;
 import java.util.Map;
 
 /**
- * A builder used to create a proper query string out of a set of parameters
+ * Used to parse Http Query String into a Map of parameters
  * @since Struts 6.1.0
  */
-public interface ParametersStringBuilder {
+public interface QueryStringParser extends Serializable {
 
-    void buildParametersString(Map<String, Object> params, StringBuilder link, String paramSeparator);
+    Map<String, Object> parse(String queryString, boolean forceValueArray);
 
 }
diff --git a/core/src/main/java/org/apache/struts2/url/StrutsParametersStringBuilder.java b/core/src/main/java/org/apache/struts2/url/StrutsQueryStringBuilder.java
similarity index 91%
rename from core/src/main/java/org/apache/struts2/url/StrutsParametersStringBuilder.java
rename to core/src/main/java/org/apache/struts2/url/StrutsQueryStringBuilder.java
index e6e38f451..ea5dca84d 100644
--- a/core/src/main/java/org/apache/struts2/url/StrutsParametersStringBuilder.java
+++ b/core/src/main/java/org/apache/struts2/url/StrutsQueryStringBuilder.java
@@ -24,19 +24,19 @@ import org.apache.logging.log4j.Logger;
 
 import java.util.Map;
 
-public class StrutsParametersStringBuilder implements ParametersStringBuilder {
+public class StrutsQueryStringBuilder implements QueryStringBuilder {
 
-    private static final Logger LOG = LogManager.getLogger(StrutsParametersStringBuilder.class);
+    private static final Logger LOG = LogManager.getLogger(StrutsQueryStringBuilder.class);
 
     private final UrlEncoder encoder;
 
     @Inject
-    public StrutsParametersStringBuilder(UrlEncoder encoder) {
+    public StrutsQueryStringBuilder(UrlEncoder encoder) {
         this.encoder = encoder;
     }
 
     @Override
-    public void buildParametersString(Map<String, Object> params, StringBuilder link, String paramSeparator) {
+    public void build(Map<String, Object> params, StringBuilder link, String paramSeparator) {
         if ((params != null) && (params.size() > 0)) {
             LOG.debug("Building query string out of: {} parameters", params.size());
             StringBuilder queryString = new StringBuilder();
diff --git a/core/src/main/java/org/apache/struts2/url/StrutsQueryStringParser.java b/core/src/main/java/org/apache/struts2/url/StrutsQueryStringParser.java
new file mode 100644
index 000000000..f1764511e
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/url/StrutsQueryStringParser.java
@@ -0,0 +1,98 @@
+/*
+ * 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.url;
+
+import com.opensymphony.xwork2.inject.Inject;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class StrutsQueryStringParser implements QueryStringParser {
+
+    private static final Logger LOG = LogManager.getLogger(StrutsQueryStringParser.class);
+
+    private final UrlDecoder decoder;
+
+    @Inject
+    public StrutsQueryStringParser(UrlDecoder decoder) {
+        this.decoder = decoder;
+    }
+
+    @Override
+    public Map<String, Object> parse(String queryString, boolean forceValueArray) {
+        if (StringUtils.isEmpty(queryString)) {
+            LOG.debug("Query String is empty, returning an empty map");
+            return Collections.emptyMap();
+        }
+
+        Map<String, Object> queryParams = new LinkedHashMap<>();
+        String[] params = queryString.split("&");
+        for (String param : params) {
+            if (StringUtils.isBlank(param)) {
+                LOG.debug("Param [{}] is blank, skipping", param);
+                continue;
+            }
+
+            String[] tmpParams = param.split("=");
+            String paramName = null;
+            String paramValue = "";
+            if (tmpParams.length > 0) {
+                paramName = tmpParams[0];
+            }
+            if (tmpParams.length > 1) {
+                paramValue = tmpParams[1];
+            }
+            if (paramName != null) {
+                extractParam(paramName, paramValue, queryParams, forceValueArray);
+            }
+        }
+        return queryParams;
+    }
+
+    private void extractParam(String paramName, String paramValue, Map<String, Object> queryParams, boolean forceValueArray) {
+        String decodedParamName = decoder.decode(paramName, true);
+        String decodedParamValue = decoder.decode(paramValue, true);
+
+        if (queryParams.containsKey(decodedParamName) || forceValueArray) {
+            // WW-1619 append new param value to existing value(s)
+            Object currentParam = queryParams.get(decodedParamName);
+            if (currentParam instanceof String) {
+                queryParams.put(decodedParamName, new String[]{(String) currentParam, decodedParamValue});
+            } else {
+                String[] currentParamValues = (String[]) currentParam;
+                if (currentParamValues != null) {
+                    List<String> paramList = new ArrayList<>(Arrays.asList(currentParamValues));
+                    paramList.add(decodedParamValue);
+                    queryParams.put(decodedParamName, paramList.toArray(new String[0]));
+                } else {
+                    queryParams.put(decodedParamName, new String[]{decodedParamValue});
+                }
+            }
+        } else {
+            queryParams.put(decodedParamName, decodedParamValue);
+        }
+    }
+}
diff --git a/core/src/main/java/org/apache/struts2/url/UrlDecoder.java b/core/src/main/java/org/apache/struts2/url/UrlDecoder.java
index b54c563f5..d2fed954b 100644
--- a/core/src/main/java/org/apache/struts2/url/UrlDecoder.java
+++ b/core/src/main/java/org/apache/struts2/url/UrlDecoder.java
@@ -18,7 +18,13 @@
  */
 package org.apache.struts2.url;
 
-public interface UrlDecoder {
+import java.io.Serializable;
+
+/**
+ * URL Decoder used internally by Struts
+ * @since Struts 6.1.0
+ */
+public interface UrlDecoder extends Serializable {
 
     /**
      * Decodes the input using default encoding, e.g.: struts.i18n.encoding
diff --git a/core/src/main/java/org/apache/struts2/url/UrlEncoder.java b/core/src/main/java/org/apache/struts2/url/UrlEncoder.java
index 976645a53..ae8bf3968 100644
--- a/core/src/main/java/org/apache/struts2/url/UrlEncoder.java
+++ b/core/src/main/java/org/apache/struts2/url/UrlEncoder.java
@@ -18,7 +18,13 @@
  */
 package org.apache.struts2.url;
 
-public interface UrlEncoder {
+import java.io.Serializable;
+
+/**
+ * URL Encoder used internally by Struts
+ * @since Struts 6.1.0
+ */
+public interface UrlEncoder extends Serializable {
 
     /**
      * Encodes the input tb be used with URL using the provided encoding
diff --git a/core/src/main/java/org/apache/struts2/views/util/DefaultUrlHelper.java b/core/src/main/java/org/apache/struts2/views/util/DefaultUrlHelper.java
index 547efef11..bb072c82b 100644
--- a/core/src/main/java/org/apache/struts2/views/util/DefaultUrlHelper.java
+++ b/core/src/main/java/org/apache/struts2/views/util/DefaultUrlHelper.java
@@ -24,16 +24,13 @@ import org.apache.commons.text.StringEscapeUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.struts2.StrutsConstants;
-import org.apache.struts2.url.ParametersStringBuilder;
+import org.apache.struts2.url.QueryStringBuilder;
+import org.apache.struts2.url.QueryStringParser;
 import org.apache.struts2.url.UrlDecoder;
 import org.apache.struts2.url.UrlEncoder;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.LinkedHashMap;
-import java.util.List;
 import java.util.Map;
 
 /**
@@ -49,7 +46,8 @@ public class DefaultUrlHelper implements UrlHelper {
     private int httpPort = DEFAULT_HTTP_PORT;
     private int httpsPort = DEFAULT_HTTPS_PORT;
 
-    private ParametersStringBuilder parametersStringBuilder;
+    private QueryStringBuilder queryStringBuilder;
+    private QueryStringParser queryStringParser;
     private UrlEncoder encoder;
     private UrlDecoder decoder;
 
@@ -74,8 +72,13 @@ public class DefaultUrlHelper implements UrlHelper {
     }
 
     @Inject
-    public void setParametersStringBuilder(ParametersStringBuilder builder) {
-        this.parametersStringBuilder = builder;
+    public void setQueryStringBuilder(QueryStringBuilder builder) {
+        this.queryStringBuilder = builder;
+    }
+
+    @Inject
+    public void setQueryStringParser(QueryStringParser queryStringParser) {
+        this.queryStringParser = queryStringParser;
     }
 
     public String buildUrl(String action, HttpServletRequest request, HttpServletResponse response, Map<String, Object> params) {
@@ -176,9 +179,9 @@ public class DefaultUrlHelper implements UrlHelper {
 
         //if the action was not explicitly set grab the params from the request
         if (escapeAmp) {
-            parametersStringBuilder.buildParametersString(params, link, AMP);
+            queryStringBuilder.build(params, link, AMP);
         } else {
-            parametersStringBuilder.buildParametersString(params, link, "&");
+            queryStringBuilder.build(params, link, "&");
         }
 
         String result = link.toString();
@@ -208,11 +211,11 @@ public class DefaultUrlHelper implements UrlHelper {
      * @param params a set of params to assign
      * @param link a based url
      * @param paramSeparator separator used
-     * @deprecated since Struts 6.1.0, use {@link ParametersStringBuilder} instead
+     * @deprecated since Struts 6.1.0, use {@link QueryStringBuilder} instead
      */
     @Deprecated
     public void buildParametersString(Map<String, Object> params, StringBuilder link, String paramSeparator) {
-        parametersStringBuilder.buildParametersString(params, link, paramSeparator);
+        queryStringBuilder.build(params, link, paramSeparator);
     }
 
     /**
@@ -269,47 +272,11 @@ public class DefaultUrlHelper implements UrlHelper {
         return decoder.decode(input, isQueryString);
     }
 
+    /**
+     * @deprecated since 6.1.0, use {@link QueryStringParser} directly, use {@link Inject} to inject a proper instance
+     */
+    @Deprecated
     public Map<String, Object> parseQueryString(String queryString, boolean forceValueArray) {
-        Map<String, Object> queryParams = new LinkedHashMap<>();
-        if (queryString != null) {
-            String[] params = queryString.split("&");
-            for (String param : params) {
-                if (param.trim().length() > 0) {
-                    String[] tmpParams = param.split("=");
-                    String paramName = null;
-                    String paramValue = "";
-                    if (tmpParams.length > 0) {
-                        paramName = tmpParams[0];
-                    }
-                    if (tmpParams.length > 1) {
-                        paramValue = tmpParams[1];
-                    }
-                    if (paramName != null) {
-                        paramName = decoder.decode(paramName, true);
-                        String translatedParamValue = decoder.decode(paramValue, true);
-
-                        if (queryParams.containsKey(paramName) || forceValueArray) {
-                            // WW-1619 append new param value to existing value(s)
-                            Object currentParam = queryParams.get(paramName);
-                            if (currentParam instanceof String) {
-                                queryParams.put(paramName, new String[]{(String) currentParam, translatedParamValue});
-                            } else {
-                                String[] currentParamValues = (String[]) currentParam;
-                                if (currentParamValues != null) {
-                                    List<String> paramList = new ArrayList<>(Arrays.asList(currentParamValues));
-                                    paramList.add(translatedParamValue);
-                                    queryParams.put(paramName, paramList.toArray(new String[0]));
-                                } else {
-                                    queryParams.put(paramName, new String[]{translatedParamValue});
-                                }
-                            }
-                        } else {
-                            queryParams.put(paramName, translatedParamValue);
-                        }
-                    }
-                }
-            }
-        }
-        return queryParams;
+        return this.queryStringParser.parse(queryString, forceValueArray);
     }
 }
diff --git a/core/src/main/java/org/apache/struts2/views/util/UrlHelper.java b/core/src/main/java/org/apache/struts2/views/util/UrlHelper.java
index b997a58ef..4f82d6848 100644
--- a/core/src/main/java/org/apache/struts2/views/util/UrlHelper.java
+++ b/core/src/main/java/org/apache/struts2/views/util/UrlHelper.java
@@ -18,6 +18,10 @@
  */
 package org.apache.struts2.views.util;
 
+import com.opensymphony.xwork2.inject.Inject;
+import org.apache.struts2.url.QueryStringBuilder;
+import org.apache.struts2.url.QueryStringParser;
+
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.util.Map;
@@ -30,14 +34,14 @@ public interface UrlHelper {
     /**
      * Default HTTP port (80).
      */
-    static final int DEFAULT_HTTP_PORT = 80;
+    int DEFAULT_HTTP_PORT = 80;
 
     /**
      * Default HTTPS port (443).
      */
-    static final int DEFAULT_HTTPS_PORT = 443;
+    int DEFAULT_HTTPS_PORT = 443;
 
-    static final String AMP = "&amp;";
+    String AMP = "&amp;";
 
     String buildUrl(String action, HttpServletRequest request, HttpServletResponse response, Map<String, Object> params);
 
@@ -50,8 +54,16 @@ public interface UrlHelper {
     String buildUrl(String action, HttpServletRequest request, HttpServletResponse response, Map<String, Object> params, String scheme,
                     boolean includeContext, boolean encodeResult, boolean forceAddSchemeHostAndPort, boolean escapeAmp);
 
+    /**
+     * @deprecated since Struts 6.1.0, use {@link QueryStringBuilder} instead
+     */
+    @Deprecated
     void buildParametersString(Map<String, Object> params, StringBuilder link, String paramSeparator);
 
+    /**
+     * @deprecated since 6.1.0, use {@link QueryStringParser} directly, use {@link Inject} to inject a proper instance
+     */
+    @Deprecated
     Map<String, Object> parseQueryString(String queryString, boolean forceValueArray);
 
 }
diff --git a/core/src/main/resources/org/apache/struts2/default.properties b/core/src/main/resources/org/apache/struts2/default.properties
index a57c48bea..5847db3be 100644
--- a/core/src/main/resources/org/apache/struts2/default.properties
+++ b/core/src/main/resources/org/apache/struts2/default.properties
@@ -279,10 +279,15 @@ struts.ognl.expressionMaxLength=256
 ### These formatters are using a slightly different patterns, please check JavaDocs of both and more details is in WW-5016
 struts.date.formatter=dateTimeFormatter
 
-### Defines which instance of ParametersStringBuilder to use, Struts provides just one instance:
-### - strutsParametersStringBuilder
-### The builder is used by UrlHelp to create a proper query string out of provided parameters map
-struts.url.parametersStringBuilder=strutsParametersStringBuilder
+### Defines which instance of QueryStringBuilder to use, Struts provides just one instance:
+### - strutsQueryStringBuilder
+### The builder is used by UrlHelp to create a proper Query String out of provided parameters map
+struts.url.queryStringBuilder=strutsQueryStringBuilder
+
+### Defines which instance of QueryStringParser to use, Struts provides just one instance:
+### - strutsQueryStringParser
+### The parser is used to parse Query String into a map
+struts.url.queryStringParser=strutsQueryStringParser
 
 ### Defines which instances of encoder and decoder to use, Struts provides one default implementation for each
 struts.url.encoder=strutsUrlEncoder
diff --git a/core/src/main/resources/struts-default.xml b/core/src/main/resources/struts-default.xml
index 35b5bf419..fc6e1c54c 100644
--- a/core/src/main/resources/struts-default.xml
+++ b/core/src/main/resources/struts-default.xml
@@ -313,8 +313,10 @@
     <bean type="com.opensymphony.xwork2.ognl.BeanInfoCacheFactory" name="struts"
           class="com.opensymphony.xwork2.ognl.DefaultOgnlBeanInfoCacheFactory" scope="singleton"/>
 
-    <bean type="org.apache.struts2.url.ParametersStringBuilder" name="strutsParametersStringBuilder"
-          class="org.apache.struts2.url.StrutsParametersStringBuilder" scope="singleton"/>
+    <bean type="org.apache.struts2.url.QueryStringBuilder" name="strutsQueryStringBuilder"
+          class="org.apache.struts2.url.StrutsQueryStringBuilder" scope="singleton"/>
+    <bean type="org.apache.struts2.url.QueryStringParser" name="strutsQueryStringParser"
+          class="org.apache.struts2.url.StrutsQueryStringParser" scope="singleton"/>
     <bean type="org.apache.struts2.url.UrlEncoder" name="strutsUrlEncoder"
           class="org.apache.struts2.url.StrutsUrlEncoder" scope="singleton"/>
     <bean type="org.apache.struts2.url.UrlDecoder" name="strutsUrlDecoder"
diff --git a/core/src/test/java/org/apache/struts2/result/ServletActionRedirectResultTest.java b/core/src/test/java/org/apache/struts2/result/ServletActionRedirectResultTest.java
index 0933f8ea7..8fdf1ae30 100644
--- a/core/src/test/java/org/apache/struts2/result/ServletActionRedirectResultTest.java
+++ b/core/src/test/java/org/apache/struts2/result/ServletActionRedirectResultTest.java
@@ -29,7 +29,8 @@ import org.apache.struts2.ServletActionContext;
 import org.apache.struts2.StrutsInternalTestCase;
 import org.apache.struts2.dispatcher.mapper.ActionMapper;
 import org.apache.struts2.dispatcher.mapper.DefaultActionMapper;
-import org.apache.struts2.views.util.DefaultUrlHelper;
+import org.apache.struts2.url.StrutsQueryStringBuilder;
+import org.apache.struts2.url.StrutsUrlEncoder;
 import org.easymock.IMocksControl;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
@@ -82,7 +83,7 @@ public class ServletActionRedirectResultTest extends StrutsInternalTestCase {
         result.setEncode(false);
         result.setPrependServletContext(false);
         result.setAnchor("fragment");
-        result.setUrlHelper(new DefaultUrlHelper());
+        result.setQueryStringBuilder(new StrutsQueryStringBuilder(new StrutsUrlEncoder()));
 
         IMocksControl control = createControl();
         ActionProxy mockActionProxy = control.createMock(ActionProxy.class);
@@ -144,7 +145,7 @@ public class ServletActionRedirectResultTest extends StrutsInternalTestCase {
         result.setEncode(false);
         result.setPrependServletContext(false);
         result.setAnchor("fragment");
-        result.setUrlHelper(new DefaultUrlHelper());
+        result.setQueryStringBuilder(new StrutsQueryStringBuilder(new StrutsUrlEncoder()));
 
         IMocksControl control = createControl();
         ActionProxy mockActionProxy = control.createMock(ActionProxy.class);
@@ -210,7 +211,7 @@ public class ServletActionRedirectResultTest extends StrutsInternalTestCase {
         result.setEncode(false);
         result.setPrependServletContext(false);
         result.setAnchor("fragment");
-        result.setUrlHelper(new DefaultUrlHelper());
+        result.setQueryStringBuilder(new StrutsQueryStringBuilder(new StrutsUrlEncoder()));
 
         IMocksControl control = createControl();
         ActionProxy mockActionProxy = control.createMock(ActionProxy.class);
@@ -264,7 +265,7 @@ public class ServletActionRedirectResultTest extends StrutsInternalTestCase {
         result.setEncode(false);
         result.setPrependServletContext(false);
         result.setAnchor("fragment");
-        result.setUrlHelper(new DefaultUrlHelper());
+        result.setQueryStringBuilder(new StrutsQueryStringBuilder(new StrutsUrlEncoder()));
 
         IMocksControl control = createControl();
         ActionProxy mockActionProxy = control.createMock(ActionProxy.class);
diff --git a/core/src/test/java/org/apache/struts2/result/ServletDispatcherResultTest.java b/core/src/test/java/org/apache/struts2/result/ServletDispatcherResultTest.java
index 4a90ae443..0f57593d6 100644
--- a/core/src/test/java/org/apache/struts2/result/ServletDispatcherResultTest.java
+++ b/core/src/test/java/org/apache/struts2/result/ServletDispatcherResultTest.java
@@ -18,29 +18,20 @@
  */
 package org.apache.struts2.result;
 
-import javax.servlet.RequestDispatcher;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
+import com.mockobjects.dynamic.C;
+import com.mockobjects.dynamic.Mock;
+import com.opensymphony.xwork2.ActionContext;
 import com.opensymphony.xwork2.mock.MockActionInvocation;
-import com.opensymphony.xwork2.util.ValueStack;
 import com.opensymphony.xwork2.util.ValueStackFactory;
-import ognl.Ognl;
-
 import org.apache.struts2.ServletActionContext;
 import org.apache.struts2.StrutsInternalTestCase;
 import org.apache.struts2.StrutsStatics;
 import org.apache.struts2.dispatcher.HttpParameters;
 
-import com.mockobjects.dynamic.C;
-import com.mockobjects.dynamic.Mock;
-import com.opensymphony.xwork2.ActionContext;
-import org.apache.struts2.result.ServletDispatcherResult;
-
+import javax.servlet.RequestDispatcher;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 
-/**
- *
- */
 public class ServletDispatcherResultTest extends StrutsInternalTestCase implements StrutsStatics {
 
     public void testInclude() {
diff --git a/core/src/test/java/org/apache/struts2/result/ServletRedirectResultTest.java b/core/src/test/java/org/apache/struts2/result/ServletRedirectResultTest.java
index 8936783fc..2ee6478f5 100644
--- a/core/src/test/java/org/apache/struts2/result/ServletRedirectResultTest.java
+++ b/core/src/test/java/org/apache/struts2/result/ServletRedirectResultTest.java
@@ -32,8 +32,8 @@ import com.opensymphony.xwork2.util.ValueStack;
 import org.apache.struts2.ServletActionContext;
 import org.apache.struts2.StrutsInternalTestCase;
 import org.apache.struts2.StrutsStatics;
-import org.apache.struts2.dispatcher.mapper.ActionMapper;
-import org.apache.struts2.views.util.DefaultUrlHelper;
+import org.apache.struts2.url.StrutsQueryStringBuilder;
+import org.apache.struts2.url.StrutsUrlEncoder;
 import org.easymock.IMocksControl;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
@@ -240,7 +240,7 @@ public class ServletRedirectResultTest extends StrutsInternalTestCase implements
         result.setEncode(false);
         result.setPrependServletContext(false);
         result.setAnchor("fragment");
-        result.setUrlHelper(new DefaultUrlHelper());
+        result.setQueryStringBuilder(new StrutsQueryStringBuilder(new StrutsUrlEncoder()));
 
         IMocksControl control = createControl();
         ActionProxy mockActionProxy = control.createMock(ActionProxy.class);
@@ -286,7 +286,7 @@ public class ServletRedirectResultTest extends StrutsInternalTestCase implements
         result.setParse(true);
         result.setEncode(false);
         result.setPrependServletContext(false);
-        result.setUrlHelper(new DefaultUrlHelper());
+        result.setQueryStringBuilder(new StrutsQueryStringBuilder(new StrutsUrlEncoder()));
         result.setSuppressEmptyParameters(true);
 
         IMocksControl control = createControl();
@@ -433,7 +433,7 @@ public class ServletRedirectResultTest extends StrutsInternalTestCase implements
         }
     }
 
-    public void testPassingNullInvocation() throws Exception{
+    public void testPassingNullInvocation() throws Exception {
         Result result = new ServletRedirectResult();
         try {
             result.execute(null);
diff --git a/core/src/test/java/org/apache/struts2/url/StrutsParametersStringBuilderTest.java b/core/src/test/java/org/apache/struts2/url/StrutsQueryStringBuilderTest.java
similarity index 88%
rename from core/src/test/java/org/apache/struts2/url/StrutsParametersStringBuilderTest.java
rename to core/src/test/java/org/apache/struts2/url/StrutsQueryStringBuilderTest.java
index 815c5ae16..b30a3b835 100644
--- a/core/src/test/java/org/apache/struts2/url/StrutsParametersStringBuilderTest.java
+++ b/core/src/test/java/org/apache/struts2/url/StrutsQueryStringBuilderTest.java
@@ -28,9 +28,9 @@ import java.util.Map;
 
 import static org.junit.Assert.assertEquals;
 
-public class StrutsParametersStringBuilderTest {
+public class StrutsQueryStringBuilderTest {
 
-    private ParametersStringBuilder builder;
+    private QueryStringBuilder builder;
 
     @Test
     public void testBuildParametersStringWithUrlHavingSomeExistingParameters() {
@@ -43,7 +43,7 @@ public class StrutsParametersStringBuilderTest {
 
         StringBuilder url = new StringBuilder("http://localhost:8080/myContext/myPage.jsp?initParam=initValue");
 
-        builder.buildParametersString(parameters, url, UrlHelper.AMP);
+        builder.build(parameters, url, UrlHelper.AMP);
 
         assertEquals(expectedUrl, url.toString());
     }
@@ -59,7 +59,7 @@ public class StrutsParametersStringBuilderTest {
 
         StringBuilder url = new StringBuilder("http://localhost:8080/myContext/myPage.jsp?initParam=initValue");
 
-        builder.buildParametersString(parameters, url, UrlHelper.AMP);
+        builder.build(parameters, url, UrlHelper.AMP);
 
         assertEquals(expectedUrl, url.toString());
     }
@@ -71,7 +71,7 @@ public class StrutsParametersStringBuilderTest {
         parameters.put("param1", new String[]{});
         parameters.put("param2", new ArrayList<>());
         StringBuilder url = new StringBuilder("https://www.nowhere.com/myworld.html");
-        builder.buildParametersString(parameters, url, UrlHelper.AMP);
+        builder.build(parameters, url, UrlHelper.AMP);
         assertEquals(expectedUrl, url.toString());
     }
 
@@ -87,13 +87,13 @@ public class StrutsParametersStringBuilderTest {
             }
         });
         StringBuilder url = new StringBuilder("https://www.nowhere.com/myworld.html");
-        builder.buildParametersString(parameters, url, "&");
+        builder.build(parameters, url, "&");
         assertEquals(expectedUrl, url.toString());
     }
 
     @Before
     public void setUp() throws Exception {
-        builder = new StrutsParametersStringBuilder(new StrutsUrlEncoder());
+        builder = new StrutsQueryStringBuilder(new StrutsUrlEncoder());
     }
 
 }
diff --git a/core/src/test/java/org/apache/struts2/url/StrutsQueryStringParserTest.java b/core/src/test/java/org/apache/struts2/url/StrutsQueryStringParserTest.java
new file mode 100644
index 000000000..001749edf
--- /dev/null
+++ b/core/src/test/java/org/apache/struts2/url/StrutsQueryStringParserTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.url;
+
+import org.assertj.core.util.Arrays;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class StrutsQueryStringParserTest {
+
+    private QueryStringParser parser;
+
+    @Test
+    public void testParseQuery() {
+        Map<String, Object> result = parser.parse("aaa=aaaval&bbb=bbbval&ccc=&%3Ca%22%3E=%3Cval%3E", false);
+
+        assertEquals("aaaval", result.get("aaa"));
+        assertEquals("bbbval", result.get("bbb"));
+        assertEquals("", result.get("ccc"));
+        assertEquals("<val>", result.get("<a\">"));
+    }
+
+    @Test
+    public void testParseQueryIntoArray() {
+        Map<String, Object> result = parser.parse("a=1&a=2&a=3", true);
+
+        Object actual = result.get("a");
+        assertThat(actual).isInstanceOf(String[].class);
+        assertThat(Arrays.asList(actual)).containsOnly("1", "2", "3");
+    }
+
+    @Test
+    public void testParseEmptyQuery() {
+        Map<String, Object> result = parser.parse("", false);
+
+        assertNotNull(result);
+        assertEquals(0, result.size());
+    }
+
+    @Test
+    public void testParseNullQuery() {
+        Map<String, Object> result = parser.parse(null, false);
+
+        assertNotNull(result);
+        assertEquals(0, result.size());
+    }
+
+    @Test
+    public void testDecodeSpacesInQueryString() {
+        Map<String, Object> queryParameters = parser.parse("name=value+with+space", false);
+
+        assertTrue(queryParameters.containsKey("name"));
+        assertEquals("value with space", queryParameters.get("name"));
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        this.parser = new StrutsQueryStringParser(new StrutsUrlDecoder());
+    }
+
+}
diff --git a/core/src/test/java/org/apache/struts2/views/util/DefaultUrlHelperTest.java b/core/src/test/java/org/apache/struts2/views/util/DefaultUrlHelperTest.java
index acbee3010..fa1c22322 100644
--- a/core/src/test/java/org/apache/struts2/views/util/DefaultUrlHelperTest.java
+++ b/core/src/test/java/org/apache/struts2/views/util/DefaultUrlHelperTest.java
@@ -23,7 +23,7 @@ import com.opensymphony.xwork2.ActionContext;
 import com.opensymphony.xwork2.inject.Container;
 import com.opensymphony.xwork2.inject.Scope.Strategy;
 import org.apache.struts2.StrutsInternalTestCase;
-import org.apache.struts2.url.StrutsParametersStringBuilder;
+import org.apache.struts2.url.StrutsQueryStringBuilder;
 import org.apache.struts2.url.StrutsUrlDecoder;
 import org.apache.struts2.url.StrutsUrlEncoder;
 
@@ -339,45 +339,13 @@ public class DefaultUrlHelperTest extends StrutsInternalTestCase {
         assertEquals(expectedString, urlString);
     }
 
-
-    public void testParseQuery() {
-        Map<String, Object> result = urlHelper.parseQueryString("aaa=aaaval&bbb=bbbval&ccc=&%3Ca%22%3E=%3Cval%3E", false);
-
-        assertEquals(result.get("aaa"), "aaaval");
-        assertEquals(result.get("bbb"), "bbbval");
-        assertEquals(result.get("ccc"), "");
-        assertEquals(result.get("<a\">"), "<val>");
-    }
-
-    public void testParseEmptyQuery() {
-        Map<String, Object> result = urlHelper.parseQueryString("", false);
-
-        assertNotNull(result);
-        assertEquals(result.size(), 0);
-    }
-
-    public void testParseNullQuery() {
-        Map<String, Object> result = urlHelper.parseQueryString(null, false);
-
-        assertNotNull(result);
-        assertEquals(result.size(), 0);
-    }
-
-    public void testDecodeSpacesInQueryString() {
-        Map<String, Object> queryParameters = urlHelper.parseQueryString("name=value+with+space", false);
-
-        assertTrue(queryParameters.containsKey("name"));
-        assertEquals("value with space", queryParameters.get("name"));
-    }
-
-
     public void setUp() throws Exception {
         super.setUp();
         StubContainer stubContainer = new StubContainer(container);
         ActionContext.getContext().withContainer(stubContainer);
         urlHelper = new DefaultUrlHelper();
         StrutsUrlEncoder encoder = new StrutsUrlEncoder();
-        urlHelper.setParametersStringBuilder(new StrutsParametersStringBuilder(encoder));
+        urlHelper.setQueryStringBuilder(new StrutsQueryStringBuilder(encoder));
         urlHelper.setEncoder(encoder);
         urlHelper.setDecoder(new StrutsUrlDecoder());
     }
diff --git a/plugins/embeddedjsp/src/main/java/org/apache/struts2/JSPRuntime.java b/plugins/embeddedjsp/src/main/java/org/apache/struts2/JSPRuntime.java
index 81bbed490..d9425bbb6 100644
--- a/plugins/embeddedjsp/src/main/java/org/apache/struts2/JSPRuntime.java
+++ b/plugins/embeddedjsp/src/main/java/org/apache/struts2/JSPRuntime.java
@@ -20,8 +20,7 @@ package org.apache.struts2;
 
 import com.opensymphony.xwork2.ActionContext;
 import org.apache.struts2.dispatcher.Parameter;
-import org.apache.struts2.views.util.DefaultUrlHelper;
-import org.apache.struts2.views.util.UrlHelper;
+import org.apache.struts2.url.QueryStringParser;
 
 import javax.servlet.Servlet;
 import javax.servlet.http.HttpServletRequest;
@@ -53,11 +52,13 @@ public abstract class JSPRuntime {
         int i = location.indexOf("?");
         if (i > 0) {
             //extract params from the url and add them to the request
-            final UrlHelper urlHelperGetInstance = ServletActionContext.getContext().getInstance(UrlHelper.class);
-            final UrlHelper contextUrlHelper = (urlHelperGetInstance != null ? urlHelperGetInstance : (UrlHelper) ActionContext.getContext().get(StrutsConstants.STRUTS_URL_HELPER));
-            final UrlHelper urlHelper = (contextUrlHelper != null ? contextUrlHelper : new DefaultUrlHelper());
+            ActionContext actionContext = ServletActionContext.getActionContext();
+            if (actionContext == null) {
+                throw new StrutsException("Running out of action context!");
+            }
+            final QueryStringParser parser = actionContext.getInstance(QueryStringParser.class);
             String query = location.substring(i + 1);
-            Map<String, Object> queryParams = urlHelper.parseQueryString(query, true);
+            Map<String, Object> queryParams = parser.parse(query, true);
             if (queryParams != null && !queryParams.isEmpty()) {
                 Map<String, Parameter> newParams = new HashMap<>();
                 for (Map.Entry<String, Object> entry : queryParams.entrySet()) {
diff --git a/plugins/embeddedjsp/src/test/java/org/apache/struts2/EmbeddedJSPResultTest.java b/plugins/embeddedjsp/src/test/java/org/apache/struts2/EmbeddedJSPResultTest.java
index 6cde3f7d6..4a8eee285 100644
--- a/plugins/embeddedjsp/src/test/java/org/apache/struts2/EmbeddedJSPResultTest.java
+++ b/plugins/embeddedjsp/src/test/java/org/apache/struts2/EmbeddedJSPResultTest.java
@@ -35,10 +35,9 @@ import junit.framework.TestCase;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.struts2.dispatcher.HttpParameters;
 import org.apache.struts2.jasper.runtime.InstanceHelper;
+import org.apache.struts2.url.QueryStringParser;
+import org.apache.struts2.url.StrutsQueryStringParser;
 import org.apache.struts2.url.StrutsUrlDecoder;
-import org.apache.struts2.url.StrutsUrlEncoder;
-import org.apache.struts2.views.util.DefaultUrlHelper;
-import org.apache.struts2.views.util.UrlHelper;
 import org.apache.tomcat.InstanceManager;
 import org.easymock.EasyMock;
 import org.springframework.mock.web.MockHttpServletRequest;
@@ -58,6 +57,8 @@ import java.util.Map;
 import java.util.concurrent.BrokenBarrierException;
 import java.util.concurrent.CyclicBarrier;
 
+import static org.apache.struts2.ServletActionContext.STRUTS_VALUESTACK_KEY;
+
 
 public class EmbeddedJSPResultTest extends TestCase {
     private HttpServletRequest request;
@@ -324,18 +325,19 @@ public class EmbeddedJSPResultTest extends TestCase {
         HttpSession session = EasyMock.createNiceMock(HttpSession.class);
         EasyMock.replay(session);
 
+        //mock value stack
+        ValueStack valueStack = EasyMock.createNiceMock(ValueStack.class);
+        EasyMock.expect(valueStack.getActionContext()).andReturn(ActionContext.getContext()).anyTimes();
+        EasyMock.replay(valueStack);
+
         EasyMock.expect(request.getSession()).andReturn(session).anyTimes();
         EasyMock.expect(request.getParameterMap()).andReturn(params).anyTimes();
         EasyMock.expect(request.getParameter("username")).andAnswer(() -> ActionContext.getContext().getParameters().get("username").getValue());
+        EasyMock.expect(request.getAttribute(STRUTS_VALUESTACK_KEY)).andReturn(valueStack).anyTimes();
         EasyMock.expect(request.getAttribute("something")).andReturn("somethingelse").anyTimes();
 
         EasyMock.replay(request);
 
-        //mock value stack
-        ValueStack valueStack = EasyMock.createNiceMock(ValueStack.class);
-        EasyMock.expect(valueStack.getActionContext()).andReturn(ActionContext.getContext()).anyTimes();
-        EasyMock.replay(valueStack);
-
         //mock converter
         XWorkConverter converter = new DummyConverter();
 
@@ -350,10 +352,8 @@ public class EmbeddedJSPResultTest extends TestCase {
         EasyMock.expect(container.getInstanceNames(FileManager.class)).andReturn(new HashSet<>()).anyTimes();
         EasyMock.expect(container.getInstance(FileManager.class)).andReturn(fileManager).anyTimes();
 
-        DefaultUrlHelper urlHelper = new DefaultUrlHelper();
-        urlHelper.setDecoder(new StrutsUrlDecoder());
-        urlHelper.setEncoder(new StrutsUrlEncoder());
-        EasyMock.expect(container.getInstance(UrlHelper.class)).andReturn(urlHelper).anyTimes();
+        QueryStringParser queryStringParser = new StrutsQueryStringParser(new StrutsUrlDecoder());
+        EasyMock.expect(container.getInstance(QueryStringParser.class)).andReturn(queryStringParser).anyTimes();
         FileManagerFactory fileManagerFactory = new DummyFileManagerFactory();
         EasyMock.expect(container.getInstance(FileManagerFactory.class)).andReturn(fileManagerFactory).anyTimes();
 
diff --git a/plugins/json/src/test/java/org/apache/struts2/json/JSONActionRedirectResultTest.java b/plugins/json/src/test/java/org/apache/struts2/json/JSONActionRedirectResultTest.java
index 268ee6087..24dc6adf5 100644
--- a/plugins/json/src/test/java/org/apache/struts2/json/JSONActionRedirectResultTest.java
+++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONActionRedirectResultTest.java
@@ -26,10 +26,9 @@ import com.opensymphony.xwork2.util.ValueStack;
 import org.apache.struts2.StrutsStatics;
 import org.apache.struts2.dispatcher.mapper.DefaultActionMapper;
 import org.apache.struts2.junit.StrutsTestCase;
-import org.apache.struts2.url.StrutsParametersStringBuilder;
-import org.apache.struts2.url.StrutsUrlDecoder;
+import org.apache.struts2.url.QueryStringBuilder;
+import org.apache.struts2.url.StrutsQueryStringBuilder;
 import org.apache.struts2.url.StrutsUrlEncoder;
-import org.apache.struts2.views.util.DefaultUrlHelper;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.mock.web.MockServletContext;
@@ -37,7 +36,7 @@ import org.springframework.mock.web.MockServletContext;
 public class JSONActionRedirectResultTest extends StrutsTestCase {
 
     private DefaultActionMapper actionMapper;
-    private DefaultUrlHelper urlHelper;
+    private QueryStringBuilder queryStringBuilder;
 
     MockActionInvocation invocation;
     MockHttpServletResponse response;
@@ -50,7 +49,7 @@ public class JSONActionRedirectResultTest extends StrutsTestCase {
         JSONActionRedirectResult result = new JSONActionRedirectResult();
         result.setActionName("targetAction");
         result.setActionMapper(actionMapper);
-        result.setUrlHelper(urlHelper);
+        result.setQueryStringBuilder(queryStringBuilder);
 
         Object action = new Object();
         stack.push(action);
@@ -69,7 +68,7 @@ public class JSONActionRedirectResultTest extends StrutsTestCase {
         JSONActionRedirectResult result = new JSONActionRedirectResult();
         result.setActionName("targetAction");
         result.setActionMapper(actionMapper);
-        result.setUrlHelper(urlHelper);
+        result.setQueryStringBuilder(queryStringBuilder);
 
         request.setParameter("struts.enableJSONValidation", "true");
         request.setParameter("struts.validateOnly", "false");
@@ -89,7 +88,7 @@ public class JSONActionRedirectResultTest extends StrutsTestCase {
         JSONActionRedirectResult result = new JSONActionRedirectResult();
         result.setActionName("targetAction");
         result.setActionMapper(actionMapper);
-        result.setUrlHelper(urlHelper);
+        result.setQueryStringBuilder(queryStringBuilder);
 
         request.setParameter("struts.enableJSONValidation", "true");
         request.setParameter("struts.validateOnly", "true");
@@ -126,10 +125,6 @@ public class JSONActionRedirectResultTest extends StrutsTestCase {
         this.invocation.setProxy(mockActionProxy);
 
         this.actionMapper = new DefaultActionMapper();
-        this.urlHelper = new DefaultUrlHelper();
-        StrutsUrlEncoder encoder = new StrutsUrlEncoder();
-        this.urlHelper.setParametersStringBuilder(new StrutsParametersStringBuilder(encoder));
-        this.urlHelper.setEncoder(encoder);
-        this.urlHelper.setDecoder(new StrutsUrlDecoder());
+        this.queryStringBuilder = new StrutsQueryStringBuilder(new StrutsUrlEncoder());
     }
 }
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 a286deb3e..754389f83 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.url.QueryStringParser;
 import org.apache.struts2.views.util.UrlHelper;
 
 import javax.portlet.PortletMode;
@@ -66,6 +66,11 @@ public class PortletUrlRenderer implements UrlRenderer {
         servletRenderer.setUrlHelper(urlHelper);
     }
 
+    @Inject
+    public void setQueryStringParser(QueryStringParser queryStringParser) {
+        this.servletRenderer.setQueryStringParser(queryStringParser);
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -75,37 +80,35 @@ public class PortletUrlRenderer implements UrlRenderer {
             return;
         }
         String result;
-        if (isPortletModeChange(urlComponent,PortletActionContext.getRequest().getPortletMode())
-        		&& StringUtils.isEmpty(urlComponent.getNamespace()))
-        {
-        	String mode = urlComponent.getPortletMode();
-        	PortletMode portletMode = new PortletMode(mode);
-        	String action = urlComponent.getAction();
-        	if (StringUtils.isEmpty(action)) {
-        		action = PortletActionContext.getModeActionMap().get(portletMode).getName();
-        	}
-        	String modeNamespace = PortletActionContext.getModeNamespaceMap().get(portletMode);
-    		result = portletUrlHelper.buildUrl(action, modeNamespace, urlComponent.getMethod(),
-    				urlComponent.getParameters(), urlComponent.getPortletUrlType(), mode, urlComponent.getWindowState());
+        if (isPortletModeChange(urlComponent, PortletActionContext.getRequest().getPortletMode())
+            && StringUtils.isEmpty(urlComponent.getNamespace())) {
+            String mode = urlComponent.getPortletMode();
+            PortletMode portletMode = new PortletMode(mode);
+            String action = urlComponent.getAction();
+            if (StringUtils.isEmpty(action)) {
+                action = PortletActionContext.getModeActionMap().get(portletMode).getName();
+            }
+            String modeNamespace = PortletActionContext.getModeNamespaceMap().get(portletMode);
+            result = portletUrlHelper.buildUrl(action, modeNamespace, urlComponent.getMethod(),
+                urlComponent.getParameters(), urlComponent.getPortletUrlType(), mode, urlComponent.getWindowState());
 
         } else {
-        	String namespace = urlComponent.determineNamespace(urlComponent.getNamespace(), urlComponent.getStack(), urlComponent.getHttpServletRequest());
-        	urlComponent.setNamespace(namespace);
-        	if (onlyActionSpecified(urlComponent)) {
-        	    if (StringUtils.isNotEmpty(urlComponent.getAction())) {
-        	        String action = urlComponent.findString(urlComponent.getAction());
+            String namespace = urlComponent.determineNamespace(urlComponent.getNamespace(), urlComponent.getStack(), urlComponent.getHttpServletRequest());
+            urlComponent.setNamespace(namespace);
+            if (onlyActionSpecified(urlComponent)) {
+                if (StringUtils.isNotEmpty(urlComponent.getAction())) {
+                    String action = urlComponent.findString(urlComponent.getAction());
                     result = portletUrlHelper.buildUrl(action, urlComponent.getNamespace(), urlComponent.getMethod(),
-                                    urlComponent.getParameters(), urlComponent.getPortletUrlType(), urlComponent.getPortletMode(), urlComponent.getWindowState());
-        	    }
-        	    else {
+                        urlComponent.getParameters(), urlComponent.getPortletUrlType(), urlComponent.getPortletMode(), urlComponent.getWindowState());
+                } else {
                     result = portletUrlHelper.buildUrl(urlComponent.getAction(), urlComponent.getNamespace(), urlComponent.getMethod(),
-                                    urlComponent.getParameters(), urlComponent.getPortletUrlType(), urlComponent.getPortletMode(), urlComponent.getWindowState());
-        	    }
-        	} else if (onlyValueSpecified(urlComponent)) {
-        		result = portletUrlHelper.buildResourceUrl(urlComponent.getValue(), urlComponent.getParameters());
-        	} else {
-        		result = createDefaultUrl(urlComponent);
-        	}
+                        urlComponent.getParameters(), urlComponent.getPortletUrlType(), urlComponent.getPortletMode(), urlComponent.getWindowState());
+                }
+            } else if (onlyValueSpecified(urlComponent)) {
+                result = portletUrlHelper.buildResourceUrl(urlComponent.getValue(), urlComponent.getParameters());
+            } else {
+                result = createDefaultUrl(urlComponent);
+            }
         }
         String anchor = urlComponent.getAnchor();
         if (StringUtils.isNotEmpty(anchor)) {
@@ -128,19 +131,19 @@ public class PortletUrlRenderer implements UrlRenderer {
         }
     }
 
-	boolean isPortletModeChange(UrlProvider urlComponent,PortletMode currentMode) {
-		if (StringUtils.isNotEmpty(urlComponent.getPortletMode())) {
-			PortletMode newPortletMode = new PortletMode(urlComponent.getPortletMode());
-        	return !(newPortletMode.equals(currentMode));
+    boolean isPortletModeChange(UrlProvider urlComponent, PortletMode currentMode) {
+        if (StringUtils.isNotEmpty(urlComponent.getPortletMode())) {
+            PortletMode newPortletMode = new PortletMode(urlComponent.getPortletMode());
+            return !(newPortletMode.equals(currentMode));
         }
-		return false;
-	}
+        return false;
+    }
 
     private String createDefaultUrl(UrlProvider urlComponent) {
         ActionInvocation ai = urlComponent.getStack().getActionContext().getActionInvocation();
         String action = ai.getProxy().getActionName();
         return portletUrlHelper.buildUrl(action, urlComponent.getNamespace(), urlComponent.getMethod(), urlComponent.getParameters(),
-                urlComponent.getPortletUrlType(), urlComponent.getPortletMode(), urlComponent.getWindowState());
+            urlComponent.getPortletUrlType(), urlComponent.getPortletMode(), urlComponent.getWindowState());
     }
 
     private boolean onlyValueSpecified(UrlProvider urlComponent) {
@@ -160,7 +163,7 @@ public class PortletUrlRenderer implements UrlRenderer {
             return;
         }
         String namespace = formComponent.determineNamespace(formComponent.namespace, formComponent.getStack(),
-                formComponent.request);
+            formComponent.request);
         String action;
         if (formComponent.action != null) {
             action = formComponent.findString(formComponent.action);
@@ -177,7 +180,7 @@ public class PortletUrlRenderer implements UrlRenderer {
         }
         if (action != null) {
             String result = portletUrlHelper.buildUrl(action, namespace, null,
-                    formComponent.getParameters(), type, formComponent.portletMode, formComponent.windowState);
+                formComponent.getParameters(), type, formComponent.portletMode, formComponent.windowState);
             formComponent.addParameter("action", result);
 
 
diff --git a/plugins/portlet/src/main/java/org/apache/struts2/portlet/result/PortletActionRedirectResult.java b/plugins/portlet/src/main/java/org/apache/struts2/portlet/result/PortletActionRedirectResult.java
index eaef67c0a..c970c4189 100644
--- a/plugins/portlet/src/main/java/org/apache/struts2/portlet/result/PortletActionRedirectResult.java
+++ b/plugins/portlet/src/main/java/org/apache/struts2/portlet/result/PortletActionRedirectResult.java
@@ -21,11 +21,11 @@ package org.apache.struts2.portlet.result;
 import com.opensymphony.xwork2.ActionInvocation;
 import com.opensymphony.xwork2.config.entities.ResultConfig;
 import com.opensymphony.xwork2.inject.Inject;
-import org.apache.struts2.result.ServletActionRedirectResult;
 import org.apache.struts2.dispatcher.mapper.ActionMapper;
 import org.apache.struts2.dispatcher.mapper.ActionMapping;
 import org.apache.struts2.portlet.PortletConstants;
-import org.apache.struts2.views.util.UrlHelper;
+import org.apache.struts2.result.ServletActionRedirectResult;
+import org.apache.struts2.url.QueryStringBuilder;
 
 import javax.portlet.PortletMode;
 import java.util.Arrays;
@@ -34,11 +34,10 @@ import java.util.List;
 import java.util.Map;
 
 /**
- * 
  * Portlet modification of the {@link ServletActionRedirectResult}.
- * 
+ * <p>
  * <!-- START SNIPPET: description -->
- * 
+ * <p>
  * This result uses the {@link ActionMapper} provided by the
  * <code>ActionMapperFactory</code> to instruct the render phase to invoke the
  * specified action and (optional) namespace. This is better than the
@@ -48,31 +47,31 @@ import java.util.Map;
  * and your application will still work. It is strongly recommended that if you
  * are redirecting to another action, you use this result rather than the
  * standard redirect result.
- * 
+ * <p>
  * See examples below for an example of how request parameters could be passed
  * in.
- * 
+ * <p>
  * <!-- END SNIPPET: description -->
- * 
+ * <p>
  * <b>This result type takes the following parameters:</b>
- * 
+ * <p>
  * <!-- START SNIPPET: params -->
- * 
+ *
  * <ul>
- * 
+ *
  * <li><b>actionName (default)</b> - the name of the action that will be
  * redirect to</li>
- * 
+ *
  * <li><b>namespace</b> - used to determine which namespace the action is in
  * that we're redirecting to . If namespace is null, this defaults to the
  * current namespace</li>
- * 
+ *
  * </ul>
- * 
+ * <p>
  * <!-- END SNIPPET: params -->
- * 
+ * <p>
  * <b>Example:</b>
- * 
+ *
  * <pre>
  * &lt;!-- START SNIPPET: example --&gt;
  *  &lt;package name=&quot;public&quot; extends=&quot;struts-default&quot;&gt;
@@ -84,19 +83,19 @@ import java.util.Map;
  *          &lt;/result&gt;
  *      &lt;/action&gt;
  *  &lt;/package&gt;
- * 
+ *
  *  &lt;package name=&quot;secure&quot; extends=&quot;struts-default&quot; namespace=&quot;/secure&quot;&gt;
  *      &lt;-- Redirect to an action in the same namespace --&gt;
  *      &lt;action name=&quot;dashboard&quot; class=&quot;...&quot;&gt;
  *          &lt;result&gt;dashboard.jsp&lt;/result&gt;
  *          &lt;result name=&quot;error&quot; type=&quot;redirect-action&quot;&gt;error&lt;/result&gt;
  *      &lt;/action&gt;
- * 
+ *
  *      &lt;action name=&quot;error&quot; class=&quot;...&quot;&gt;
  *          &lt;result&gt;error.jsp&lt;/result&gt;
  *      &lt;/action&gt;
  *  &lt;/package&gt;
- * 
+ *
  *  &lt;package name=&quot;passingRequestParameters&quot; extends=&quot;struts-default&quot; namespace=&quot;/passingRequestParameters&quot;&gt;
  *     &lt;-- Pass parameters (reportType, width and height) --&gt;
  *     &lt;!--
@@ -113,106 +112,109 @@ import java.util.Map;
  *        &lt;/result&gt;
  *     &lt;/action&gt;
  *  &lt;/package&gt;
- * 
- * 
+ *
+ *
  *  &lt;!-- END SNIPPET: example --&gt;
  * </pre>
- * 
+ *
  * @see ActionMapper
  */
 public class PortletActionRedirectResult extends PortletResult {
 
-	private static final long serialVersionUID = -7627388936683562557L;
+    private static final long serialVersionUID = -7627388936683562557L;
 
-	/** The default parameter */
-	public static final String DEFAULT_PARAM = "actionName";
+    /**
+     * The default parameter
+     */
+    public static final String DEFAULT_PARAM = "actionName";
 
-	protected String actionName;
-	protected String namespace;
-	protected String method;
+    protected String actionName;
+    protected String namespace;
+    protected String method;
 
-	private Map<String, Object> requestParameters = new LinkedHashMap<String, Object>();
-	private ActionMapper actionMapper;
-    private UrlHelper urlHelper;
+    private final Map<String, Object> requestParameters = new LinkedHashMap<>();
 
-	public PortletActionRedirectResult() {
-		super();
-	}
+    private ActionMapper actionMapper;
+    private QueryStringBuilder queryStringBuilder;
 
-	public PortletActionRedirectResult(String actionName) {
-		this(null, actionName, null);
-	}
+    public PortletActionRedirectResult() {
+        super();
+    }
 
-	public PortletActionRedirectResult(String actionName, String method) {
-		this(null, actionName, method);
-	}
+    public PortletActionRedirectResult(String actionName) {
+        this(null, actionName, null);
+    }
 
-	public PortletActionRedirectResult(String namespace, String actionName, String method) {
-		super(null);
-		this.namespace = namespace;
-		this.actionName = actionName;
-		this.method = method;
-	}
+    public PortletActionRedirectResult(String actionName, String method) {
+        this(null, actionName, method);
+    }
 
-	protected List<String> prohibitedResultParam = Arrays.asList(DEFAULT_PARAM, "namespace", "method", "encode", "parse",
-            "location", "prependServletContext");
+    public PortletActionRedirectResult(String namespace, String actionName, String method) {
+        super(null);
+        this.namespace = namespace;
+        this.actionName = actionName;
+        this.method = method;
+    }
 
-	@Inject
-	public void setActionMapper(ActionMapper actionMapper) {
-		this.actionMapper = actionMapper;
-	}
+    protected List<String> prohibitedResultParam = Arrays.asList(DEFAULT_PARAM, "namespace", "method", "encode", "parse",
+        "location", "prependServletContext");
 
     @Inject
-    public void setUrlHelper(UrlHelper urlHelper) {
-        this.urlHelper = urlHelper;
+    public void setActionMapper(ActionMapper actionMapper) {
+        this.actionMapper = actionMapper;
+    }
+
+    @Inject
+    public void setQueryStringBuilder(QueryStringBuilder queryStringBuilder) {
+        this.queryStringBuilder = queryStringBuilder;
     }
 
     /**
-	 * @see com.opensymphony.xwork2.Result#execute(com.opensymphony.xwork2.ActionInvocation)
-	 */
-	public void execute(ActionInvocation invocation) throws Exception {
-		if (invocation == null) {
-			throw new IllegalArgumentException("Invocation cannot be null!");
-		}
+     * @see com.opensymphony.xwork2.Result#execute(com.opensymphony.xwork2.ActionInvocation)
+     */
+    public void execute(ActionInvocation invocation) throws Exception {
+        if (invocation == null) {
+            throw new IllegalArgumentException("Invocation cannot be null!");
+        }
 
-		actionName = conditionalParse(actionName, invocation);
-		parseLocation = false;
+        actionName = conditionalParse(actionName, invocation);
+        parseLocation = false;
 
-		String portletNamespace = (String)invocation.getInvocationContext().get(PortletConstants.PORTLET_NAMESPACE);
-		if (portletMode != null) {
-			Map<PortletMode, String> namespaceMap = getNamespaceMap(invocation);
-			namespace = namespaceMap.get(portletMode);
-		}
-		if (namespace == null) {
-			namespace = invocation.getProxy().getNamespace();
-		} else {
-			namespace = conditionalParse(namespace, invocation);
-		}
-		if (method == null) {
-			method = "";
-		} else {
-			method = conditionalParse(method, invocation);
-		}
+        String portletNamespace = (String) invocation.getInvocationContext().get(PortletConstants.PORTLET_NAMESPACE);
+        if (portletMode != null) {
+            Map<PortletMode, String> namespaceMap = getNamespaceMap(invocation);
+            namespace = namespaceMap.get(portletMode);
+        }
+        if (namespace == null) {
+            namespace = invocation.getProxy().getNamespace();
+        } else {
+            namespace = conditionalParse(namespace, invocation);
+        }
+        if (method == null) {
+            method = "";
+        } else {
+            method = conditionalParse(method, invocation);
+        }
 
-		String resultCode = invocation.getResultCode();
-		if (resultCode != null) {
-			ResultConfig resultConfig = invocation.getProxy().getConfig().getResults().get(resultCode);
-			Map<String, String> resultConfigParams = resultConfig.getParams();
+        String resultCode = invocation.getResultCode();
+        if (resultCode != null) {
+            ResultConfig resultConfig = invocation.getProxy().getConfig().getResults().get(resultCode);
+            Map<String, String> resultConfigParams = resultConfig.getParams();
             for (Map.Entry<String, String> e : resultConfigParams.entrySet()) {
                 if (!prohibitedResultParam.contains(e.getKey())) {
                     requestParameters.put(e.getKey(), e.getValue() == null ? "" : conditionalParse(e.getValue(), invocation));
                 }
             }
-		}
+        }
 
-		StringBuilder tmpLocation = new StringBuilder(actionMapper.getUriFromActionMapping(new ActionMapping(actionName,
-				(portletNamespace == null ? namespace : portletNamespace + namespace), method, null)));
-		urlHelper.buildParametersString(requestParameters, tmpLocation, "&");
+        ActionMapping actionMapping = new ActionMapping(actionName, (portletNamespace == null ? namespace : portletNamespace + namespace), method, null);
+        StringBuilder tmpLocation = new StringBuilder(actionMapper.getUriFromActionMapping(actionMapping));
+        queryStringBuilder.build(requestParameters, tmpLocation, "&");
 
-		setLocation(tmpLocation.toString());
+        setLocation(tmpLocation.toString());
 
-		super.execute(invocation);
-	}
+        super.execute(invocation);
+    }
 
     @SuppressWarnings("unchecked")
     private Map<PortletMode, String> getNamespaceMap(ActionInvocation invocation) {
@@ -220,43 +222,42 @@ public class PortletActionRedirectResult extends PortletResult {
     }
 
     /**
-	 * Sets the action name
-	 * 
-	 * @param actionName The name
-	 */
-	public void setActionName(String actionName) {
-		this.actionName = actionName;
-	}
+     * Sets the action name
+     *
+     * @param actionName The name
+     */
+    public void setActionName(String actionName) {
+        this.actionName = actionName;
+    }
 
-	/**
-	 * Sets the namespace
-	 * 
-	 * @param namespace The namespace
-	 */
-	public void setNamespace(String namespace) {
-		this.namespace = namespace;
-	}
+    /**
+     * Sets the namespace
+     *
+     * @param namespace The namespace
+     */
+    public void setNamespace(String namespace) {
+        this.namespace = namespace;
+    }
 
-	/**
-	 * Sets the method
-	 * 
-	 * @param method The method
-	 */
-	public void setMethod(String method) {
-		this.method = method;
-	}
+    /**
+     * Sets the method
+     *
+     * @param method The method
+     */
+    public void setMethod(String method) {
+        this.method = method;
+    }
 
-	/**
-	 * Adds a request parameter to be added to the redirect url
-	 * 
-	 * @param key The parameter name
-	 * @param value The parameter value
-	 *
-	 * @return the portlet action redirect result
-	 */
-	public PortletActionRedirectResult addParameter(String key, Object value) {
-		requestParameters.put(key, String.valueOf(value));
-		return this;
-	}
+    /**
+     * Adds a request parameter to be added to the redirect url
+     *
+     * @param key   The parameter name
+     * @param value The parameter value
+     * @return the portlet action redirect result
+     */
+    public PortletActionRedirectResult addParameter(String key, Object value) {
+        requestParameters.put(key, String.valueOf(value));
+        return this;
+    }
 
 }