Skip to content

Commit

Permalink
Support @UnitOfWork in sub-resources
Browse files Browse the repository at this point in the history
Currently, Dropwizard doesn't open transactions in sub-resources as reported
in #1806. The problem is that Dropwizard scans resource methods for
`@UnitOfWork` during resource initilization and sub-resource methods are
not resolved at that time and can't be registered by Dropwizard. The fix is
to defer the lookup of the `@UnitOfWork` annotation on a method until it's
invoked and than cache it. One downside of this approache is that
`ConcurrentMap` doesn't support null values, so we have to use `Optional`
from `UnitOfWork` which can add an additional overhead, but it should be
negligible.
  • Loading branch information
arteam committed Mar 13, 2017
1 parent 11366d5 commit 63be4e9
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 33 deletions.
5 changes: 5 additions & 0 deletions dropwizard-hibernate/pom.xml
Expand Up @@ -52,6 +52,11 @@
<artifactId>hsqldb</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-inmemory</artifactId>
Expand Down
@@ -1,7 +1,6 @@
package io.dropwizard.hibernate;

import org.glassfish.jersey.server.internal.process.MappableException;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.ResourceMethod;
import org.glassfish.jersey.server.monitoring.ApplicationEvent;
import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
Expand All @@ -10,9 +9,11 @@
import org.hibernate.SessionFactory;

import javax.ws.rs.ext.Provider;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;


/**
Expand All @@ -27,7 +28,7 @@
@Provider
public class UnitOfWorkApplicationListener implements ApplicationEventListener {

private Map<Method, UnitOfWork> methodMap = new HashMap<>();
private ConcurrentMap<ResourceMethod, Optional<UnitOfWork>> methodMap = new ConcurrentHashMap<>();
private Map<String, SessionFactory> sessionFactories = new HashMap<>();

public UnitOfWorkApplicationListener() {
Expand Down Expand Up @@ -58,11 +59,11 @@ public void registerSessionFactory(String name, SessionFactory sessionFactory) {
}

private static class UnitOfWorkEventListener implements RequestEventListener {
private final Map<Method, UnitOfWork> methodMap;
private ConcurrentMap<ResourceMethod, Optional<UnitOfWork>> methodMap;
private final UnitOfWorkAspect unitOfWorkAspect;

UnitOfWorkEventListener(Map<Method, UnitOfWork> methodMap,
Map<String, SessionFactory> sessionFactories) {
UnitOfWorkEventListener(ConcurrentMap<ResourceMethod, Optional<UnitOfWork>> methodMap,
Map<String, SessionFactory> sessionFactories) {
this.methodMap = methodMap;
unitOfWorkAspect = new UnitOfWorkAspect(sessionFactories);
}
Expand All @@ -71,9 +72,9 @@ private static class UnitOfWorkEventListener implements RequestEventListener {
public void onEvent(RequestEvent event) {
final RequestEvent.Type eventType = event.getType();
if (eventType == RequestEvent.Type.RESOURCE_METHOD_START) {
UnitOfWork unitOfWork = methodMap.get(event.getUriInfo()
.getMatchedResourceMethod().getInvocable().getDefinitionMethod());
unitOfWorkAspect.beforeStart(unitOfWork);
Optional<UnitOfWork> unitOfWork = methodMap.computeIfAbsent(event.getUriInfo()
.getMatchedResourceMethod(), UnitOfWorkEventListener::registerUnitOfWorkAnnotations);
unitOfWorkAspect.beforeStart(unitOfWork.orElse(null));
} else if (eventType == RequestEvent.Type.RESP_FILTERS_START) {
try {
unitOfWorkAspect.afterEnd();
Expand All @@ -86,40 +87,24 @@ public void onEvent(RequestEvent event) {
unitOfWorkAspect.onFinish();
}
}

private static Optional<UnitOfWork> registerUnitOfWorkAnnotations(ResourceMethod method) {
UnitOfWork annotation = method.getInvocable().getDefinitionMethod().getAnnotation(UnitOfWork.class);
if (annotation == null) {
annotation = method.getInvocable().getHandlingMethod().getAnnotation(UnitOfWork.class);
}
return Optional.ofNullable(annotation);
}
}

@Override
public void onEvent(ApplicationEvent event) {
if (event.getType() == ApplicationEvent.Type.INITIALIZATION_APP_FINISHED) {
for (Resource resource : event.getResourceModel().getResources()) {
for (ResourceMethod method : resource.getAllMethods()) {
registerUnitOfWorkAnnotations(method);
}

for (Resource childResource : resource.getChildResources()) {
for (ResourceMethod method : childResource.getAllMethods()) {
registerUnitOfWorkAnnotations(method);
}
}
}
}
}

@Override
public RequestEventListener onRequest(RequestEvent event) {
return new UnitOfWorkEventListener(methodMap, sessionFactories);
}

private void registerUnitOfWorkAnnotations(ResourceMethod method) {
UnitOfWork annotation = method.getInvocable().getDefinitionMethod().getAnnotation(UnitOfWork.class);

if (annotation == null) {
annotation = method.getInvocable().getHandlingMethod().getAnnotation(UnitOfWork.class);
}

if (annotation != null) {
this.methodMap.put(method.getInvocable().getDefinitionMethod(), annotation);
}

}
}
@@ -0,0 +1,243 @@
package io.dropwizard.hibernate;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.dropwizard.Application;
import io.dropwizard.Configuration;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.db.PooledDataSourceFactory;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import io.dropwizard.testing.ResourceHelpers;
import io.dropwizard.testing.junit.DropwizardAppRule;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.junit.ClassRule;
import org.junit.Test;

import javax.ws.rs.*;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

public class SubResourcesTest {

public static class TestConfiguration extends Configuration {

DataSourceFactory dataSource = new DataSourceFactory();

TestConfiguration(@JsonProperty("dataSource") DataSourceFactory dataSource) {
this.dataSource = dataSource;
}
}

public static class TestApplication extends Application<TestConfiguration> {
final HibernateBundle<TestConfiguration> hibernate = new HibernateBundle<TestConfiguration>(Person.class, Dog.class) {
@Override
public PooledDataSourceFactory getDataSourceFactory(TestConfiguration configuration) {
return configuration.dataSource;
}
};

@Override
public void initialize(Bootstrap<TestConfiguration> bootstrap) {
bootstrap.addBundle(hibernate);
}

@Override
public void run(TestConfiguration configuration, Environment environment) throws Exception {
final SessionFactory sessionFactory = hibernate.getSessionFactory();
initDatabase(sessionFactory);

environment.jersey().register(new UnitOfWorkApplicationListener("hr-db", sessionFactory));
environment.jersey().register(new PersonResource(new PersonDAO(sessionFactory), new DogDAO(sessionFactory)));
}

private void initDatabase(SessionFactory sessionFactory) {
try (Session session = sessionFactory.openSession()) {
Transaction transaction = session.beginTransaction();
session.createNativeQuery("CREATE TABLE people (name varchar(100) primary key, email varchar(16), birthday timestamp)")
.executeUpdate();
session.createNativeQuery("INSERT INTO people VALUES ('Greg', 'greg@yahooo.com', '1989-02-13')")
.executeUpdate();
session.createNativeQuery("CREATE TABLE dogs (name varchar(100) primary key, owner varchar(100) REFERENCES people(name))")
.executeUpdate();
session.createNativeQuery("INSERT INTO dogs VALUES ('Bello', 'Greg')")
.executeUpdate();
transaction.commit();
}
}
}

@Produces(MediaType.APPLICATION_JSON)
@Path("/people")
public static class PersonResource {
private final PersonDAO personDao;
private final DogResource dogResource;

PersonResource(PersonDAO dao, DogDAO dogDao) {
this.personDao = dao;
this.dogResource = new DogResource(dogDao, personDao);
}

@GET
@Path("{name}")
@UnitOfWork(readOnly = true)
public Optional<Person> find(@PathParam("name") String name) {
return personDao.findByName(name);
}

@POST
@UnitOfWork
public Person save(Person person) {
return personDao.persist(person);
}

@Path("/{ownerName}/dogs")
public DogResource dogResource() {
return dogResource;
}
}

@Produces(MediaType.APPLICATION_JSON)
public static class DogResource {

private final DogDAO dogDAO;
private final PersonDAO personDAO;

DogResource(DogDAO dogDAO, PersonDAO personDAO) {
this.dogDAO = dogDAO;
this.personDAO = personDAO;
}

@GET
// Intentionally no `@UnitOfWork`
public List<Dog> findAll(@PathParam("ownerName") String ownerName) {
return dogDAO.findByOwner(ownerName);
}

@GET
@Path("{dogName}")
@UnitOfWork(readOnly = true)
public Optional<Dog> find(@PathParam("ownerName") String ownerName,
@PathParam("dogName") String dogName) {
return dogDAO.findByOwnerAndName(ownerName, dogName);
}

@POST
@UnitOfWork
public Dog create(@PathParam("ownerName") String ownerName, Dog dog) {
Optional<Person> person = personDAO.findByName(ownerName);
if (!person.isPresent()) {
throw new WebApplicationException(404);
}
dog.setOwner(person.get());
return dogDAO.persist(dog);
}
}

public static class PersonDAO extends AbstractDAO<Person> {
PersonDAO(SessionFactory sessionFactory) {
super(sessionFactory);
}

Optional<Person> findByName(String name) {
return Optional.ofNullable(get(name));
}
}

public static class DogDAO extends AbstractDAO<Dog> {
DogDAO(SessionFactory sessionFactory) {
super(sessionFactory);
}

Optional<Dog> findByOwnerAndName(String ownerName, String dogName) {
return query("SELECT d FROM Dog d WHERE d.owner.name=:owner AND d.name=:name")
.setParameter("owner", ownerName)
.setParameter("name", dogName)
.uniqueResultOptional();
}

List<Dog> findByOwner(String ownerName) {
return query("SELECT d FROM Dog d WHERE d.owner.name=:owner")
.setParameter("owner", ownerName)
.list();
}
}

@ClassRule
public static DropwizardAppRule<TestConfiguration> APP_RULE = new DropwizardAppRule<>(TestApplication.class,
ResourceHelpers.resourceFilePath("hibernate-sub-resource-test.yaml"));

private static String baseUri() {
return "http://localhost:" + APP_RULE.getLocalPort();
}

@Test
public void canReadFromTopResource() throws Exception {
final Person person = APP_RULE.client()
.target(baseUri() + "/people/Greg")
.request()
.get(Person.class);

assertThat(person.getName()).isEqualTo("Greg");
}

@Test
public void canWriteTopResource() throws Exception {
final Person person = APP_RULE.client()
.target(baseUri() + "/people")
.request()
.post(Entity.entity("{\"name\": \"Jason\", \"email\": \"jason@gmail.com\", \"birthday\":637317407000}",
MediaType.APPLICATION_JSON_TYPE), Person.class);

assertThat(person.getName()).isEqualTo("Jason");
}

@Test
public void canReadFromSubResources() throws Exception {
final Dog dog = APP_RULE.client()
.target(baseUri() + "/people/Greg/dogs/Bello")
.request()
.get(Dog.class);

assertThat(dog.getName()).isEqualTo("Bello");
assertThat(dog.getOwner()).isNotNull();
assertThat(dog.getOwner().getName()).isEqualTo("Greg");
}

@Test
public void canWriteSubResource() throws Exception {
final Dog dog = APP_RULE.client()
.target(baseUri() + "/people/Greg/dogs")
.request()
.post(Entity.entity("{\"name\": \"Bandit\"}", MediaType.APPLICATION_JSON_TYPE), Dog.class);

assertThat(dog.getName()).isEqualTo("Bandit");
assertThat(dog.getOwner()).isNotNull();
assertThat(dog.getOwner().getName()).isEqualTo("Greg");
}

@Test
public void errorsAreHandled() throws Exception {
Response response = APP_RULE.client()
.target(baseUri() + "/people/Jim/dogs")
.request()
.post(Entity.entity("{\"name\": \"Bullet\"}", MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(404);
}

@Test
public void noSessionErrorIsRaised() throws Exception {
Response response = APP_RULE.client()
.target(baseUri() + "/people/Greg/dogs")
.request()
.get();
assertThat(response.getStatus()).isEqualTo(500);
}

}
@@ -0,0 +1,10 @@
server:
applicationConnectors:
- type: http
port: 0
adminConnectors:
- type: http
port: 0
dataSource:
url: 'jdbc:h2:mem:sub-resources-test'
driverClass: "org.h2.Driver"

0 comments on commit 63be4e9

Please sign in to comment.