diff --git a/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameter.java b/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameter.java
index 1fbafa3c7f..9e936239ba 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameter.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameter.java
@@ -43,4 +43,15 @@
* In a practical sense, the depth dictates the number of periods or brackets that can appear in the parameter name.
*/
int depth() default 0;
+
+ /**
+ * Allows a dynamic property sink, such as a REST JSON or XML any-setter, to accept parameter
+ * names that do not correspond to declared members on the action.
+ *
+ * This is an explicit opt-in for input channels that support dynamic keys. Ordinary parameter
+ * injection ignores this flag, although placing {@code @StrutsParameter} on a field still makes
+ * that field eligible for ordinary query and form parameter injection. The {@link #depth()} value
+ * limits nesting below each dynamic key.
+ */
+ boolean allowDynamicKeys() default false;
}
diff --git a/plugins/rest/src/main/java/org/apache/struts2/rest/RestConstants.java b/plugins/rest/src/main/java/org/apache/struts2/rest/RestConstants.java
index cb47b6a939..d2675ecee0 100644
--- a/plugins/rest/src/main/java/org/apache/struts2/rest/RestConstants.java
+++ b/plugins/rest/src/main/java/org/apache/struts2/rest/RestConstants.java
@@ -23,6 +23,7 @@ public class RestConstants {
public static final String REST_LOGGER = "struts.rest.logger";
public static final String REST_DEFAULT_ERROR_RESULT_NAME = "struts.rest.defaultErrorResultName";
public static final String REST_CONTENT_RESTRICT_TO_GET = "struts.rest.content.restrictToGET";
+ public static final String REST_ANY_SETTER_REQUIRE_ANNOTATIONS = "struts.rest.anySetter.requireAnnotations";
public static final String REST_MAPPER_INDEX_METHOD_NAME = "struts.mapper.indexMethodName";
public static final String REST_MAPPER_GET_METHOD_NAME = "struts.mapper.getMethodName";
public static final String REST_MAPPER_POST_METHOD_NAME = "struts.mapper.postMethodName";
diff --git a/plugins/rest/src/main/java/org/apache/struts2/rest/config/entities/RestConstantConfig.java b/plugins/rest/src/main/java/org/apache/struts2/rest/config/entities/RestConstantConfig.java
index d83f01b42b..2b814cfe09 100644
--- a/plugins/rest/src/main/java/org/apache/struts2/rest/config/entities/RestConstantConfig.java
+++ b/plugins/rest/src/main/java/org/apache/struts2/rest/config/entities/RestConstantConfig.java
@@ -29,6 +29,7 @@ public class RestConstantConfig extends ConstantConfig {
private Boolean restLogger;
private String restDefaultErrorResultName;
private Boolean restContentRestrictToGet;
+ private Boolean restAnySetterRequireAnnotations;
private String mapperIndexMethodName;
private String mapperGetMethodName;
private String mapperPostMethodName;
@@ -50,6 +51,8 @@ public Map getAllAsStringsMap() {
map.put(RestConstants.REST_LOGGER, Objects.toString(restLogger, null));
map.put(RestConstants.REST_DEFAULT_ERROR_RESULT_NAME, restDefaultErrorResultName);
map.put(RestConstants.REST_CONTENT_RESTRICT_TO_GET, Objects.toString(restContentRestrictToGet, null));
+ map.put(RestConstants.REST_ANY_SETTER_REQUIRE_ANNOTATIONS,
+ Objects.toString(restAnySetterRequireAnnotations, null));
map.put(RestConstants.REST_MAPPER_INDEX_METHOD_NAME, mapperIndexMethodName);
map.put(RestConstants.REST_MAPPER_GET_METHOD_NAME, mapperGetMethodName);
map.put(RestConstants.REST_MAPPER_POST_METHOD_NAME, mapperPostMethodName);
@@ -98,6 +101,14 @@ public void setRestContentRestrictToGet(Boolean restContentRestrictToGet) {
this.restContentRestrictToGet = restContentRestrictToGet;
}
+ public Boolean getRestAnySetterRequireAnnotations() {
+ return restAnySetterRequireAnnotations;
+ }
+
+ public void setRestAnySetterRequireAnnotations(Boolean restAnySetterRequireAnnotations) {
+ this.restAnySetterRequireAnnotations = restAnySetterRequireAnnotations;
+ }
+
public String getMapperIndexMethodName() {
return mapperIndexMethodName;
}
diff --git a/plugins/rest/src/main/java/org/apache/struts2/rest/handler/JacksonJsonHandler.java b/plugins/rest/src/main/java/org/apache/struts2/rest/handler/JacksonJsonHandler.java
index 834661cd5d..20f520d420 100644
--- a/plugins/rest/src/main/java/org/apache/struts2/rest/handler/JacksonJsonHandler.java
+++ b/plugins/rest/src/main/java/org/apache/struts2/rest/handler/JacksonJsonHandler.java
@@ -21,9 +21,12 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.SerializationFeature;
+import org.apache.commons.lang3.BooleanUtils;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.StrutsConstants;
+import org.apache.struts2.rest.RestConstants;
+import org.apache.struts2.rest.handler.jackson.ParameterAuthorizingModule;
import java.io.IOException;
import java.io.Reader;
@@ -36,14 +39,19 @@ public class JacksonJsonHandler implements AuthorizationAwareContentTypeHandler
private static final String DEFAULT_CONTENT_TYPE = "application/json";
private String defaultEncoding = "ISO-8859-1";
+ private final ParameterAuthorizingModule parameterAuthorizingModule = new ParameterAuthorizingModule();
private ObjectMapper mapper = new ObjectMapper()
- .registerModule(new org.apache.struts2.rest.handler.jackson.ParameterAuthorizingModule());
+ .registerModule(parameterAuthorizingModule);
@Override
public void toObject(ActionInvocation invocation, Reader in, Object target) throws IOException {
mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
ObjectReader or = mapper.readerForUpdating(target);
- or.readValue(in);
+ try {
+ or.readValue(in);
+ } finally {
+ parameterAuthorizingModule.clearAuthorizationContext();
+ }
}
@Override
@@ -67,4 +75,9 @@ public String getExtension() {
public void setDefaultEncoding(String val) {
this.defaultEncoding = val;
}
+
+ @Inject(value = RestConstants.REST_ANY_SETTER_REQUIRE_ANNOTATIONS, required = false)
+ public void setAnySetterRequireAnnotations(String value) {
+ parameterAuthorizingModule.setRequireAnySetterAnnotations(BooleanUtils.toBoolean(value));
+ }
}
diff --git a/plugins/rest/src/main/java/org/apache/struts2/rest/handler/JacksonXmlHandler.java b/plugins/rest/src/main/java/org/apache/struts2/rest/handler/JacksonXmlHandler.java
index ccc102023e..a25b151aa6 100644
--- a/plugins/rest/src/main/java/org/apache/struts2/rest/handler/JacksonXmlHandler.java
+++ b/plugins/rest/src/main/java/org/apache/struts2/rest/handler/JacksonXmlHandler.java
@@ -20,9 +20,13 @@
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import org.apache.commons.lang3.BooleanUtils;
import org.apache.struts2.ActionInvocation;
+import org.apache.struts2.inject.Inject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.apache.struts2.rest.RestConstants;
+import org.apache.struts2.rest.handler.jackson.ParameterAuthorizingModule;
import java.io.IOException;
import java.io.Reader;
@@ -37,17 +41,22 @@ public class JacksonXmlHandler implements AuthorizationAwareContentTypeHandler {
private static final String DEFAULT_CONTENT_TYPE = "application/xml";
private final XmlMapper mapper;
+ private final ParameterAuthorizingModule parameterAuthorizingModule = new ParameterAuthorizingModule();
public JacksonXmlHandler() {
mapper = new XmlMapper();
- mapper.registerModule(new org.apache.struts2.rest.handler.jackson.ParameterAuthorizingModule());
+ mapper.registerModule(parameterAuthorizingModule);
}
@Override
public void toObject(ActionInvocation invocation, Reader in, Object target) throws IOException {
LOG.debug("Converting input into an object of: {}", target.getClass().getName());
ObjectReader or = mapper.readerForUpdating(target);
- or.readValue(in);
+ try {
+ or.readValue(in);
+ } finally {
+ parameterAuthorizingModule.clearAuthorizationContext();
+ }
}
@Override
@@ -67,4 +76,9 @@ public String getExtension() {
return "xml";
}
+ @Inject(value = RestConstants.REST_ANY_SETTER_REQUIRE_ANNOTATIONS, required = false)
+ public void setAnySetterRequireAnnotations(String value) {
+ parameterAuthorizingModule.setRequireAnySetterAnnotations(BooleanUtils.toBoolean(value));
+ }
+
}
diff --git a/plugins/rest/src/main/java/org/apache/struts2/rest/handler/jackson/AuthorizingSettableAnyProperty.java b/plugins/rest/src/main/java/org/apache/struts2/rest/handler/jackson/AuthorizingSettableAnyProperty.java
new file mode 100644
index 0000000000..6b11f13bf1
--- /dev/null
+++ b/plugins/rest/src/main/java/org/apache/struts2/rest/handler/jackson/AuthorizingSettableAnyProperty.java
@@ -0,0 +1,266 @@
+/*
+ * 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.rest.handler.jackson;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.BeanProperty;
+import com.fasterxml.jackson.databind.DeserializationConfig;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.deser.SettableAnyProperty;
+import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
+import com.fasterxml.jackson.databind.util.TokenBuffer;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.struts2.interceptor.parameter.ParameterAuthorizationContext;
+import org.apache.struts2.interceptor.parameter.StrutsParameter;
+
+import java.io.IOException;
+
+/**
+ * Requires an explicit {@link StrutsParameter#allowDynamicKeys()} opt-in before a Jackson
+ * any-setter can consume dynamic REST body properties.
+ */
+final class AuthorizingSettableAnyProperty extends SettableAnyProperty {
+
+ private static final long serialVersionUID = 1L;
+ private static final Logger LOG = LogManager.getLogger(AuthorizingSettableAnyProperty.class);
+ private static final Object REJECTED_VALUE = new Object();
+
+ private final SettableAnyProperty delegate;
+ private final StrutsParameter permission;
+ private final boolean creatorParameter;
+
+ AuthorizingSettableAnyProperty(SettableAnyProperty delegate) {
+ super(delegate.getProperty(), memberOf(delegate.getProperty()), delegate.getType(),
+ null, null, null);
+ this.delegate = delegate;
+ this.permission = permissionOf(delegate.getProperty());
+ this.creatorParameter = delegate.getParameterIndex() >= 0;
+ }
+
+ private static AnnotatedMember memberOf(BeanProperty property) {
+ return property == null ? null : property.getMember();
+ }
+
+ private static StrutsParameter permissionOf(BeanProperty property) {
+ AnnotatedMember member = memberOf(property);
+ return member == null ? null : member.getAnnotation(StrutsParameter.class);
+ }
+
+ @Override
+ public SettableAnyProperty withValueDeserializer(JsonDeserializer