Last active
December 27, 2022 00:37
-
-
Save agentgt/4458079 to your computer and use it in GitHub Desktop.
Spring Immutable Object Web Data Binding
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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