Skip to content

Commit

Permalink
Merge pull request #692 from apache/WW-5310-fragment
Browse files Browse the repository at this point in the history
[WW-5310] Supports fragment in URL
  • Loading branch information
lukaszlenart committed Jun 28, 2023
2 parents 3891a6b + 4a85b6c commit 7a92cdc
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,10 @@ public void renderFormUrl(Form formComponent) {
}
}

Map<String, Object> actionParams = null;
QueryStringParser.Result queryStringResult = queryStringParser.empty();
if (action != null && action.indexOf('?') > 0) {
String queryString = action.substring(action.indexOf('?') + 1);
actionParams = queryStringParser.parse(queryString, false);
queryStringResult = queryStringParser.parse(queryString);
action = action.substring(0, action.indexOf('?'));
}

Expand All @@ -176,7 +176,7 @@ public void renderFormUrl(Form formComponent) {

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, queryStringResult.getQueryParams(), scheme, formComponent.includeContext, true, false, false);
formComponent.addParameter("action", result);

// let's try to get the actual action class and name
Expand Down Expand Up @@ -213,7 +213,7 @@ public void renderFormUrl(Form formComponent) {
LOG.warn("No configuration found for the specified action: '{}' in namespace: '{}'. Form action defaulting to 'action' attribute's literal value.", actionName, namespace);
}

String result = urlHelper.buildUrl(action, formComponent.request, formComponent.response, actionParams, scheme, formComponent.includeContext, true);
String result = urlHelper.buildUrl(action, formComponent.request, formComponent.response, queryStringResult.getQueryParams(), scheme, formComponent.includeContext, true);
formComponent.addParameter("action", result);

// namespace: cut out anything between the start and the last /
Expand Down Expand Up @@ -291,7 +291,11 @@ private void includeExtraParameters(UrlProvider urlComponent) {

private void includeGetParameters(UrlProvider urlComponent) {
String query = extractQueryString(urlComponent);
mergeRequestParameters(urlComponent.getValue(), urlComponent.getParameters(), queryStringParser.parse(query, false));
QueryStringParser.Result result = queryStringParser.parse(query);
mergeRequestParameters(urlComponent.getValue(), urlComponent.getParameters(), result.getQueryParams());
if (!result.getQueryFragment().isEmpty()) {
urlComponent.setAnchor(result.getQueryFragment());
}
}

private String extractQueryString(UrlProvider urlComponent) {
Expand Down Expand Up @@ -339,7 +343,7 @@ protected void mergeRequestParameters(String value, Map<String, Object> paramete
if (StringUtils.contains(value, "?")) {
String queryString = value.substring(value.indexOf('?') + 1);

mergedParams = queryStringParser.parse(queryString, false);
mergedParams = new LinkedHashMap<>(queryStringParser.parse(queryString).getQueryParams());
for (Map.Entry<String, ?> entry : contextParameters.entrySet()) {
if (!mergedParams.containsKey(entry.getKey())) {
mergedParams.put(entry.getKey(), entry.getValue());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.PageContext;
import java.util.Map;

/**
* <!-- START SNIPPET: description -->
Expand Down Expand Up @@ -140,9 +139,9 @@ public void doExecute(String finalLocation, ActionInvocation invocation) throws
if (StringUtils.isNotEmpty(finalLocation) && finalLocation.indexOf('?') > 0) {
String queryString = finalLocation.substring(finalLocation.indexOf('?') + 1);
HttpParameters parameters = getParameters(invocation);
Map<String, Object> queryParams = queryStringParser.parse(queryString, true);
if (queryParams != null && !queryParams.isEmpty()) {
parameters = HttpParameters.create(queryParams).withParent(parameters).build();
QueryStringParser.Result queryParams = queryStringParser.parse(queryString);
if (!queryParams.isEmpty()) {
parameters = HttpParameters.create(queryParams.getQueryParams()).withParent(parameters).build();
invocation.getInvocationContext().withParameters(parameters);
// put to extraContext, see Dispatcher#createContextMap
invocation.getInvocationContext().getContextMap().put("parameters", parameters);
Expand Down
38 changes: 37 additions & 1 deletion core/src/main/java/org/apache/struts2/url/QueryStringParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,47 @@
import java.util.Map;

/**
* Used to parse Http Query String into a Map of parameters
* Used to parse Http Query String into a map of parameters with support for fragment
*
* @since Struts 6.1.0
*/
public interface QueryStringParser extends Serializable {

/**
* @deprecated since Struts 6.2.0, use {@link #parse(String)} instead
*/
@Deprecated
Map<String, Object> parse(String queryString, boolean forceValueArray);

/**
* @param queryString a query string to parse
* @return a {@link Result} of parsing the query string
* @since Struts 6.2.0
*/
Result parse(String queryString);

/**
* Return an empty {@link Result}
* @return empty result
*/
Result empty();

/**
* Represents result of parsing query string by implementation of {@link QueryStringParser}
*/
interface Result {

Result addParam(String name, String value);

Result withQueryFragment(String queryFragment);

Map<String, Object> getQueryParams();

String getQueryFragment();

boolean contains(String name);

boolean isEmpty();
}

}
111 changes: 91 additions & 20 deletions core/src/main/java/org/apache/struts2/url/StrutsQueryStringParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,18 @@ public StrutsQueryStringParser(UrlDecoder decoder) {

@Override
public Map<String, Object> parse(String queryString, boolean forceValueArray) {
return parse(queryString).getQueryParams();
}

@Override
public Result parse(String queryString) {
if (StringUtils.isEmpty(queryString)) {
LOG.debug("Query String is empty, returning an empty map");
return Collections.emptyMap();
return this.empty();
}

Map<String, Object> queryParams = new LinkedHashMap<>();
String[] params = queryString.split("&");
Result queryParams = StrutsQueryStringParserResult.create();
String[] params = extractParams(queryString);
for (String param : params) {
if (StringUtils.isBlank(param)) {
LOG.debug("Param [{}] is blank, skipping", param);
Expand All @@ -64,32 +69,98 @@ public Map<String, Object> parse(String queryString, boolean forceValueArray) {
} else {
paramName = param;
}
extractParam(paramName, paramValue, queryParams, forceValueArray);
queryParams = extractParam(paramName, paramValue, queryParams);
}
return queryParams.withQueryFragment(extractFragment(queryString));
}

@Override
public Result empty() {
return new StrutsQueryStringParserResult(Collections.emptyMap(), "");
}

private String[] extractParams(String queryString) {
LOG.trace("Extracting params from query string: {}", queryString);
String[] params = queryString.split("&");

int fragmentIndex = queryString.lastIndexOf("#");
if (fragmentIndex > -1) {
LOG.trace("Stripping fragment at index: {}", fragmentIndex);
params = queryString.substring(0, fragmentIndex).split("&");
}
return queryParams;
return params;
}

private void extractParam(String paramName, String paramValue, Map<String, Object> queryParams, boolean forceValueArray) {
private Result extractParam(String paramName, String paramValue, Result queryParams) {
String decodedParamName = decoder.decode(paramName, true);
String decodedParamValue = decoder.decode(paramValue, true);
return queryParams.addParam(decodedParamName, decodedParamValue);
}

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]));
private String extractFragment(String queryString) {
int fragmentIndex = queryString.lastIndexOf("#");
if (fragmentIndex > -1) {
return queryString.substring(fragmentIndex + 1);
}
return "";
}

public static class StrutsQueryStringParserResult implements Result {

private final Map<String, Object> queryParams;
private String queryFragment;

static Result create() {
return new StrutsQueryStringParserResult(new LinkedHashMap<>(), "");
}

private StrutsQueryStringParserResult(Map<String, Object> queryParams, String queryFragment) {
this.queryParams = queryParams;
this.queryFragment = queryFragment;
}

public Result addParam(String name, String value) {
if (queryParams.containsKey(name)) {
// WW-1619 append new param value to existing value(s)
Object currentParam = queryParams.get(name);
if (currentParam instanceof String) {
queryParams.put(name, new String[]{(String) currentParam, value});
} else {
queryParams.put(decodedParamName, new String[]{decodedParamValue});
String[] currentParamValues = (String[]) currentParam;
if (currentParamValues != null) {
List<String> paramList = new ArrayList<>(Arrays.asList(currentParamValues));
paramList.add(value);
queryParams.put(name, paramList.toArray(new String[0]));
} else {
queryParams.put(name, new String[]{value});
}
}
} else {
queryParams.put(name, value);
}
} else {
queryParams.put(decodedParamName, decodedParamValue);

return this;
}

public Result withQueryFragment(String queryFragment) {
this.queryFragment = queryFragment;
return this;
}

public Map<String, Object> getQueryParams() {
return Collections.unmodifiableMap(queryParams);
}

public String getQueryFragment() {
return queryFragment;
}

public boolean contains(String name) {
return queryParams.containsKey(name);
}

public boolean isEmpty() {
return queryParams.isEmpty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class StrutsQueryStringParserTest {

@Test
public void testParseQuery() {
Map<String, Object> result = parser.parse("aaa=aaaval&bbb=bbbval&ccc=&%3Ca%22%3E=%3Cval%3E", false);
Map<String, Object> result = parser.parse("aaa=aaaval&bbb=bbbval&ccc=&%3Ca%22%3E=%3Cval%3E").getQueryParams();

assertEquals("aaaval", result.get("aaa"));
assertEquals("bbbval", result.get("bbb"));
Expand All @@ -45,7 +45,7 @@ public void testParseQuery() {

@Test
public void testParseQueryIntoArray() {
Map<String, Object> result = parser.parse("a=1&a=2&a=3", true);
Map<String, Object> result = parser.parse("a=1&a=2&a=3").getQueryParams();

Object actual = result.get("a");
assertThat(actual).isInstanceOf(String[].class);
Expand All @@ -54,31 +54,31 @@ public void testParseQueryIntoArray() {

@Test
public void testParseEmptyQuery() {
Map<String, Object> result = parser.parse("", false);
Map<String, Object> result = parser.parse("").getQueryParams();

assertNotNull(result);
assertEquals(0, result.size());
}

@Test
public void testParseNullQuery() {
Map<String, Object> result = parser.parse(null, false);
Map<String, Object> result = parser.parse(null).getQueryParams();

assertNotNull(result);
assertEquals(0, result.size());
}

@Test
public void testDecodeSpacesInQueryString() {
Map<String, Object> queryParameters = parser.parse("name=value+with+space", false);
Map<String, Object> queryParameters = parser.parse("name=value+with+space").getQueryParams();

assertTrue(queryParameters.containsKey("name"));
assertEquals("value with space", queryParameters.get("name"));
}

@Test
public void shouldProperlySplitParamsWithDoubleEqualSign() {
Map<String, Object> queryParameters = parser.parse("id1=n123=&id2=n3456", false);
Map<String, Object> queryParameters = parser.parse("id1=n123=&id2=n3456").getQueryParams();

assertTrue(queryParameters.containsKey("id1"));
assertTrue(queryParameters.containsKey("id2"));
Expand All @@ -88,21 +88,30 @@ public void shouldProperlySplitParamsWithDoubleEqualSign() {

@Test
public void shouldHandleParamWithNoValue1() {
Map<String, Object> queryParameters = parser.parse("paramNoValue", false);
Map<String, Object> queryParameters = parser.parse("paramNoValue").getQueryParams();

assertTrue(queryParameters.containsKey("paramNoValue"));
assertEquals("", queryParameters.get("paramNoValue"));
}

@Test
public void shouldHandleParamWithNoValue2() {
Map<String, Object> queryParameters = parser.parse("paramNoValue&param1=1234", false);
Map<String, Object> queryParameters = parser.parse("paramNoValue&param1=1234").getQueryParams();

assertTrue(queryParameters.containsKey("paramNoValue"));
assertTrue(queryParameters.containsKey("param1"));
assertEquals("1234", queryParameters.get("param1"));
}

@Test
public void shouldHandleParamAndFragment() {
QueryStringParser.Result queryParameters = parser.parse("param1=1234#test");

assertTrue(queryParameters.getQueryParams().containsKey("param1"));
assertEquals("1234", queryParameters.getQueryParams().get("param1"));
assertEquals("test", queryParameters.getQueryFragment());
}

@Before
public void setUp() throws Exception {
this.parser = new StrutsQueryStringParser(new StrutsUrlDecoder());
Expand Down

0 comments on commit 7a92cdc

Please sign in to comment.