Added ApplicationEvent handling and annotation-based extension mechanism.
This commit is contained in:
@@ -23,6 +23,8 @@ import javax.persistence.metamodel.SingularAttribute;
|
||||
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
@@ -34,6 +36,13 @@ import org.springframework.data.rest.core.SimpleLink;
|
||||
import org.springframework.data.rest.core.util.UriUtils;
|
||||
import org.springframework.data.rest.repository.JpaEntityMetadata;
|
||||
import org.springframework.data.rest.repository.JpaRepositoryMetadata;
|
||||
import org.springframework.data.rest.repository.RepositoryConstraintViolationException;
|
||||
import org.springframework.data.rest.repository.context.AfterChildSaveEvent;
|
||||
import org.springframework.data.rest.repository.context.AfterDeleteEvent;
|
||||
import org.springframework.data.rest.repository.context.AfterSaveEvent;
|
||||
import org.springframework.data.rest.repository.context.BeforeChildSaveEvent;
|
||||
import org.springframework.data.rest.repository.context.BeforeDeleteEvent;
|
||||
import org.springframework.data.rest.repository.context.BeforeSaveEvent;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -44,21 +53,26 @@ import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jon@jbrisbin.com>
|
||||
*/
|
||||
@Controller
|
||||
public class RepositoryRestController implements InitializingBean {
|
||||
public class RepositoryRestController
|
||||
implements ApplicationEventPublisherAware,
|
||||
InitializingBean {
|
||||
|
||||
public static final String STATUS = "status";
|
||||
public static final String HEADERS = "headers";
|
||||
@@ -67,14 +81,21 @@ public class RepositoryRestController implements InitializingBean {
|
||||
public static final String SELF = "self";
|
||||
public static final String LINKS = "_links";
|
||||
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
private MediaType uriListMediaType = MediaType.parseMediaType("text/uri-list");
|
||||
private MediaType jsonMediaType = MediaType.parseMediaType("application/x-spring-data+json");
|
||||
private JpaRepositoryMetadata repositoryMetadata;
|
||||
private Map<CrudRepository, TypeMetaCacheEntry> typeMetaCache = new ConcurrentHashMap<CrudRepository, TypeMetaCacheEntry>();
|
||||
private ConversionService conversionService = new DefaultConversionService();
|
||||
private List<HttpMessageConverter<?>> httpMessageConverters;
|
||||
private ContentNegotiatingViewResolver viewResolver;
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
|
||||
this.eventPublisher = eventPublisher;
|
||||
}
|
||||
|
||||
public JpaRepositoryMetadata getRepositoryMetadata() {
|
||||
return repositoryMetadata;
|
||||
}
|
||||
@@ -126,6 +147,23 @@ public class RepositoryRestController implements InitializingBean {
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentNegotiatingViewResolver getViewResolver() {
|
||||
return viewResolver;
|
||||
}
|
||||
|
||||
public void setViewResolver(ContentNegotiatingViewResolver viewResolver) {
|
||||
this.viewResolver = viewResolver;
|
||||
}
|
||||
|
||||
public ContentNegotiatingViewResolver viewResolver() {
|
||||
return viewResolver;
|
||||
}
|
||||
|
||||
public RepositoryRestController viewResolver(ContentNegotiatingViewResolver viewResolver) {
|
||||
this.viewResolver = viewResolver;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MediaType getUriListMediaType() {
|
||||
return uriListMediaType;
|
||||
}
|
||||
@@ -263,7 +301,13 @@ public class RepositoryRestController implements InitializingBean {
|
||||
if (null == incoming) {
|
||||
model.addAttribute(STATUS, HttpStatus.NOT_ACCEPTABLE);
|
||||
} else {
|
||||
if (null != eventPublisher) {
|
||||
eventPublisher.publishEvent(new BeforeSaveEvent(incoming));
|
||||
}
|
||||
Object savedEntity = repo.save(incoming);
|
||||
if (null != eventPublisher) {
|
||||
eventPublisher.publishEvent(new AfterSaveEvent(savedEntity));
|
||||
}
|
||||
String sId = typeMeta.entityInfo.getId(savedEntity).toString();
|
||||
|
||||
URI selfUri = buildUri(baseUri, repository, sId);
|
||||
@@ -384,14 +428,26 @@ public class RepositoryRestController implements InitializingBean {
|
||||
} else {
|
||||
typeMeta.entityMetadata.id(serId, incoming);
|
||||
if (request.getMethod() == HttpMethod.POST) {
|
||||
repo.save(incoming);
|
||||
if (null != eventPublisher) {
|
||||
eventPublisher.publishEvent(new BeforeSaveEvent(incoming));
|
||||
}
|
||||
Object savedEntity = repo.save(incoming);
|
||||
if (null != eventPublisher) {
|
||||
eventPublisher.publishEvent(new AfterSaveEvent(savedEntity));
|
||||
}
|
||||
URI selfUri = buildUri(baseUri, repository, id);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set(LOCATION, selfUri.toString());
|
||||
model.addAttribute(HEADERS, headers);
|
||||
model.addAttribute(STATUS, HttpStatus.CREATED);
|
||||
} else {
|
||||
repo.save(incoming);
|
||||
if (null != eventPublisher) {
|
||||
eventPublisher.publishEvent(new BeforeSaveEvent(incoming));
|
||||
}
|
||||
Object savedEntity = repo.save(incoming);
|
||||
if (null != eventPublisher) {
|
||||
eventPublisher.publishEvent(new AfterSaveEvent(savedEntity));
|
||||
}
|
||||
model.addAttribute(STATUS, HttpStatus.NO_CONTENT);
|
||||
}
|
||||
}
|
||||
@@ -415,7 +471,13 @@ public class RepositoryRestController implements InitializingBean {
|
||||
TypeMetaCacheEntry typeMeta = typeMetaEntry(repo);
|
||||
Serializable serId = stringToSerializable(id, typeMeta.idType);
|
||||
|
||||
if (null != eventPublisher) {
|
||||
eventPublisher.publishEvent(new BeforeDeleteEvent(serId));
|
||||
}
|
||||
repo.delete(serId);
|
||||
if (null != eventPublisher) {
|
||||
eventPublisher.publishEvent(new AfterDeleteEvent(serId));
|
||||
}
|
||||
|
||||
model.addAttribute(STATUS, HttpStatus.NO_CONTENT);
|
||||
}
|
||||
@@ -547,6 +609,7 @@ public class RepositoryRestController implements InitializingBean {
|
||||
if (null == attr) {
|
||||
model.addAttribute(STATUS, HttpStatus.NOT_FOUND);
|
||||
} else {
|
||||
Object child = typeMeta.entityMetadata.get(attr.getName(), entity);
|
||||
final AtomicReference<String> rel = new AtomicReference<String>();
|
||||
Handler<Object, Void> entityHandler = new Handler<Object, Void>() {
|
||||
@Override public Void handle(Object childEntity) {
|
||||
@@ -621,7 +684,16 @@ public class RepositoryRestController implements InitializingBean {
|
||||
}
|
||||
}
|
||||
|
||||
repo.save(entity);
|
||||
if (null != eventPublisher) {
|
||||
eventPublisher.publishEvent(new BeforeSaveEvent(entity));
|
||||
eventPublisher.publishEvent(new BeforeChildSaveEvent(entity, child));
|
||||
}
|
||||
Object savedEntity = repo.save(entity);
|
||||
if (null != eventPublisher) {
|
||||
child = typeMeta.entityMetadata.get(attr.getName(), savedEntity);
|
||||
eventPublisher.publishEvent(new AfterChildSaveEvent(savedEntity, child));
|
||||
eventPublisher.publishEvent(new AfterSaveEvent(savedEntity));
|
||||
}
|
||||
|
||||
if (request.getMethod() == HttpMethod.PUT) {
|
||||
model.addAttribute(STATUS, HttpStatus.NO_CONTENT);
|
||||
@@ -658,9 +730,16 @@ public class RepositoryRestController implements InitializingBean {
|
||||
} else {
|
||||
final Attribute attr = typeMeta.entityMetadata.linkedAttributes().get(property);
|
||||
if (null != attr) {
|
||||
Object child = typeMeta.entityMetadata.get(property, entity);
|
||||
typeMeta.entityMetadata.set(property, null, entity);
|
||||
|
||||
repo.save(entity);
|
||||
if (null != eventPublisher) {
|
||||
eventPublisher.publishEvent(new BeforeChildSaveEvent(entity, child));
|
||||
}
|
||||
Object savedEntity = repo.save(entity);
|
||||
if (null != eventPublisher) {
|
||||
eventPublisher.publishEvent(new AfterChildSaveEvent(savedEntity, null));
|
||||
}
|
||||
|
||||
model.addAttribute(STATUS, HttpStatus.NO_CONTENT);
|
||||
} else {
|
||||
@@ -817,7 +896,24 @@ public class RepositoryRestController implements InitializingBean {
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
Map m = new HashMap();
|
||||
m.put("message", ex.getMessage());
|
||||
return new ResponseEntity(objectMapper.writeValueAsBytes(m), headers, HttpStatus.BAD_REQUEST);
|
||||
return new ResponseEntity(objectMapper.writeValueAsBytes(m), headers, HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@ExceptionHandler(RepositoryConstraintViolationException.class)
|
||||
public Model handleValidationFailure(RepositoryConstraintViolationException ex) throws IOException {
|
||||
Model model = new ExtendedModelMap();
|
||||
model.addAttribute(STATUS, HttpStatus.BAD_REQUEST);
|
||||
|
||||
Map m = new HashMap();
|
||||
List<String> errors = new ArrayList<String>();
|
||||
for (FieldError fe : ex.getErrors().getFieldErrors()) {
|
||||
errors.add(fe.getDefaultMessage());
|
||||
}
|
||||
m.put("errors", errors);
|
||||
|
||||
model.addAttribute(RESOURCE, m);
|
||||
return model;
|
||||
}
|
||||
|
||||
private static URI buildUri(URI baseUri, String... pathSegments) {
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
package org.springframework.data.rest.webmvc;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.persistence.EntityManagerFactory;
|
||||
|
||||
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.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.data.rest.repository.JpaRepositoryMetadata;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
|
||||
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.servlet.View;
|
||||
import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
|
||||
@@ -37,21 +25,23 @@ public class RepositoryRestMvcConfiguration {
|
||||
@Autowired
|
||||
RepositoryRestConfiguration parentConfig;
|
||||
RepositoryRestController repositoryRestController;
|
||||
@Autowired(required = false)
|
||||
ContentNegotiatingViewResolver viewResolver;
|
||||
|
||||
@Bean ContentNegotiatingViewResolver contentNegotiatingViewResolver() {
|
||||
ContentNegotiatingViewResolver viewResolver = new ContentNegotiatingViewResolver();
|
||||
Map<String, String> jsonTypes = new HashMap<String, String>() {{
|
||||
put("json", "application/json");
|
||||
put("sdjson", "application/x-spring-data+json");
|
||||
put("urilist", "text/uri-list");
|
||||
}};
|
||||
if (null == viewResolver) {
|
||||
viewResolver = new ContentNegotiatingViewResolver();
|
||||
Map<String, String> jsonTypes = new HashMap<String, String>() {{
|
||||
put("json", "application/json");
|
||||
put("urilist", "text/uri-list");
|
||||
}};
|
||||
|
||||
viewResolver.setMediaTypes(jsonTypes);
|
||||
viewResolver.setDefaultViews(
|
||||
Arrays.asList((View) new JsonView("application/json"),
|
||||
(View) new JsonView("application/x-spring-data+json"),
|
||||
(View) new UriListView())
|
||||
);
|
||||
viewResolver.setMediaTypes(jsonTypes);
|
||||
viewResolver.setDefaultViews(
|
||||
Arrays.asList((View) new JsonView("application/json"),
|
||||
(View) new UriListView())
|
||||
);
|
||||
}
|
||||
return viewResolver;
|
||||
}
|
||||
|
||||
@@ -61,6 +51,7 @@ public class RepositoryRestMvcConfiguration {
|
||||
.repositoryMetadata(parentConfig.jpaRepositoryMetadata())
|
||||
.conversionService(parentConfig.conversionService())
|
||||
.httpMessageConverters(parentConfig.httpMessageConverters())
|
||||
.viewResolver(contentNegotiatingViewResolver())
|
||||
.jsonMediaType("application/json");
|
||||
}
|
||||
return repositoryRestController;
|
||||
|
||||
@@ -3,8 +3,11 @@ package org.springframework.data.rest.webmvc.spec
|
||||
import org.codehaus.jackson.map.ObjectMapper
|
||||
import org.codehaus.jackson.map.ser.CustomSerializerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.data.rest.core.SimpleLink
|
||||
import org.springframework.data.rest.core.util.FluentBeanSerializer
|
||||
import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener
|
||||
import org.springframework.data.rest.test.webmvc.Address
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestConfiguration
|
||||
import org.springframework.data.rest.webmvc.RepositoryRestController
|
||||
@@ -22,7 +25,7 @@ import spock.lang.Specification
|
||||
/**
|
||||
* @author Jon Brisbin <jon@jbrisbin.com>
|
||||
*/
|
||||
@ContextConfiguration(classes = [RepositoryRestConfiguration, RepositoryRestMvcConfiguration])
|
||||
@ContextConfiguration(classes = [RepositoryRestConfiguration, RepositoryRestMvcConfiguration, RepositorySpecConfig])
|
||||
class RepositoryRestControllerSpec extends Specification {
|
||||
|
||||
@Shared
|
||||
@@ -132,3 +135,12 @@ class RepositoryRestControllerSpec extends Specification {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
class RepositorySpecConfig {
|
||||
|
||||
@Bean ValidatingRepositoryEventListener validator() {
|
||||
new ValidatingRepositoryEventListener()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.springframework.data.rest.test.webmvc;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.ValidationUtils;
|
||||
import org.springframework.validation.Validator;
|
||||
|
||||
/**
|
||||
* @author Jon Brisbin <jon@jbrisbin.com>
|
||||
*/
|
||||
public class PersonValidator implements Validator {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PersonValidator.class);
|
||||
|
||||
@Override public boolean supports(Class<?> clazz) {
|
||||
return ClassUtils.isAssignable(clazz, Person.class);
|
||||
}
|
||||
|
||||
@Override public void validate(Object target, Errors errors) {
|
||||
Person p = (Person) target;
|
||||
LOG.debug("validating Person " + p);
|
||||
ValidationUtils.rejectIfEmpty(errors, "name", "field.name.required", "Field 'name' cannot be blank.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
<jpa:repositories base-package="org.springframework.data.rest.test.webmvc"/>
|
||||
|
||||
<bean id="beforeSavePersonValidator" class="org.springframework.data.rest.test.webmvc.PersonValidator"/>
|
||||
|
||||
<bean class="org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener"/>
|
||||
|
||||
<!--
|
||||
<bean class="org.springframework.data.rest.test.webmvc.PersonLoader">
|
||||
<property name="personRepository" ref="personRepository"/>
|
||||
|
||||
Reference in New Issue
Block a user