#18, DATAREST-20, DATAREST-21, DATAREST-22 Bugfixes, re-vamp output method, JSONPE support
There were some issues with how results were being displayed in searches versus entity lists. This code changes the way all results are displayed by offereing several options for output. The default is to inline the entity in the response and page the results (defaults to 20). If the UA sends an `Accept` header of `application/x-spring-data-compact+json` or `text/uri-list`, however, SD REST will output a compact list of only links and will not inline the entities. The method of output was completely rewritten to use HttpMessageConverters exclusively. Views and the subsequent ContentNegotiatingViewResolver machinery have been elimintated. This should make it easier to embed inside an existing Spring MVC application. This is also easily extendable so the user can plug in their own set of HttpMessageConverters for any output format they like (JAXB, Atom/XML, etc...). Also added was JSONPE support. By setting the `jsonpParamName` property on the `RepositoryRestConfiguration` customization class, the user can change the default JSONP param name of `callback`. There's also a `jsonpOnErrParamName` property on that configuration (defaults to `null`) that will allow you to capture errors using JSONP. Normally this would not be possible using script element injection, but the REST controller changes the status code to 200 and pass your javascript function the actual response code and wraps the error as the second parameter.
This commit is contained in:
@@ -10,7 +10,7 @@ cglibVersion = 2.2
|
||||
groovyVersion = 1.8.6
|
||||
|
||||
# Supporting libraries
|
||||
sdCommonsVersion = 1.3.1.RELEASE
|
||||
sdCommonsVersion = 1.3.2.RELEASE
|
||||
sdJpaVersion = 1.1.0.RELEASE
|
||||
jacksonVersion = 1.9.7
|
||||
hibernateVersion = 4.1.4.Final
|
||||
|
||||
@@ -16,60 +16,60 @@ import org.springframework.util.ClassUtils;
|
||||
*
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
*/
|
||||
public class FluentBeanSerializer extends SerializerBase {
|
||||
public class FluentBeanSerializer extends SerializerBase<Object> {
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
public FluentBeanSerializer(final Class<?> t) {
|
||||
super(t);
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public FluentBeanSerializer( final Class t ) {
|
||||
super( t );
|
||||
|
||||
if (!FluentBeanUtils.isFluentBean(t)) {
|
||||
throw new IllegalArgumentException("Class of type " + t + " is not a FluentBean");
|
||||
if ( !FluentBeanUtils.isFluentBean( t ) ) {
|
||||
throw new IllegalArgumentException( "Class of type " + t + " is not a FluentBean" );
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override
|
||||
public void serialize(final Object value,
|
||||
final JsonGenerator jgen,
|
||||
final SerializerProvider provider)
|
||||
public void serialize( final Object value,
|
||||
final JsonGenerator jgen,
|
||||
final SerializerProvider provider )
|
||||
throws IOException,
|
||||
JsonGenerationException {
|
||||
if (null == value) {
|
||||
provider.defaultSerializeNull(jgen);
|
||||
if ( null == value ) {
|
||||
provider.defaultSerializeNull( jgen );
|
||||
} else {
|
||||
Class<?> type = value.getClass();
|
||||
if (ClassUtils.isAssignable(type, Collection.class)) {
|
||||
if ( ClassUtils.isAssignable( type, Collection.class ) ) {
|
||||
jgen.writeStartArray();
|
||||
for (Object o : (Collection) value) {
|
||||
write(o, jgen, provider);
|
||||
for ( Object o : (Collection) value ) {
|
||||
write( o, jgen, provider );
|
||||
}
|
||||
jgen.writeEndArray();
|
||||
} else if (ClassUtils.isAssignable(type, Map.class)) {
|
||||
} else if ( ClassUtils.isAssignable( type, Map.class ) ) {
|
||||
jgen.writeStartObject();
|
||||
for (Map.Entry<String, Object> entry : ((Map<String, Object>) value).entrySet()) {
|
||||
jgen.writeFieldName(entry.getKey());
|
||||
write(entry.getValue(), jgen, provider);
|
||||
for ( Map.Entry<String, Object> entry : ((Map<String, Object>) value).entrySet() ) {
|
||||
jgen.writeFieldName( entry.getKey() );
|
||||
write( entry.getValue(), jgen, provider );
|
||||
}
|
||||
jgen.writeEndObject();
|
||||
} else {
|
||||
write(value, jgen, provider);
|
||||
write( value, jgen, provider );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void write(final Object value,
|
||||
final JsonGenerator jgen,
|
||||
final SerializerProvider provider) throws IOException {
|
||||
private void write( final Object value,
|
||||
final JsonGenerator jgen,
|
||||
final SerializerProvider provider ) throws IOException {
|
||||
Class<?> type = value.getClass();
|
||||
if (ClassUtils.isAssignable(type, _handledType)) {
|
||||
if ( ClassUtils.isAssignable( type, _handledType ) ) {
|
||||
jgen.writeStartObject();
|
||||
for (String fname : FluentBeanUtils.metadata(type).fieldNames()) {
|
||||
jgen.writeFieldName(fname);
|
||||
write(FluentBeanUtils.get(fname, value), jgen, provider);
|
||||
for ( String fname : FluentBeanUtils.metadata( type ).fieldNames() ) {
|
||||
jgen.writeFieldName( fname );
|
||||
write( FluentBeanUtils.get( fname, value ), jgen, provider );
|
||||
}
|
||||
jgen.writeEndObject();
|
||||
} else {
|
||||
jgen.writeObject(value);
|
||||
jgen.writeObject( value );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.codehaus.jackson.JsonEncoding;
|
||||
import org.codehaus.jackson.JsonGenerator;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.codehaus.jackson.map.ser.CustomSerializerFactory;
|
||||
import org.springframework.data.rest.core.SimpleLink;
|
||||
import org.springframework.data.rest.core.util.FluentBeanSerializer;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public abstract class JacksonUtil {
|
||||
|
||||
public static final Charset DEFAULT_CHARSET = Charset.forName( "UTF-8" );
|
||||
public static final MediaType COMPACT_JSON = new MediaType( "application",
|
||||
"x-spring-data-compact+json",
|
||||
DEFAULT_CHARSET );
|
||||
public static final MediaType VERBOSE_JSON = new MediaType( "application",
|
||||
"x-spring-data-verbose+json",
|
||||
DEFAULT_CHARSET );
|
||||
public static final MediaType APPLICATION_JAVASCRIPT = new MediaType( "application",
|
||||
"javascript",
|
||||
DEFAULT_CHARSET );
|
||||
|
||||
private JacksonUtil() {
|
||||
}
|
||||
|
||||
public static MappingJacksonHttpMessageConverter createJacksonHttpMessageConverter( final ObjectMapper objectMapper ) {
|
||||
CustomSerializerFactory customSerializerFactory = new CustomSerializerFactory();
|
||||
customSerializerFactory.addSpecificMapping( SimpleLink.class, new FluentBeanSerializer( SimpleLink.class ) );
|
||||
objectMapper.setSerializerFactory( customSerializerFactory );
|
||||
MappingJacksonHttpMessageConverter jsonConverter = new MappingJacksonHttpMessageConverter() {
|
||||
{
|
||||
setSupportedMediaTypes( Arrays.asList( MediaType.APPLICATION_JSON, COMPACT_JSON, VERBOSE_JSON ) );
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeInternal( Object object, HttpOutputMessage outputMessage )
|
||||
throws IOException,
|
||||
HttpMessageNotWritableException {
|
||||
JsonEncoding encoding = getJsonEncoding( outputMessage.getHeaders().getContentType() );
|
||||
// Believe it or not, this is the only way to get pretty-printing from Jackson in this configuration
|
||||
JsonGenerator jsonGenerator = objectMapper
|
||||
.getJsonFactory()
|
||||
.createJsonGenerator( outputMessage.getBody(), encoding )
|
||||
.useDefaultPrettyPrinter();
|
||||
try {
|
||||
objectMapper.writeValue( jsonGenerator, object );
|
||||
} catch ( IOException ex ) {
|
||||
throw new HttpMessageNotWritableException( "Could not write JSON: " + ex.getMessage(), ex );
|
||||
}
|
||||
}
|
||||
};
|
||||
jsonConverter.setObjectMapper( objectMapper );
|
||||
|
||||
return jsonConverter;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Map;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.codehaus.jackson.map.ser.CustomSerializerFactory;
|
||||
import org.springframework.data.rest.core.SimpleLink;
|
||||
import org.springframework.data.rest.core.util.FluentBeanSerializer;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.servlet.view.AbstractView;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
*/
|
||||
@SuppressWarnings({"unchecked"})
|
||||
public class JsonView extends AbstractView {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
{
|
||||
CustomSerializerFactory customSerializerFactory = new CustomSerializerFactory();
|
||||
customSerializerFactory.addSpecificMapping(SimpleLink.class, new FluentBeanSerializer(SimpleLink.class));
|
||||
mapper.setSerializerFactory(customSerializerFactory);
|
||||
}
|
||||
|
||||
public JsonView(String mediaType) {
|
||||
setContentType(mediaType);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderMergedOutputModel(Map<String, Object> model,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) throws Exception {
|
||||
HttpStatus status = status(model);
|
||||
response.setStatus(status.value());
|
||||
|
||||
String contentType = getContentType();
|
||||
HttpHeaders headers = headers(model);
|
||||
if (null != headers) {
|
||||
for (Map.Entry<String, String> entry : headers.toSingleValueMap().entrySet()) {
|
||||
response.setHeader(entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (null != headers.getContentType()) {
|
||||
contentType = headers.getContentType().toString();
|
||||
}
|
||||
}
|
||||
response.setContentType(contentType);
|
||||
|
||||
Object resource = model.get("resource");
|
||||
if (null != resource) {
|
||||
if (resource instanceof Throwable) {
|
||||
resource = ((Throwable) resource).getMessage();
|
||||
}
|
||||
ByteArrayOutputStream bout = new ByteArrayOutputStream();
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(bout, resource);
|
||||
|
||||
response.getOutputStream().write(bout.toByteArray());
|
||||
} else {
|
||||
response.setContentLength(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private HttpStatus status(Map<String, Object> model) {
|
||||
Object o = model.get("status");
|
||||
if (null != o && o instanceof HttpStatus) {
|
||||
return (HttpStatus) o;
|
||||
}
|
||||
throw new IllegalArgumentException("No status is set in the model.");
|
||||
}
|
||||
|
||||
private HttpHeaders headers(Map<String, Object> model) {
|
||||
Object o = model.get("headers");
|
||||
if (null != o && o instanceof HttpHeaders) {
|
||||
return (HttpHeaders) o;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,8 @@ public class RepositoryRestConfiguration {
|
||||
private String pageParamName = "page";
|
||||
private String limitParamName = "limit";
|
||||
private String sortParamName = "sort";
|
||||
private String jsonpParamName = "callback";
|
||||
private String jsonpOnErrParamName = null;
|
||||
private List<HttpMessageConverter<?>> customConverters = Collections.emptyList();
|
||||
|
||||
public int getDefaultPageSize() {
|
||||
@@ -63,4 +65,22 @@ public class RepositoryRestConfiguration {
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getJsonpParamName() {
|
||||
return jsonpParamName;
|
||||
}
|
||||
|
||||
public RepositoryRestConfiguration setJsonpParamName( String jsonpParamName ) {
|
||||
this.jsonpParamName = jsonpParamName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getJsonpOnErrParamName() {
|
||||
return jsonpOnErrParamName;
|
||||
}
|
||||
|
||||
public RepositoryRestConfiguration setJsonpOnErrParamName( String jsonpOnErrParamName ) {
|
||||
this.jsonpOnErrParamName = jsonpOnErrParamName;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,27 +13,27 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
|
||||
*/
|
||||
public class RepositoryRestHandlerAdapter extends RequestMappingHandlerAdapter {
|
||||
|
||||
public RepositoryRestHandlerAdapter() {
|
||||
setCustomArgumentResolvers(Arrays.asList(
|
||||
public RepositoryRestHandlerAdapter( RepositoryRestConfiguration config ) {
|
||||
setCustomArgumentResolvers( Arrays.asList(
|
||||
new ServerHttpRequestMethodArgumentResolver(),
|
||||
new PagingAndSortingMethodArgumentResolver()
|
||||
));
|
||||
new PagingAndSortingMethodArgumentResolver( config )
|
||||
) );
|
||||
|
||||
// Add JSON converter for special Spring Data media type
|
||||
MappingJacksonHttpMessageConverter json = new MappingJacksonHttpMessageConverter();
|
||||
json.setSupportedMediaTypes(
|
||||
Arrays.asList(MediaType.APPLICATION_JSON, MediaType.valueOf("application/x-spring-data+json"))
|
||||
Arrays.asList( MediaType.APPLICATION_JSON, MediaType.valueOf( "application/x-spring-data+json" ) )
|
||||
);
|
||||
getMessageConverters().add(json);
|
||||
getMessageConverters().add( json );
|
||||
}
|
||||
|
||||
@Override public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE;
|
||||
}
|
||||
|
||||
@Override protected boolean supportsInternal(HandlerMethod handlerMethod) {
|
||||
return super.supportsInternal(handlerMethod)
|
||||
&& RepositoryRestController.class.isAssignableFrom(handlerMethod.getBeanType());
|
||||
@Override protected boolean supportsInternal( HandlerMethod handlerMethod ) {
|
||||
return super.supportsInternal( handlerMethod )
|
||||
&& RepositoryRestController.class.isAssignableFrom( handlerMethod.getBeanType() );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportResource;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener;
|
||||
import org.springframework.data.rest.repository.jpa.JpaRepositoryExporter;
|
||||
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
|
||||
import org.springframework.web.servlet.ViewResolver;
|
||||
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
@@ -28,60 +21,38 @@ public class RepositoryRestMvcConfiguration {
|
||||
@Autowired(required = false)
|
||||
ValidatingRepositoryEventListener validatingRepositoryEventListener;
|
||||
|
||||
@Bean PersistenceAnnotationBeanPostProcessor persistenceAnnotationBeanPostProcessor() {
|
||||
@Autowired(required = false)
|
||||
RepositoryRestConfiguration repositoryRestConfig = RepositoryRestConfiguration.DEFAULT;
|
||||
|
||||
@Bean public PersistenceAnnotationBeanPostProcessor persistenceAnnotationBeanPostProcessor() {
|
||||
return new PersistenceAnnotationBeanPostProcessor();
|
||||
}
|
||||
|
||||
@Bean JpaRepositoryExporter jpaRepositoryExporter() {
|
||||
if (null == customJpaRepositoryExporter) {
|
||||
@Bean public JpaRepositoryExporter jpaRepositoryExporter() {
|
||||
if ( null == customJpaRepositoryExporter ) {
|
||||
return new JpaRepositoryExporter();
|
||||
} else {
|
||||
return customJpaRepositoryExporter;
|
||||
}
|
||||
}
|
||||
|
||||
@Bean ValidatingRepositoryEventListener validatingRepositoryEventListener() {
|
||||
if (null == validatingRepositoryEventListener) {
|
||||
@Bean public ValidatingRepositoryEventListener validatingRepositoryEventListener() {
|
||||
if ( null == validatingRepositoryEventListener ) {
|
||||
return new ValidatingRepositoryEventListener();
|
||||
} else {
|
||||
return validatingRepositoryEventListener;
|
||||
}
|
||||
}
|
||||
|
||||
@Bean JsonView jsonView() {
|
||||
return new JsonView("application/json");
|
||||
}
|
||||
|
||||
@Bean UriListView urilistView() {
|
||||
return new UriListView();
|
||||
}
|
||||
|
||||
@Bean ContentNegotiatingViewResolver contentNegotiatingViewResolver() {
|
||||
ContentNegotiatingViewResolver viewResolver = new ContentNegotiatingViewResolver();
|
||||
viewResolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
|
||||
|
||||
Map<String, String> mediaTypes = new HashMap<String, String>() {{
|
||||
put("json", "application/json");
|
||||
put("urilist", "text/uri-list");
|
||||
}};
|
||||
viewResolver.setMediaTypes(mediaTypes);
|
||||
|
||||
RepositoryRestViewResolver jsonvr = new RepositoryRestViewResolver(jsonView());
|
||||
RepositoryRestViewResolver urilistvr = new RepositoryRestViewResolver(urilistView());
|
||||
viewResolver.setViewResolvers(Arrays.<ViewResolver>asList(jsonvr, urilistvr));
|
||||
|
||||
return viewResolver;
|
||||
}
|
||||
|
||||
@Bean RepositoryRestController repositoryRestController() throws Exception {
|
||||
@Bean public RepositoryRestController repositoryRestController() throws Exception {
|
||||
return new RepositoryRestController();
|
||||
}
|
||||
|
||||
@Bean RepositoryRestHandlerAdapter repositoryExporterHandlerAdapter() {
|
||||
return new RepositoryRestHandlerAdapter();
|
||||
@Bean public RepositoryRestHandlerAdapter repositoryExporterHandlerAdapter() {
|
||||
return new RepositoryRestHandlerAdapter( repositoryRestConfig );
|
||||
}
|
||||
|
||||
@Bean RepositoryRestHandlerMapping repositoryExporterHandlerMapping() {
|
||||
@Bean public RepositoryRestHandlerMapping repositoryExporterHandlerMapping() {
|
||||
return new RepositoryRestHandlerMapping();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.servlet.View;
|
||||
import org.springframework.web.servlet.ViewResolver;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jon@jbrisbin.com>
|
||||
*/
|
||||
public class RepositoryRestViewResolver implements ViewResolver {
|
||||
|
||||
private View view;
|
||||
private Map<String, View> customViewMappings = Collections.emptyMap();
|
||||
|
||||
public RepositoryRestViewResolver(View view) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
public RepositoryRestViewResolver setCustomViewMappings(Map<String, View> customViewMappings) {
|
||||
this.customViewMappings = customViewMappings;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override public View resolveViewName(String viewName, Locale locale) throws Exception {
|
||||
if (customViewMappings.containsKey(viewName)) {
|
||||
return customViewMappings.get(viewName);
|
||||
} else if (viewName.startsWith("org.springframework.data.rest")) {
|
||||
return view;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package org.springframework.data.rest.webmvc;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
@@ -16,16 +15,16 @@ import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
*/
|
||||
public class ServerHttpRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Override public boolean supportsParameter(MethodParameter parameter) {
|
||||
return ClassUtils.isAssignable(parameter.getParameterType(), ServerHttpRequest.class);
|
||||
@Override public boolean supportsParameter( MethodParameter parameter ) {
|
||||
return ClassUtils.isAssignable( parameter.getParameterType(), ServletServerHttpRequest.class );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter,
|
||||
ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest,
|
||||
WebDataBinderFactory binderFactory) throws Exception {
|
||||
return new ServletServerHttpRequest((HttpServletRequest) webRequest.getNativeRequest());
|
||||
public Object resolveArgument( MethodParameter parameter,
|
||||
ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest,
|
||||
WebDataBinderFactory binderFactory ) throws Exception {
|
||||
return new ServletServerHttpRequest( (HttpServletRequest) webRequest.getNativeRequest() );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.data.rest.core.Link;
|
||||
import org.springframework.data.rest.core.SimpleLink;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin
|
||||
*/
|
||||
public class UriListHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
|
||||
|
||||
public static final Charset DEFAULT_CHARSET = Charset.forName( "ISO-8859-1" );
|
||||
|
||||
public UriListHttpMessageConverter() {
|
||||
super( new MediaType( "text", "uri-list", DEFAULT_CHARSET ) );
|
||||
}
|
||||
|
||||
@Override protected boolean supports( Class<?> clazz ) {
|
||||
return (List.class.isAssignableFrom( clazz )
|
||||
|| Map.class.isAssignableFrom( clazz )
|
||||
|| Links.class.isAssignableFrom( clazz ));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override
|
||||
protected Object readInternal( Class<?> clazz,
|
||||
HttpInputMessage inputMessage )
|
||||
throws IOException,
|
||||
HttpMessageNotReadableException {
|
||||
|
||||
String rel = inputMessage.getHeaders().getFirst( "x-spring-data-urilist-rel" );
|
||||
if ( null == rel ) {
|
||||
rel = inputMessage.getHeaders().getLocation().getPath().substring( 1 ).replaceAll( "/", "." );
|
||||
}
|
||||
BufferedReader reader = new BufferedReader( new InputStreamReader( inputMessage.getBody() ) );
|
||||
String line = null;
|
||||
Object links = null;
|
||||
try {
|
||||
links = clazz.newInstance();
|
||||
} catch ( InstantiationException e ) {
|
||||
throw new HttpMessageNotReadableException( e.getMessage(), e );
|
||||
} catch ( IllegalAccessException e ) {
|
||||
throw new HttpMessageNotReadableException( e.getMessage(), e );
|
||||
}
|
||||
while ( null != (line = reader.readLine()) ) {
|
||||
if ( links instanceof Links ) {
|
||||
((Links) links).add( new SimpleLink( rel, URI.create( line.trim() ) ) );
|
||||
} else if ( links instanceof List ) {
|
||||
((List) links).add( new SimpleLink( rel, URI.create( line.trim() ) ) );
|
||||
} else if ( links instanceof Map ) {
|
||||
List l = (List) ((Map) links).get( "_links" );
|
||||
if ( null == l ) {
|
||||
l = new ArrayList();
|
||||
((Map) links).put( "_links", l );
|
||||
}
|
||||
l.add( new SimpleLink( rel, URI.create( line.trim() ) ) );
|
||||
}
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeInternal( Object links, HttpOutputMessage outputMessage )
|
||||
throws IOException,
|
||||
HttpMessageNotWritableException {
|
||||
OutputStream body = outputMessage.getBody();
|
||||
if ( links instanceof Links ) {
|
||||
for ( SimpleLink link : ((Links) links).getLinks() ) {
|
||||
body.write( link.href().toASCIIString().getBytes() );
|
||||
body.write( '\n' );
|
||||
}
|
||||
} else if ( links instanceof List ) {
|
||||
for ( Object o : (List) links ) {
|
||||
if ( o instanceof Link ) {
|
||||
body.write( ((Link) o).href().toASCIIString().getBytes() );
|
||||
} else {
|
||||
body.write( o.toString().getBytes() );
|
||||
}
|
||||
body.write( '\n' );
|
||||
}
|
||||
} else if ( links instanceof Map ) {
|
||||
writeInternal( ((Map) links).get( "_links" ), outputMessage );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.data.rest.core.Link;
|
||||
import org.springframework.data.rest.core.SimpleLink;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.servlet.view.AbstractView;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
*/
|
||||
public class UriListView extends AbstractView {
|
||||
|
||||
public UriListView() {
|
||||
setContentType("text/uri-list");
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override
|
||||
protected void renderMergedOutputModel(Map<String, Object> model,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) throws Exception {
|
||||
|
||||
Object resource = model.get("resource");
|
||||
response.setContentType(getContentType());
|
||||
|
||||
HttpStatus status = (HttpStatus) model.get("status");
|
||||
HttpHeaders headers = (HttpHeaders) model.get("headers");
|
||||
List<SimpleLink> links = null;
|
||||
if (resource instanceof List) {
|
||||
links = (List<SimpleLink>) resource;
|
||||
} else if (resource instanceof Map) {
|
||||
Map m = (Map) resource;
|
||||
Object o = m.get("_links");
|
||||
if (null != o && o instanceof List) {
|
||||
links = (List<SimpleLink>) o;
|
||||
} else {
|
||||
response.setStatus(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
|
||||
return;
|
||||
}
|
||||
} else if (resource instanceof Links) {
|
||||
links = ((Links) resource).getLinks();
|
||||
}
|
||||
|
||||
if (null != status) {
|
||||
response.setStatus(status.value());
|
||||
}
|
||||
|
||||
if (null != headers) {
|
||||
for (Map.Entry<String, String> entry : headers.toSingleValueMap().entrySet()) {
|
||||
response.setHeader(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
PrintWriter out = response.getWriter();
|
||||
if (null != links) {
|
||||
for (Link l : links) {
|
||||
out.println(l.href().toString());
|
||||
}
|
||||
}
|
||||
out.flush();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import org.springframework.data.rest.core.SimpleLink
|
||||
import org.springframework.data.rest.core.util.FluentBeanSerializer
|
||||
import org.springframework.data.rest.test.webmvc.Address
|
||||
import org.springframework.data.rest.webmvc.PagingAndSorting
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestConfiguration
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestController
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration
|
||||
import org.springframework.http.HttpStatus
|
||||
@@ -67,7 +68,7 @@ class RepositoryRestControllerSpec extends Specification {
|
||||
|
||||
emf = webAppCtx.getBean(EntityManagerFactory)
|
||||
controller = webAppCtx.getBean(RepositoryRestController)
|
||||
pageSort = new PagingAndSorting("page", "limit", "sort", new PageRequest(0, 1000))
|
||||
pageSort = new PagingAndSorting(RepositoryRestConfiguration.DEFAULT, new PageRequest(0, 1000))
|
||||
uriBuilder = UriComponentsBuilder.fromUriString("http://localhost:8080/data")
|
||||
|
||||
def customSerializerFactory = new CustomSerializerFactory()
|
||||
@@ -87,75 +88,71 @@ class RepositoryRestControllerSpec extends Specification {
|
||||
def model = new ExtendedModelMap()
|
||||
|
||||
when: "listing available repositories"
|
||||
def mv = controller.listRepositories(uriBuilder)
|
||||
def reposLinks = mv.model.resource?.links
|
||||
def req = createRequest("POST", "people")
|
||||
def response = controller.listRepositories(new ServletServerHttpRequest(req), uriBuilder)
|
||||
def reposLinks = mapper.readValue(response.body, Map)?._links
|
||||
|
||||
then:
|
||||
mv.model.status == HttpStatus.OK
|
||||
response.statusCode == HttpStatus.OK
|
||||
reposLinks?.size() == 4
|
||||
|
||||
when: "adding an entity"
|
||||
model.clear()
|
||||
def req = createRequest("POST", "people")
|
||||
def data = mapper.writeValueAsBytes([name: "John Doe"])
|
||||
req.content = data
|
||||
mv = controller.create(new ServletServerHttpRequest(req), req, uriBuilder, "people")
|
||||
response = controller.create(new ServletServerHttpRequest(req), req, uriBuilder, "people")
|
||||
|
||||
then:
|
||||
mv.model.status == HttpStatus.CREATED
|
||||
response.statusCode == HttpStatus.CREATED
|
||||
|
||||
when: "getting a specific entity"
|
||||
model.clear()
|
||||
req = createRequest("GET", "people/1")
|
||||
mv = controller.entity(new ServletServerHttpRequest(req), uriBuilder, "people", "1")
|
||||
response = controller.entity(new ServletServerHttpRequest(req), uriBuilder, "people", "1")
|
||||
def entityData = mapper.readValue(response.body, Map)
|
||||
|
||||
then:
|
||||
mv.model.resource?.name == "John Doe"
|
||||
entityData?.name == "John Doe"
|
||||
|
||||
when: "updating an entity"
|
||||
mv.model.clear()
|
||||
req = createRequest("PUT", "people/1")
|
||||
data = mapper.writeValueAsBytes([name: "Johnnie Doe", version: 0])
|
||||
req.content = data
|
||||
mv = controller.createOrUpdate(new ServletServerHttpRequest(req), uriBuilder, "people", "1")
|
||||
response = controller.createOrUpdate(new ServletServerHttpRequest(req), uriBuilder, "people", "1")
|
||||
|
||||
then:
|
||||
mv.model.status == HttpStatus.NO_CONTENT
|
||||
response.statusCode == HttpStatus.NO_CONTENT
|
||||
|
||||
when: "listing available entities"
|
||||
mv.model.clear()
|
||||
mv = controller.listEntities(pageSort, uriBuilder, "people")
|
||||
def peopleLinks = mv.model.resource?.links
|
||||
response = controller.listEntities(new ServletServerHttpRequest(req), pageSort, uriBuilder, "people")
|
||||
def selfLink = mapper.readValue(response.body, Map)?.results[0]?._links[2]
|
||||
|
||||
then:
|
||||
mv.model.status == HttpStatus.OK
|
||||
peopleLinks[0].href().toString() == "http://localhost:8080/data/people/1"
|
||||
response.statusCode == HttpStatus.OK
|
||||
selfLink.href == "http://localhost:8080/data/people/1"
|
||||
|
||||
when: "creating a child entity"
|
||||
mv.model.clear()
|
||||
req = createRequest("POST", "address")
|
||||
data = mapper.writeValueAsBytes(new Address(["1 W. 1st St."] as String[], "Univille", "ST", "12345"))
|
||||
req.content = data
|
||||
mv = controller.create(new ServletServerHttpRequest(req), req, uriBuilder, "address")
|
||||
response = controller.create(new ServletServerHttpRequest(req), req, uriBuilder, "address")
|
||||
|
||||
then:
|
||||
mv.model.status == HttpStatus.CREATED
|
||||
response.statusCode == HttpStatus.CREATED
|
||||
|
||||
when: "linking child to parent entity"
|
||||
mv.model.clear()
|
||||
req = createRequest("POST", "people/1/addresses")
|
||||
req.contentType = "text/uri-list"
|
||||
data = "http://localhost:8080/data/address/1".bytes
|
||||
req.content = data
|
||||
mv = controller.updatePropertyOfEntity(new ServletServerHttpRequest(req), uriBuilder, "people", "1", "addresses")
|
||||
response = controller.updatePropertyOfEntity(new ServletServerHttpRequest(req), uriBuilder, "people", "1", "addresses")
|
||||
|
||||
then:
|
||||
mv.model.status == HttpStatus.CREATED
|
||||
response.statusCode == HttpStatus.CREATED
|
||||
|
||||
when: "getting property of an entity"
|
||||
mv.model.clear()
|
||||
mv = controller.propertyOfEntity(uriBuilder, "people", "1", "addresses")
|
||||
def addrLinks = mv.model.resource?.links
|
||||
response = controller.propertyOfEntity(new ServletServerHttpRequest(req), uriBuilder, "people", "1", "addresses")
|
||||
def addrLinks = mapper.readValue((byte[]) response.body, Map)?._links
|
||||
|
||||
then:
|
||||
null != addrLinks
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
|
||||
xmlns:p="http://www.springframework.org/schema/p"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
|
||||
@@ -9,6 +10,10 @@
|
||||
|
||||
<jpa:repositories base-package="org.springframework.data.rest.test.webmvc"/>
|
||||
|
||||
<bean id="config" class="org.springframework.data.rest.webmvc.RepositoryRestConfiguration"
|
||||
p:jsonpParamName="callback"
|
||||
p:jsonpOnErrParamName="errback"/>
|
||||
|
||||
<!--
|
||||
If you need to add Converters to the REST exporter to handle the property types you're using
|
||||
in your entities, then just configure a ConversionServiceFactoryBean here, add the Converteres
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="org.springframework.data.rest.auditlog" level="INFO"/>
|
||||
<logger name="org.springframework.data.rest" level="DEBUG"/>
|
||||
<logger name="org.springframework.data" level="INFO"/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user