Added ApplicationEvent handling and annotation-based extension mechanism.

This commit is contained in:
Jon Brisbin
2012-04-26 11:40:14 -05:00
parent 57caa05823
commit fe9e485d81
31 changed files with 987 additions and 41 deletions

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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()
}
}

View File

@@ -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.");
}
}

View File

@@ -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"/>