Skip to content

Instantly share code, notes, and snippets.

@agentgt
Last active December 27, 2022 00:37
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save agentgt/4458079 to your computer and use it in GitHub Desktop.
Save agentgt/4458079 to your computer and use it in GitHub Desktop.
Spring Immutable Object Web Data Binding
public class ExampleController {
@RequestMapping(value = {"/blah", ""})
public @ResponseBody Map<String, Object> blah(@Valid Blah blah, BindingResult errors, ModelMap model) {
if (errors.hasErrors()) {
return ModelUtils.mapBuilder().put("status", "errors").build();
}
return ModelUtils.mapBuilder().put("status", blah.getFirst() + " " + blah.getLast()).build();
}
public static class Blah {
@NotNull
private final String first;
private final String last;
@JsonCreator
private Blah(@JsonProperty("first") String first, @JsonProperty("last") String last) {
super();
this.first = first;
this.last = last;
}
public String getFirst() {
return first;
}
public String getLast() {
return last;
}
}
}
package com.snaphop.spring;
import static java.util.Arrays.asList;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
import javax.annotation.concurrent.Immutable;
import org.codehaus.jackson.annotate.JsonCreator;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
public class ImmutableObjectMessageConverter extends AbstractHttpMessageConverter<Object> {
private final ObjectMapper objectMapper = new ObjectMapper();
private LoadingCache<Class<?>, Boolean> supportsClassCache = CacheBuilder.newBuilder()
.weakKeys()
.build(new CacheLoader<Class<?>, Boolean>() {
@Override
public Boolean load(Class<?> key) throws Exception {
try {
//Only allow immutable objects that support Jackson
if( key.getAnnotation(Immutable.class) != null && objectMapper.canSerialize(key))
return true;
Constructor<?>[] cons = key.getDeclaredConstructors();
for (Constructor<?> c : cons) {
JsonCreator jc = c.getAnnotation(JsonCreator.class);
if (jc != null && objectMapper.canSerialize(key))
return true;
}
} catch (Exception e) {
//Reflection might have failed maybe because of security manager
}
return false;
}
});
private Charset charset = Charset.forName("UTF-8");
@Override
protected boolean supports(Class<?> clazz) {
try {
return supportsClassCache.get(clazz);
} catch (ExecutionException e) {
return false;
}
}
@Override
protected boolean canRead(MediaType mediaType) {
return true;
//return mediaType == MediaType.APPLICATION_FORM_URLENCODED;
}
@Override
protected boolean canWrite(MediaType mediaType) {
return false;
}
@Override
protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
ServletServerHttpRequest servletRequest = inputMessage instanceof ServletServerHttpRequest
? (ServletServerHttpRequest) inputMessage : null;
final MultiValueMap<String, String> result;
if (servletRequest == null) {
/*
* Stolen from org.springframework.http.converter.FormHttpMessageConverter
*/
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset = contentType.getCharSet() != null ? contentType.getCharSet() : this.charset;
String body = FileCopyUtils.copyToString(new InputStreamReader(inputMessage.getBody(), charset));
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
result = new LinkedMultiValueMap<String, String>(pairs.length);
for (String pair : pairs) {
int idx = pair.indexOf('=');
if (idx == -1) {
result.add(URLDecoder.decode(pair, charset.name()), null);
}
else {
String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
result.add(name, value);
}
}
}
else {
result = new LinkedMultiValueMap<String, String>(servletRequest.getServletRequest().getParameterMap().size());
Map<?,?> m = servletRequest.getServletRequest().getParameterMap();
for (Entry<?,?> e : m.entrySet()) {
if (e.getValue() != null) {
result.put(""+e.getKey(), asList((String[]) e.getValue()));
}
}
}
return objectMapper.convertValue(result.toSingleValueMap(), clazz);
}
@Override
protected void writeInternal(Object t, HttpOutputMessage outputMessage) throws IOException,
HttpMessageNotWritableException {
throw new UnsupportedOperationException("writeInternal is not yet supported");
}
}
package com.snaphop.spring;
import static java.util.Arrays.asList;
import java.lang.reflect.Constructor;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
import javax.annotation.concurrent.Immutable;
import javax.servlet.ServletRequest;
import org.codehaus.jackson.annotate.JsonCreator;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.annotation.ModelAttributeMethodProcessor;
import org.springframework.web.servlet.mvc.method.annotation.ServletRequestDataBinderFactory;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
public class ImmutableObjectModelAttributeMethodProcessor extends ModelAttributeMethodProcessor {
private final ObjectMapper objectMapper = new ObjectMapper();
private LoadingCache<Class<?>, Boolean> supportsClassCache = CacheBuilder.newBuilder()
.weakKeys()
.build(new CacheLoader<Class<?>, Boolean>() {
@Override
public Boolean load(Class<?> key) throws Exception {
try {
//Only allow immutable objects that support Jackson
if( key.getAnnotation(Immutable.class) != null && objectMapper.canSerialize(key))
return true;
Constructor<?>[] cons = key.getDeclaredConstructors();
for (Constructor<?> c : cons) {
JsonCreator jc = c.getAnnotation(JsonCreator.class);
if (jc != null && objectMapper.canSerialize(key))
return true;
}
} catch (Exception e) {
//Reflection might have failed maybe because of security manager
}
return false;
}
});
public ImmutableObjectModelAttributeMethodProcessor() {
super(true);
}
@Override
protected Object createAttribute(String attributeName, MethodParameter parameter,
WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {
LinkedMultiValueMap<String, String> result = new LinkedMultiValueMap<String, String>(request.getParameterMap().size());
for (Entry<String,String[]> e : request.getParameterMap().entrySet()) {
if (e.getValue() != null) {
result.put(""+e.getKey(), asList(e.getValue()));
}
}
return objectMapper.convertValue(result.toSingleValueMap(), parameter.getParameterType());
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
try {
return supportsClassCache.get(parameter.getParameterType());
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
/**
* {@inheritDoc}
* <p>Downcast {@link WebDataBinder} to {@link ServletRequestDataBinder} before binding.
* @see ServletRequestDataBinderFactory
*/
@Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
servletBinder.bind(servletRequest);
}
}
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="com.snaphop.spring.ImmutableObjectMessageConverter" />
</mvc:message-converters>
<mvc:argument-resolvers>
<bean class="com.snaphop.spring.ImmutableObjectModelAttributeMethodProcessor" />
</mvc:argument-resolvers>
</mvc:annotation-driven>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment