I have a command object:
public class Job {
private String jobType;
private String location;
}
Which is bound by spring-mvc:
#RequestMapping("/foo")
public String doSomethingWithJob(Job job) {
...
}
Which works fine for http://example.com/foo?jobType=permanent&location=Stockholm. But now I need to make it work for the following url instead:
http://example.com/foo?jt=permanent&loc=Stockholm
Obviously, I don't want to change my command object, because the field names have to remain long (as they are used in the code). How can I customize that? Is there an option to do something like this:
public class Job {
#RequestParam("jt")
private String jobType;
#RequestParam("loc")
private String location;
}
This doesn't work (#RequestParam can't be applied to fields).
The thing I'm thinking about is a custom message converter similar to FormHttpMessageConverter and read a custom annotation on the target object
This solution more concise but requires using RequestMappingHandlerAdapter, which Spring use when <mvc:annotation-driven /> enabled.
Hope it will help somebody.
The idea is to extend ServletRequestDataBinder like this:
/**
* ServletRequestDataBinder which supports fields renaming using {#link ParamName}
*
* #author jkee
*/
public class ParamNameDataBinder extends ExtendedServletRequestDataBinder {
private final Map<String, String> renameMapping;
public ParamNameDataBinder(Object target, String objectName, Map<String, String> renameMapping) {
super(target, objectName);
this.renameMapping = renameMapping;
}
#Override
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
super.addBindValues(mpvs, request);
for (Map.Entry<String, String> entry : renameMapping.entrySet()) {
String from = entry.getKey();
String to = entry.getValue();
if (mpvs.contains(from)) {
mpvs.add(to, mpvs.getPropertyValue(from).getValue());
}
}
}
}
Appropriate processor:
/**
* Method processor supports {#link ParamName} parameters renaming
*
* #author jkee
*/
public class RenamingProcessor extends ServletModelAttributeMethodProcessor {
#Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
//Rename cache
private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<Class<?>, Map<String, String>>();
public RenamingProcessor(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Class<?> targetClass = target.getClass();
if (!replaceMap.containsKey(targetClass)) {
Map<String, String> mapping = analyzeClass(targetClass);
replaceMap.put(targetClass, mapping);
}
Map<String, String> mapping = replaceMap.get(targetClass);
ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), mapping);
requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder, nativeWebRequest);
super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
}
private static Map<String, String> analyzeClass(Class<?> targetClass) {
Field[] fields = targetClass.getDeclaredFields();
Map<String, String> renameMap = new HashMap<String, String>();
for (Field field : fields) {
ParamName paramNameAnnotation = field.getAnnotation(ParamName.class);
if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) {
renameMap.put(paramNameAnnotation.value(), field.getName());
}
}
if (renameMap.isEmpty()) return Collections.emptyMap();
return renameMap;
}
}
Annotation:
/**
* Overrides parameter name
* #author jkee
*/
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface ParamName {
/**
* The name of the request parameter to bind to.
*/
String value();
}
Spring config:
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean class="ru.yandex.metrika.util.params.RenamingProcessor">
<constructor-arg name="annotationNotRequired" value="true"/>
</bean>
</mvc:argument-resolvers>
</mvc:annotation-driven>
And finally, usage (like Bozho solution):
public class Job {
#ParamName("job-type")
private String jobType;
#ParamName("loc")
private String location;
}
Here's what I got working:
First, a parameter resolver:
/**
* This resolver handles command objects annotated with #SupportsAnnotationParameterResolution
* that are passed as parameters to controller methods.
*
* It parses #CommandPerameter annotations on command objects to
* populate the Binder with the appropriate values (that is, the filed names
* corresponding to the GET parameters)
*
* In order to achieve this, small pieces of code are copied from spring-mvc
* classes (indicated in-place). The alternative to the copied lines would be to
* have a decorator around the Binder, but that would be more tedious, and still
* some methods would need to be copied.
*
* #author bozho
*
*/
public class AnnotationServletModelAttributeResolver extends ServletModelAttributeMethodProcessor {
/**
* A map caching annotation definitions of command objects (#CommandParameter-to-fieldname mappings)
*/
private ConcurrentMap<Class<?>, Map<String, String>> definitionsCache = Maps.newConcurrentMap();
public AnnotationServletModelAttributeResolver(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.getParameterType().isAnnotationPresent(SupportsAnnotationParameterResolution.class)) {
return true;
}
return false;
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
bind(servletRequest, servletBinder);
}
#SuppressWarnings("unchecked")
public void bind(ServletRequest request, ServletRequestDataBinder binder) {
Map<String, ?> propertyValues = parsePropertyValues(request, binder);
MutablePropertyValues mpvs = new MutablePropertyValues(propertyValues);
MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
if (multipartRequest != null) {
bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
}
// two lines copied from ExtendedServletRequestDataBinder
String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
mpvs.addPropertyValues((Map<String, String>) request.getAttribute(attr));
binder.bind(mpvs);
}
private Map<String, ?> parsePropertyValues(ServletRequest request, ServletRequestDataBinder binder) {
// similar to WebUtils.getParametersStartingWith(..) (prefixes not supported)
Map<String, Object> params = Maps.newTreeMap();
Assert.notNull(request, "Request must not be null");
Enumeration<?> paramNames = request.getParameterNames();
Map<String, String> parameterMappings = getParameterMappings(binder);
while (paramNames != null && paramNames.hasMoreElements()) {
String paramName = (String) paramNames.nextElement();
String[] values = request.getParameterValues(paramName);
String fieldName = parameterMappings.get(paramName);
// no annotation exists, use the default - the param name=field name
if (fieldName == null) {
fieldName = paramName;
}
if (values == null || values.length == 0) {
// Do nothing, no values found at all.
} else if (values.length > 1) {
params.put(fieldName, values);
} else {
params.put(fieldName, values[0]);
}
}
return params;
}
/**
* Gets a mapping between request parameter names and field names.
* If no annotation is specified, no entry is added
* #return
*/
private Map<String, String> getParameterMappings(ServletRequestDataBinder binder) {
Class<?> targetClass = binder.getTarget().getClass();
Map<String, String> map = definitionsCache.get(targetClass);
if (map == null) {
Field[] fields = targetClass.getDeclaredFields();
map = Maps.newHashMapWithExpectedSize(fields.length);
for (Field field : fields) {
CommandParameter annotation = field.getAnnotation(CommandParameter.class);
if (annotation != null && !annotation.value().isEmpty()) {
map.put(annotation.value(), field.getName());
}
}
definitionsCache.putIfAbsent(targetClass, map);
return map;
} else {
return map;
}
}
/**
* Copied from WebDataBinder.
*
* #param multipartFiles
* #param mpvs
*/
protected void bindMultipart(Map<String, List<MultipartFile>> multipartFiles, MutablePropertyValues mpvs) {
for (Map.Entry<String, List<MultipartFile>> entry : multipartFiles.entrySet()) {
String key = entry.getKey();
List<MultipartFile> values = entry.getValue();
if (values.size() == 1) {
MultipartFile value = values.get(0);
if (!value.isEmpty()) {
mpvs.add(key, value);
}
} else {
mpvs.add(key, values);
}
}
}
}
And then registering the parameter resolver using a post-processor. It should be registered as a <bean>:
/**
* Post-processor to be used if any modifications to the handler adapter need to be made
*
* #author bozho
*
*/
public class AnnotationHandlerMappingPostProcessor implements BeanPostProcessor {
#Override
public Object postProcessAfterInitialization(Object bean, String arg1)
throws BeansException {
return bean;
}
#Override
public Object postProcessBeforeInitialization(Object bean, String arg1)
throws BeansException {
if (bean instanceof RequestMappingHandlerAdapter) {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
List<HandlerMethodArgumentResolver> resolvers = adapter.getCustomArgumentResolvers();
if (resolvers == null) {
resolvers = Lists.newArrayList();
}
resolvers.add(new AnnotationServletModelAttributeResolver(false));
adapter.setCustomArgumentResolvers(resolvers);
}
return bean;
}
}
In Spring 3.1, ServletRequestDataBinder provides a hook for additional bind values:
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
}
The ExtendedServletRequestDataBinder subclass uses it to add URI template variables as binding values. You could extend it further to make it possible to add command-specific field aliases.
You can override RequestMappingHandlerAdapter.createDataBinderFactory(..) to provide a custom WebDataBinder instance. From a controller's perspective it could look like this:
#InitBinder
public void initBinder(MyWebDataBinder binder) {
binder.addFieldAlias("jobType", "jt");
// ...
}
Thanks the answer of #jkee .
Here is my solution.
First, a custom annotation:
#Inherited
#Documented
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
public #interface ParamName {
/**
* The name of the request parameter to bind to.
*/
String value();
}
A customer DataBinder:
public class ParamNameDataBinder extends ExtendedServletRequestDataBinder {
private final Map<String, String> paramMappings;
public ParamNameDataBinder(Object target, String objectName, Map<String, String> paramMappings) {
super(target, objectName);
this.paramMappings = paramMappings;
}
#Override
protected void addBindValues(MutablePropertyValues mutablePropertyValues, ServletRequest request) {
super.addBindValues(mutablePropertyValues, request);
for (Map.Entry<String, String> entry : paramMappings.entrySet()) {
String paramName = entry.getKey();
String fieldName = entry.getValue();
if (mutablePropertyValues.contains(paramName)) {
mutablePropertyValues.add(fieldName, mutablePropertyValues.getPropertyValue(paramName).getValue());
}
}
}
}
A parameter resolver:
public class ParamNameProcessor extends ServletModelAttributeMethodProcessor {
#Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
private static final Map<Class<?>, Map<String, String>> PARAM_MAPPINGS_CACHE = new ConcurrentHashMap<>(256);
public ParamNameProcessor() {
super(false);
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestParam.class)
&& !BeanUtils.isSimpleProperty(parameter.getParameterType())
&& Arrays.stream(parameter.getParameterType().getDeclaredFields())
.anyMatch(field -> field.getAnnotation(ParamName.class) != null);
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Map<String, String> paramMappings = this.getParamMappings(target.getClass());
ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), paramMappings);
requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder, nativeWebRequest);
super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
}
/**
* Get param mappings.
* Cache param mappings in memory.
*
* #param targetClass
* #return {#link Map<String, String>}
*/
private Map<String, String> getParamMappings(Class<?> targetClass) {
if (PARAM_MAPPINGS_CACHE.containsKey(targetClass)) {
return PARAM_MAPPINGS_CACHE.get(targetClass);
}
Field[] fields = targetClass.getDeclaredFields();
Map<String, String> paramMappings = new HashMap<>(32);
for (Field field : fields) {
ParamName paramName = field.getAnnotation(ParamName.class);
if (paramName != null && !paramName.value().isEmpty()) {
paramMappings.put(paramName.value(), field.getName());
}
}
PARAM_MAPPINGS_CACHE.put(targetClass, paramMappings);
return paramMappings;
}
}
Finally, a bean configuration for adding ParamNameProcessor into the first of argument resolvers:
#Configuration
public class WebConfig {
/**
* Processor for annotation {#link ParamName}.
*
* #return ParamNameProcessor
*/
#Bean
protected ParamNameProcessor paramNameProcessor() {
return new ParamNameProcessor();
}
/**
* Custom {#link BeanPostProcessor} for adding {#link ParamNameProcessor} into the first of
* {#link RequestMappingHandlerAdapter#argumentResolvers}.
*
* #return BeanPostProcessor
*/
#Bean
public BeanPostProcessor beanPostProcessor() {
return new BeanPostProcessor() {
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof RequestMappingHandlerAdapter) {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>(adapter.getArgumentResolvers());
argumentResolvers.add(0, paramNameProcessor());
adapter.setArgumentResolvers(argumentResolvers);
}
return bean;
}
};
}
}
Param pojo:
#Data
public class Foo {
private Integer id;
#ParamName("first_name")
private String firstName;
#ParamName("last_name")
private String lastName;
#ParamName("created_at")
#DateTimeFormat(pattern = "yyyy-MM-dd")
private Date createdAt;
}
Controller method:
#GetMapping("/foos")
public ResponseEntity<List<Foo>> listFoos(#RequestParam Foo foo, #PageableDefault(sort = "id") Pageable pageable) {
List<Foo> foos = fooService.listFoos(foo, pageable);
return ResponseEntity.ok(foos);
}
That's all.
There is a simple way, you can just add one more setter method, like "setLoc,setJt".
there is no nice built in way to do it, you can only choose which workaround you apply. The difference between handling
#RequestMapping("/foo")
public String doSomethingWithJob(Job job)
and
#RequestMapping("/foo")
public String doSomethingWithJob(String stringjob)
is that job is a bean and stringjob isn't (no surprise so far). The real difference is that beans are resolved with the standard Spring bean resolver mechanism, while string params are resolved by spring MVC that knows the concept of the #RequestParam annotation. To make the long story short there is no way in the standard spring bean resolution (that is using classes like PropertyValues, PropertyValue, GenericTypeAwarePropertyDescriptor) to resolve "jt" to a property called "jobType" or at least I dont know about it.
The workarounds coud be as others suggested to add a custom PropertyEditor or a filter, but I think it just messes up the code. In my opinion the cleanest solution would be to declare a class like this :
public class JobParam extends Job {
public String getJt() {
return super.job;
}
public void setJt(String jt) {
super.job = jt;
}
}
then use that in your controller
#RequestMapping("/foo")
public String doSomethingWithJob(JobParam job) {
...
}
UPDATE :
A slightly simpler option is to not to extend, just add the extra getters, setters to the original class
public class Job {
private String jobType;
private String location;
public String getJt() {
return jobType;
}
public void setJt(String jt) {
jobType = jt;
}
}
You can use Jackson com.fasterxml.jackson.databind.ObjectMapper to convert any map to your DTO/POJO class with nested props. You need annotate your POJOs with #JsonUnwrapped on nested object. Like this:
public class MyRequest {
#JsonUnwrapped
private NestedObject nested;
public NestedObject getNested() {
return nested;
}
}
And than use it like this:
#RequestMapping(method = RequestMethod.GET, value = "/myMethod")
#ResponseBody
public Object myMethod(#RequestParam Map<String, Object> allRequestParams) {
MyRequest request = new ObjectMapper().convertValue(allRequestParams, MyRequest.class);
...
}
That's all. A little coding. Also, you can give any names to your props usign #JsonProperty.
I would like to point you to another direction. But I do not know if it works.
I would try to manipulate the binding itself.
It is done by WebDataBinder and will be invoked from HandlerMethodInvoker method Object[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception
I have no deep look in Spring 3.1, but what I have seen, is that this part of Spring has been changed a lot. So it is may possible to exchange the WebDataBinder. In Spring 3.0 it seams not possible without overriding the HandlerMethodInvoker.
Try intercepting request using InterceptorAdaptor, and then using simple checking mechanism decide whether to foward the request to the controller handler. Also wrap HttpServletRequestWrapper around the request, to enable you override the requests getParameter().
This way you can repass the actual parameter name and its value back to the request to be seen by the controller.
Example option:
public class JobInterceptor extends HandlerInterceptorAdapter {
private static final String requestLocations[]={"rt", "jobType"};
private boolean isEmpty(String arg)
{
return (arg !=null && arg.length() > 0);
}
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
//Maybe something like this
if(!isEmpty(request.getParameter(requestLocations[0]))|| !isEmpty(request.getParameter(requestLocations[1]))
{
final String value =
!isEmpty(request.getParameter(requestLocations[0])) ? request.getParameter(requestLocations[0]) : !isEmpty(request
.getParameter(requestLocations[1])) ? request.getParameter(requestLocations[1]) : null;
HttpServletRequest wrapper = new HttpServletRequestWrapper(request)
{
public String getParameter(String name)
{
super.getParameterMap().put("JobType", value);
return super.getParameter(name);
}
};
//Accepted request - Handler should carry on.
return super.preHandle(request, response, handler);
}
//Ignore request if above condition was false
return false;
}
}
Finally wrap the HandlerInterceptorAdaptor around your controller handler as shown below. The SelectedAnnotationHandlerMapping allows you to specify which handler will be interecepted.
<bean id="jobInterceptor" class="mypackage.JobInterceptor"/>
<bean id="publicMapper" class="org.springplugins.web.SelectedAnnotationHandlerMapping">
<property name="urls">
<list>
<value>/foo</value>
</list>
</property>
<property name="interceptors">
<list>
<ref bean="jobInterceptor"/>
</list>
</property>
</bean>
EDITED.
There's a little improvement to jkee's answer.
In order to support inheritance you should also analyze parent classes.
/**
* ServletRequestDataBinder which supports fields renaming using {#link ParamName}
*
* #author jkee
* #author Yauhen Parmon
*/
public class ParamRenamingProcessor extends ServletModelAttributeMethodProcessor {
#Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
//Rename cache
private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<>();
public ParamRenamingProcessor(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Class<?> targetClass = Objects.requireNonNull(target).getClass();
if (!replaceMap.containsKey(targetClass)) {
replaceMap.put(targetClass, analyzeClass(targetClass));
}
Map<String, String> mapping = replaceMap.get(targetClass);
ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), mapping);
Objects.requireNonNull(requestMappingHandlerAdapter.getWebBindingInitializer())
.initBinder(paramNameDataBinder);
super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
}
private Map<String, String> analyzeClass(Class<?> targetClass) {
Map<String, String> renameMap = new HashMap<>();
for (Field field : targetClass.getDeclaredFields()) {
ParamName paramNameAnnotation = field.getAnnotation(ParamName.class);
if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) {
renameMap.put(paramNameAnnotation.value(), field.getName());
}
}
if (targetClass.getSuperclass() != Object.class) {
renameMap.putAll(analyzeClass(targetClass.getSuperclass()));
}
return renameMap;
}
}
This processor will analyze fields of superclasses annotated with #ParamName. It also doesn't use initBinder method with 2 parameters which is deprecated as of Spring 5.0. All the rest in jkee's answer is OK.
Related
I am trying to filter fields in a nested object:
class Response {
// These objects themselves can have many fields within
private final PropA a;
private final PropB b;
#JsonCreator
public Response(PropA a, PropB b) { ... }
}
I'd like a generic 'filter helper' to achieve the above logic. Here is what I have so far (following a similar approach as this project)
public class FilterHelper {
private final ObjectMapper objectMapper;
public FilterHelper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.objectMapper.addMixIn(Object.class, MyFilterMixin.class);
}
#JsonFilter("myfilter")
public static class MyFilterMixin {
}
private static class MyFilter extends SimpleBeanPropertyFilter {
private final Set<String> properties;
public MyFilter(Set<String> properties) {
super();
this.properties = properties;
}
#Override
public void serializeAsField(final Object pojo, final JsonGenerator jgen, final SerializerProvider provider,
final PropertyWriter writer) throws Exception {
System.out.println("************************** " + writer.getName());
if (properties.contains(writer.getName())) {
writer.serializeAsField(pojo, jgen, provider);
} else if (!jgen.canOmitFields()) {
writer.serializeAsOmittedField(pojo, jgen, provider);
}
}
}
public String filter(T obj, Set<String> fields) {
FilterProvider filterProvider = new SimpleFilterProvider().addFilter("myfilter", new MyFilter(fields));
return objectMapper.writer(filterProvider).writeValueAsString(obj);
}
}
When I hit this endpoint with ?fields=one,two as query parameter I expect to see from a line printed to console for every field within that top level Response object as follows:
******************* a
******************* a1
******************* a2
******************* ..etc
******************* b
******************* b1
******************* b2
******************* ..etc
but I am only seeing output for the top level a and b fields followed by an error before getting a 500 status code from the endpoint:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot resolve PropertyFilter with id 'myfilter'; no FilterProvider configured (through reference chain: com.google.common.collect.SingletonImmutableList[0])
It is worth mentioning that I had this working somehow, but it was broken after some changes I don't recall.
Unless you need to provide custom serialization for different fields, you should not be hooking the serializeAsField and instead you should be overriding the #include variant methods:
com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter#include(com.fasterxml.jackson.databind.ser.BeanPropertyWriter)
com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter#include(com.fasterxml.jackson.databind.ser.PropertyWriter)
as follows:
private static class MyFilter extends SimpleBeanPropertyFilter {
private final Set<String> properties;
public MyFilter(Set<String> properties) {
super();
this.properties = properties;
}
#Override
protected boolean include(BeanPropertyWriter writer) {
return !this.properties.contains(writer.getName());
}
#Override
protected boolean include(PropertyWriter writer) {
return !this.properties.contains(writer.getName());
}
}
There is even a static factory providing a com.fasterxml.jackson.databind.ser.PropertyFilter that filters out a specific set of fields:
com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter#serializeAllExcept(java.util.Set<java.lang.String>)
Extra issue
At the filter helper level, you are serializing the filtered object to JSON then deserializing it back (with filtered fields) to an object that you are handing back as the endpoint response.
Solution / Alternative
You can simply omit the intermediary step by just sterilizing the result Response with the filter fields predicate and returning the result JSON as ResponseEntity:
FilterHelper:
#Component
public class FilterHelper {
private final ObjectMapper objectMapper;
#Autowired
public FilterHelper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.objectMapper.addMixIn(Object.class, MyFilterMixin.class);
}
#JsonFilter("myfilter")
public static class MyFilterMixin {
}
private static class MyFilter extends SimpleBeanPropertyFilter {
private final Set<String> properties;
public MyFilter(Set<String> properties) {
super();
this.properties = properties;
}
#Override
protected boolean include(BeanPropertyWriter writer) {
return !this.properties.contains(writer.getName());
}
#Override
protected boolean include(PropertyWriter writer) {
return !this.properties.contains(writer.getName());
}
}
public String filter(Object dto, Set<String> fields) {
if (fields == null || fields.isEmpty()) {
return "";
}
FilterProvider filterProvider = new SimpleFilterProvider()
.addFilter("myfilter", SimpleBeanPropertyFilter.serializeAllExcept(fields));
try {
return objectMapper.writer(filterProvider).writeValueAsString(dto);
} catch (JsonProcessingException e) {
return "";
}
}
}
Controller:
#GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(OK)
ReponseEntity<String> someEndpoint(#RequestParam(name = "fields") Set<String> fields) {
Response response = getResponseFromSomewhere();
return ResponseEntity.ok(filterHelper.filter(response, fields));
}
I have a spring REST controller which returns the following JSON payload:
[
{
"id": 5920,
"title": "a title"
},
{
"id": 5926,
"title": "another title",
}
]
The REST controller with its corresponding get request method:
#RequestMapping(value = "example")
public Iterable<Souvenir> souvenirs(#PathVariable("user") String user) {
return new souvenirRepository.findByUserUsernameOrderById(user);
}
Now the Souvenir class is a pojo:
#Entity
#Data
public class Souvenir {
#Id
#GeneratedValue
private long id;
private String title;
private Date date;
}
Regarding https://www.owasp.org/index.php/OWASP_AJAX_Security_Guidelines#Always_return_JSON_with_an_Object_on_the_outside and http://haacked.com/archive/2009/06/25/json-hijacking.aspx/ I would like to wrap the response within an object so that it is not vulnerable to attacks. Of course I could do something like this:
#RequestMapping(value = "example")
public SouvenirWrapper souvenirs(#PathVariable("user") String user) {
return new SouvenirWrapper(souvenirRepository.findByUserUsernameOrderById(user));
}
#Data
class SouvenirWrapper {
private final List<Souvenir> souvenirs;
public SouvenirWrapper(List<Souvenir> souvenirs) {
this.souvenirs = souvenirs;
}
}
This results in the following JSON payload:
{
"souvenirs": [
{
"id": 5920,
"title": "a title"
},
{
"id": 5926,
"title": "another title",
}
]
}
This helps in preventing some JSON/Javascript attacks but I don't like the verbosity of the Wrapper class. I could of course generalize the above approach with generics. Is there another way to achieve the same result in the Spring ecosystem (with an annotation or something similar)? An idea would be that the behaviour is done by Spring automatically, so whenever there is a REST controller that returns a list of objects, it could wrap those objects within an object wrapper so that no direct list of objects get serialized?
I ended up with the following solution (thanks to #vadim-kirilchuk):
My controller still looks exactly as before:
#RequestMapping(value = "example")
public Iterable<Souvenir> souvenirs(#PathVariable("user") String user) {
return new souvenirRepository.findByUserUsernameOrderById(user);
}
I added the following implementation of ResponseBodyAdvice which basically gets executed when a controller in the referenced package tries to respond to a client call (to my understanding):
#ControllerAdvice(basePackages = "package.where.all.my.controllers.are")
public class JSONResponseWrapper implements ResponseBodyAdvice {
#Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
#Override
#SuppressWarnings("unchecked")
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof List) {
return new Wrapper<>((List<Object>) body);
}
return body;
}
#Data // just the lombok annotation which provides getter and setter
private class Wrapper<T> {
private final List<T> list;
public Wrapper(List<T> list) {
this.list = list;
}
}
}
So with this approach I can keep my existing method signature in my controller (public Iterable<Souvenir> souvenirs(#PathVariable("user") String user)) and future controllers don't have to worry about wrapping its Iterables within such a wrapper because the framework does this part of the work.
Based in your solution I ended up with a more flexible option. First I created an annotation to activate the behaviour whenever I want and with a customizable wrapper attribute name:
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Target({ElementType.TYPE, ElementType.METHOD})
public #interface JsonListWrapper {
String name() default "list";
}
This annotation can be used on the entity class so it's applied to all controllers responses of List<MyEntity> or can be used for specifics controller methods.
The ControllerAdvice will look like this (note that I return a Map<Object> to dynamically set the wrapper name as a map key).
public class WebResponseModifierAdvice implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(final MethodParameter returnType, final Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public Object beforeBodyWrite(final Object body,
final MethodParameter returnType,
final MediaType selectedContentType,
final Class<? extends HttpMessageConverter<?>> selectedConverterType,
final ServerHttpRequest request,
final ServerHttpResponse response) {
if (body instanceof List && selectedContentType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
return checkListWrapper(body, returnType);
} else {
return body;
}
}
/**
* Detects use of {#link JsonListWrapper} in a response like <tt>List<T></tt>
* in case it's necesary to wrap the answer.
*
* #param body body to be written in the response
* #param returnType controller method return type
* #return
*/
private Object checkListWrapper(final Object body,
final MethodParameter returnType) {
String wrapperName = null;
try {
// Checks class level annotation (List<C>).
String typeName = "";
String where = "";
String whereName = "";
// Gets generic type List<T>
Type[] actualTypeArgs = ((ParameterizedType) returnType.getGenericParameterType()).getActualTypeArguments();
if (actualTypeArgs.length > 0) {
Type listType = ((ParameterizedType) returnType.getGenericParameterType()).getActualTypeArguments()[0];
if (listType instanceof ParameterizedType) {
Type elementType = ((ParameterizedType) listType).getActualTypeArguments()[0];
elementType.getClass();
try {
typeName = elementType.getTypeName();
Class<?> clz = Class.forName(typeName);
JsonListWrapper classListWrapper = AnnotationUtils.findAnnotation(clz, JsonListWrapper.class);
if (classListWrapper != null) {
where = "clase";
whereName = typeName;
wrapperName = classListWrapper.name();
}
} catch (ClassNotFoundException e) {
log.error("Class not found" + elementType.getTypeName(), e);
}
}
}
// Checks method level annotations (prevails over class level)
JsonListWrapper methodListWrapper = AnnotationUtils.findAnnotation(returnType.getMethod(), JsonListWrapper.class);
if (methodListWrapper != null) {
where = "método";
whereName = returnType.getMethod().getDeclaringClass() + "." + returnType.getMethod().getName() + "()";
wrapperName = methodListWrapper.name();
}
if (wrapperName != null) {
if (log.isTraceEnabled()) {
log.trace("#JsonListWrapper detected {} {}. Wrapping List<{}> in \"{}\"", where, whereName, typeName, wrapperName);
}
final Map<String, Object> map = new HashMap<>(1);
map.put(wrapperName, body);
return map;
}
} catch(Exception ex) {
log.error("Error getting type of List in the response", ex);
}
return body;
}
}
This way you can use either:
#JsonListWrapper(name = "souvenirs")
public class Souvenir {
//...class members
}
...or
#JsonListWrapper(name = "souvenirs")
#RequestMapping(value = "example")
public ResponseEntity<List<Souvenir>> souvenirs(#PathVariable("user") String user) {
return new souvenirRepository.findByUserUsernameOrderById(user);
}
Mapping URL request parameters with Spring MVC to an object is fairly straightforward if you're using camelCase parameters in your request, but when presented with hyphen delimited values, how do you map these to an object?
Example for reference:
Controller:
#RestController
public class MyController {
#RequestMapping(value = "/search", method = RequestMethod.GET)
public ResponseEntity<String> search(RequestParams requestParams) {
return new ResponseEntity<>("my-val-1: " + requestParams.getMyVal1() + " my-val-2: " + requestParams.getMyVal2(), HttpStatus.OK);
}
}
Object to hold parameters:
public class RequestParams {
private String myVal1;
private String myVal2;
public RequestParams() {}
public String getMyVal1() {
return myVal1;
}
public void setMyVal1(String myVal1) {
this.myVal1 = myVal1;
}
public String getMyVal2() {
return myVal2;
}
public void setMyVal2(String myVal2) {
this.myVal2 = myVal2;
}
}
A request made like this works fine:
GET http://localhost:8080/search?myVal1=foo&myVal2=bar
But, what I want is for a request with hyphens to map to the object, like so:
GET http://localhost:8080/search?my-val-1=foo&my-val-2=bar
What do I need to configure in Spring to map url request parameters with hyphens to fields in an object? Bear in mind that we may have many parameters, so using a #RequestParam annotation for each field is not ideal.
I extended ServletRequestDataBinder and ServletModelAttributeMethodProcessor to solve the problem.
Consider that your domain object may already be annotated with #JsonProperty or #XmlElement for serialization. This example assumes this is the case. But you could also create your own custom annotation for this purpose e.g. #MyParamMapping.
An example of your annotated domain class is:
public class RequestParams {
#XmlElement(name = "my-val-1" )
#JsonProperty(value = "my-val-1")
private String myVal1;
#XmlElement(name = "my-val-2")
#JsonProperty(value = "my-val-2")
private String myVal2;
public RequestParams() {
}
public String getMyVal1() {
return myVal1;
}
public void setMyVal1(String myVal1) {
this.myVal1 = myVal1;
}
public String getMyVal2() {
return myVal2;
}
public void setMyVal2(String myVal2) {
this.myVal2 = myVal2;
}
}
You will need a SerletModelAttributeMethodProcessor to analyze the target class, generate a mapping, invoke your ServletRequestDataBinder.
public class KebabCaseProcessor extends ServletModelAttributeMethodProcessor {
public KebabCaseProcessor(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<Class<?>, Map<String, String>>();
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Class<?> targetClass = target.getClass();
if (!replaceMap.containsKey(targetClass)) {
Map<String, String> mapping = analyzeClass(targetClass);
replaceMap.put(targetClass, mapping);
}
Map<String, String> mapping = replaceMap.get(targetClass);
ServletRequestDataBinder kebabCaseDataBinder = new KebabCaseRequestDataBinder(target, binder.getObjectName(), mapping);
requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(kebabCaseDataBinder, nativeWebRequest);
super.bindRequestParameters(kebabCaseDataBinder, nativeWebRequest);
}
private static Map<String, String> analyzeClass(Class<?> targetClass) {
Field[] fields = targetClass.getDeclaredFields();
Map<String, String> renameMap = new HashMap<String, String>();
for (Field field : fields) {
XmlElement xmlElementAnnotation = field.getAnnotation(XmlElement.class);
JsonProperty jsonPropertyAnnotation = field.getAnnotation(JsonProperty.class);
if (xmlElementAnnotation != null && !xmlElementAnnotation.name().isEmpty()) {
renameMap.put(xmlElementAnnotation.name(), field.getName());
} else if (jsonPropertyAnnotation != null && !jsonPropertyAnnotation.value().isEmpty()) {
renameMap.put(jsonPropertyAnnotation.value(), field.getName());
}
}
if (renameMap.isEmpty())
return Collections.emptyMap();
return renameMap;
}
}
This KebabCaseProcessor will use reflection to get a list of mappings for your request object. It will then invoke the KebabCaseDataBinder - passing in the mappings.
#Configuration
public class KebabCaseRequestDataBinder extends ExtendedServletRequestDataBinder {
private final Map<String, String> renameMapping;
public KebabCaseRequestDataBinder(Object target, String objectName, Map<String, String> mapping) {
super(target, objectName);
this.renameMapping = mapping;
}
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
super.addBindValues(mpvs, request);
for (Map.Entry<String, String> entry : renameMapping.entrySet()) {
String from = entry.getKey();
String to = entry.getValue();
if (mpvs.contains(from)) {
mpvs.add(to, mpvs.getPropertyValue(from).getValue());
}
}
}
}
All that remains now is to add this behavior to your configuration. The following configuration overrides the default configuration that the #EnableWebMVC delivers and adds this behavior to your request processing.
#Configuration
public static class WebContextConfiguration extends WebMvcConfigurationSupport {
#Override
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(kebabCaseProcessor());
}
#Bean
protected KebabCaseProcessor kebabCaseProcessor() {
return new KebabCaseProcessor(true);
}
}
Credit should be given to #Jkee. This solution is derivative of an example he posted here: How to customize parameter names when binding spring mvc command objects.
One way I can think of getting around the hyphens is to use HttpServletRequestWrapper class to wrap the original request.
Parse all the request parameters in this class and convert all hyphenated parameters into camelcase. After this, spring will be able to automatically map those parameters to your POJO classes.
public class CustomRequestWrapper extends HttpServletRequestWrapper {
private Map<String, String> camelCasedParams = new Hashmap();
public CustomRequestWrapper(HttpServletRequest req){
//Get all params from request.
//Transform each param name from hyphenated to camel case
//Put them in camelCasedParams;
}
public String getParameter(String name){
return camelCasedParams.get(name);
}
//Similarly, override other methods related to request parameters
}
Inject this request wrapper from J2EE filter. You can refer to below link for a tutorial on injecting request wrappers using filter.
http://www.programcreek.com/java-api-examples/javax.servlet.http.HttpServletRequestWrapper
Update your web xml to include filter and its filter mapping.
Edit I added more detail to help others and left the original question for history
Background I have prototyped a REST call that returns JSON in a Spring Controller that works with my client software. The client software has a specific way it queries for data. That query is not compatible with the my Spring code, so I had a few lines that did the conversion. I refactored the conversion code into its own object. Instead of creating each time in my REST methods that require it, I would like to have it pre-populated before it gets to my method.
Question In a Spring Controller can I have Spring pre-populate an object from the values in the URL and the header, similar to how Spring populates and object from a form?
Current code
#RequestMapping(value="", headers = "Accept=application/json", method = RequestMethod.GET)
#ResponseBody
public ResponseEntity<String> searchUserProjects(
#RequestParam(required = false) String projectName,
#RequestParam(required = false) String sortBy,
#RequestHeader(value = "Range") String range) {
Original Question I know in Spring you can take the properties of a form and map them to an object. In addition, I know you can map a field to property converter object, I cannot remember the exact name, but I have done it. My question, is it possible to have Spring populate an object from values in the URL and the header and then pass that into the method instead of declaring them at the method signature of the controller?
Edit:
The registration method in the applicationContext.xml
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean class="app.util.dojo.DojoQueryProcessorHandlerMethodArgumentResolver"/>
</mvc:argument-resolvers>
</mvc:annotation-driven>
And the handler method with parameter
public ResponseEntity<String> searchUserProjects(#RequestParam(required = false) String projectName, #ProcessDojoQuery DojoRestQueryProcessor dojoQueryResults) {
DojoRestQueryProcessor.java
package app.util.dojo;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
public class DojoRestQueryProcessor {
protected String[] rangeArray;
protected String range;
protected String sortBy;
protected int startIndex;
protected int endIndex;
public DojoRestQueryProcessor() {
}
public DojoRestQueryProcessor(String range, String sortBy) {
if (range== null && sortBy == null)
return;
if (range.length() <= 3 || !range.contains("-"))
throw new DojoRestQueryProcessorException("Range value does not meet spec. " + range);
this.rangeArray = range.substring(6).split("-");
this.range = range;
this.sortBy = sortBy;
}
public PageRequest createPageRequest() {
startIndex = Integer.parseInt(rangeArray[0]);
endIndex = Integer.parseInt(rangeArray[1]);
if (startIndex >= endIndex)
throw new IllegalArgumentException("The starting index for a range needs to be less than the end index.");
Sort.Order[] sortOrders = null;
if (sortBy != null && sortBy.length() > 2)
sortOrders = convertDojoSortValuesToSpringSorts(sortBy.split(","));
int pageSize = endIndex-startIndex+1;
int pageNum = ((endIndex+1)/pageSize)-1;
PageRequest pageRequest = null;
if (sortOrders != null)
pageRequest = new PageRequest(pageNum, pageSize, new Sort(sortOrders));
else
pageRequest = new PageRequest(pageNum, pageSize);
return pageRequest;
}
public static Sort.Order[] convertDojoSortValuesToSpringSorts(String[] sortStrings) {
if (sortStrings == null)
return null;
Sort.Order[] sortOrders = new Sort.Order[sortStrings.length];
for (int i = 0; i < sortStrings.length; i++) {
String sortString = sortStrings[i];
if (sortString.startsWith("-")) {
sortOrders[i] = new Sort.Order(Direction.DESC, sortString.substring(1));
} else {
sortOrders[i] = new Sort.Order(Direction.ASC, sortString.substring(1));
}
}
return sortOrders;
}
public int getStartIndex() {
return startIndex;
}
public int getEndIndex() {
return endIndex;
}
public String getRange() {
return range;
}
public String getSortBy() {
return sortBy;
}
}
My Method Handler:
package app.util.dojo;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerMapping;
public class DojoQueryProcessorHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(ProcessDojoQuery.class) && parameter.getParameterType().equals(DojoRestQueryProcessor.class) ;
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory)
throws Exception {
String rangeField = parameter.getParameterAnnotation(ProcessDojoQuery.class).rangeField();
String sortByField = parameter.getParameterAnnotation(ProcessDojoQuery.class).sortByField();
String range = getRangeValue(rangeField, webRequest);
String sortBy = getSortByValue(sortByField, webRequest);
return new DojoRestQueryProcessor(range, sortBy);
}
private String getSortByValue(String rangeField, NativeWebRequest webRequest) {
Map<String, String> pathVariables = getPathVariables(webRequest);
return pathVariables.get(rangeField);
}
private Map<String, String> getPathVariables(NativeWebRequest webRequest) {
HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
return (Map<String, String>) httpServletRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
}
private String getHeaderValue(String headerName, NativeWebRequest webRequest) {
HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
return httpServletRequest.getHeader(headerName);
}
private String getRangeValue(String rangeField, NativeWebRequest webRequest) {
return getHeaderValue(rangeField, webRequest);
}
}
It is possible, but you would have to do it yourself (once).
The interface for this is HandlerMethodArgumentResolver. The way I see it is you would create an annotation, like #FromUrlAndHeaders and use that to annotate the parameter in the method:
#RequestMapping(value = "/someRequest/path")
public String doBusiness(#FromUrlAndHeaders CustomObject customObject) {
// do business with customObject
}
Then the fun part is creating your own HandlerMethodArgumentResolver.
public class FromUrlAndHeadersHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(FromUrlAndHeaders.class);
}
#Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// use the various objects here
// request to get parameters and headers
// mavContainer for model attributes (if you need)
// parameter for class type and annotation attributes
// etc.
// note that the parameter class type matters, are your creating a CustomObject, a String, a DifferentClassObject, etc...
}
}
You can then register this HandlerMethodArgumentResolver and let it do work.
The DispatcherServlet stack uses a list of HandlerMethodArgumentResolver implementation instances to decide what argument to pass to your method. There's one for #ModelAttribute, for #PathVariable, for #RequestParam, for #RequestBody, for ModelMap, for HttpServletRequest, for HttpServletResponse, basically for each parameter type supported by default. You can see all of them in the javadoc.
Related:
Spring MVC controller with multiple #RequestBody
Controller handler method supported return types
maybe i didn't get your question and this is not what your looking for, but if you want all parameters to be injected in action method,just declare it as :
#RequestMapping(method = { RequestMethod.POST })
public ResponseEntity doSomethingCool(#RequestParam Map<String, String> parameters) {
...
}
I am using a javax.servlet.http.HttpServletRequest to implement a web application.
I have no problem to get the parameter of a request using the getParameter method. However I don't know how to set a parameter in my request.
You can't, not using the standard API. HttpServletRequest represent a request received by the server, and so adding new parameters is not a valid option (as far as the API is concerned).
You could in principle implement a subclass of HttpServletRequestWrapper which wraps the original request, and intercepts the getParameter() methods, and pass the wrapped request on when you forward.
If you go this route, you should use a Filter to replace your HttpServletRequest with a HttpServletRequestWrapper:
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (servletRequest instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
// Check wether the current request needs to be able to support the body to be read multiple times
if (MULTI_READ_HTTP_METHODS.contains(request.getMethod())) {
// Override current HttpServletRequest with custom implementation
filterChain.doFilter(new HttpServletRequestWrapper(request), servletResponse);
return;
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
If you really want to do this, create an HttpServletRequestWrapper.
public class AddableHttpRequest extends HttpServletRequestWrapper {
private HashMap params = new HashMap();
public AddableingHttpRequest(HttpServletRequest request) {
super(request);
}
public String getParameter(String name) {
// if we added one, return that one
if ( params.get( name ) != null ) {
return params.get( name );
}
// otherwise return what's in the original request
HttpServletRequest req = (HttpServletRequest) super.getRequest();
return validate( name, req.getParameter( name ) );
}
public void addParameter( String name, String value ) {
params.put( name, value );
}
}
From your question, I think what you are trying to do is to store something (an object, a string...) to foward it then to another servlet, using RequestDispatcher().
To do this you don't need to set a paramater but an attribute using
void setAttribute(String name, Object o);
and then
Object getAttribute(String name);
The most upvoted solution generally works but for Spring and/or Spring Boot, the values will not wire to parameters in controller methods annotated with #RequestParam unless you specifically implemented getParameterValues(). I combined the solution(s) here and from this blog:
import java.util.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class MutableHttpRequest extends HttpServletRequestWrapper {
private final Map<String, String[]> mutableParams = new HashMap<>();
public MutableHttpRequest(final HttpServletRequest request) {
super(request);
}
public MutableHttpRequest addParameter(String name, String value) {
if (value != null)
mutableParams.put(name, new String[] { value });
return this;
}
#Override
public String getParameter(final String name) {
String[] values = getParameterMap().get(name);
return Arrays.stream(values)
.findFirst()
.orElse(super.getParameter(name));
}
#Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> allParameters = new HashMap<>();
allParameters.putAll(super.getParameterMap());
allParameters.putAll(mutableParams);
return Collections.unmodifiableMap(allParameters);
}
#Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(getParameterMap().keySet());
}
#Override
public String[] getParameterValues(final String name) {
return getParameterMap().get(name);
}
}
note that this code is not super-optimized but it works.
As mentioned in the previous posts, using an HttpServletReqiestWrapper is the way to go, however the missed part in those posts was that apart from overriding the method getParameter(), you should also override other parameter related methods to produce a consistent response. e.g. the value of a param added by the custom request wrapper should also be included in the parameters map returned by the method getParameterMap(). Here is an example:
public class AddableHttpRequest extends HttpServletRequestWrapper {
/** A map containing additional request params this wrapper adds to the wrapped request */
private final Map<String, String> params = new HashMap<>();
/**
* Constructs a request object wrapping the given request.
* #throws java.lang.IllegalArgumentException if the request is null
*/
AddableHttpRequest(final HttpServletRequest request) {
super(request)
}
#Override
public String getParameter(final String name) {
// if we added one with the given name, return that one
if ( params.get( name ) != null ) {
return params.get( name );
} else {
// otherwise return what's in the original request
return super.getParameter(name);
}
}
/**
* *** OVERRIDE THE METHODS BELOW TO REFLECT PARAMETERS ADDED BY THIS WRAPPER ****
*/
#Override
public Map<String, String> getParameterMap() {
// defaulf impl, should be overridden for an approprivate map of request params
return super.getParameterMap();
}
#Override
public Enumeration<String> getParameterNames() {
// defaulf impl, should be overridden for an approprivate map of request params names
return super.getParameterNames();
}
#Override
public String[] getParameterValues(final String name) {
// defaulf impl, should be overridden for an approprivate map of request params values
return super.getParameterValues(name);
}
}
The missing getParameterMap override ended up being a real problem for me. So this is what I ended up with:
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
/***
* Request wrapper enabling the update of a request-parameter.
*
* #author E.K. de Lang
*
*/
final class HttpServletRequestReplaceParameterWrapper
extends HttpServletRequestWrapper
{
private final Map<String, String[]> keyValues;
#SuppressWarnings("unchecked")
HttpServletRequestReplaceParameterWrapper(HttpServletRequest request, String key, String value)
{
super(request);
keyValues = new HashMap<String, String[]>();
keyValues.putAll(request.getParameterMap());
// Can override the values in the request
keyValues.put(key, new String[] { value });
}
#SuppressWarnings("unchecked")
HttpServletRequestReplaceParameterWrapper(HttpServletRequest request, Map<String, String> additionalRequestParameters)
{
super(request);
keyValues = new HashMap<String, String[]>();
keyValues.putAll(request.getParameterMap());
for (Map.Entry<String, String> entry : additionalRequestParameters.entrySet()) {
keyValues.put(entry.getKey(), new String[] { entry.getValue() });
}
}
#Override
public String getParameter(String name)
{
if (keyValues.containsKey(name)) {
String[] strings = keyValues.get(name);
if (strings == null || strings.length == 0) {
return null;
}
else {
return strings[0];
}
}
else {
// Just in case the request has some tricks of it's own.
return super.getParameter(name);
}
}
#Override
public String[] getParameterValues(String name)
{
String[] value = this.keyValues.get(name);
if (value == null) {
// Just in case the request has some tricks of it's own.
return super.getParameterValues(name);
}
else {
return value;
}
}
#Override
public Map<String, String[]> getParameterMap()
{
return this.keyValues;
}
}
Sorry, but why not use the following construction:
request.getParameterMap().put(parameterName, new String[] {parameterValue});