简体   繁体   中英

Dropwizard abstract resource design

I think this is a more generic java question, but I'll explain what I'm trying to do and hopefully someone can point me the right way;

I'm trying to create a generic abstract class that all my resources can extend from.

The abstract class has basic CRUD implementations for the standard stuff

@Produces("application/vnd.api+json")
@Consumes("application/vnd.api+json")
public abstract class AbstractResource {

    static final Logger LOGGER = LoggerFactory.getLogger(AbstractResource.class);

    AbstractRepository repository;

    AbstractResource(AbstractRepository repository) {
        this.repository = repository;
    }

    @GET
    public Response getAll(@Auth User user, @QueryParam("query") String query) {
        String result = query != null ? repository.getByQuery(query) : repository.getAll();
        return Response.status(Response.Status.OK).entity(result).build();
    }

    @GET
    @Path("/{id}")
    public Response getById(@Auth User user, @PathParam("id") String id) {
        String result = repository.getById(id);
        return Response.status(Response.Status.OK).entity(result).build();
    }

    @POST
    public Response save(@Auth User user, String payload) {
        String result = repository.save(payload);
        return Response.status(Response.Status.OK).entity(result).build();
    }

    @PATCH
    @Path("/{id}")
    public Response update(@Auth User user, @PathParam("id") String id, String payload) {
        String result = repository.update(payload);
        return Response.status(Response.Status.OK).entity(result).build();
    }

    @DELETE
    @Path("/{id}")
    public Response delete(@Auth User user, @PathParam("id") String id) {
        repository.delete(id);
        return Response.status(Response.Status.NO_CONTENT).build();
    }

}

I can use this without a problem simply doing

@Path("/movies")
public class MovieResource extends AbstractResource {
    public MovieResource(MovieRepository repository) {
        super(repository);
    }
}

and I can now access all the methods and override as required.

Where I run into problems is when I need to overload a method. Take the first getAll method from the abstract class as example, I want to change the parameters in only the Movie.class

@Path("/movies")
public class MovieResource extends AbstractResource {

    public MovieResource(MovieRepository repository) {
        super(repository);
    }

    @GET
    public Response getAll(@Auth User user, @QueryParam("query") String query, @QueryParam("limit") String limit, @QueryParam("page") String page) {
        String result = repository.getPaginated(limit, page);
        return Response.status(Response.Status.OK).entity(result).build();
    }

}

So the getAll method has a different set of parameters in just the Movie.class . This causes Jersey to blow up with

[[FATAL] A resource model has ambiguous (sub-)resource method for HTTP method GET and input mime-types as defined by"@Consumes" and "@Produces" annotations at Java methods public javax.ws.rs.core.Response space.cuttlefish.domain.resources.MovieResource.getAll(space.cuttlefish.domain.model.User,java.lang.String,java.lang.String,java.lang.String) and public javax.ws.rs.core.Response space.cuttlefish.domain.resources.AbstractResource.getAll(space.cuttlefish.domain.model.User,java.lang.String) at matching regular expression /movies. These two methods produces and consumes exactly the same mime-types and therefore their invocation as a resource methods will always fail.; source='org.glassfish.jersey.server.model.RuntimeResource@6a1ef65c']

Because the original getAll method of the abstract already has the @GET annotation.

So, how do I go about solving this?

Do I remove all the annotations from the abstract class, and then have to override and re-add the annotations in each resource? That just seems messy and prone to error... There must be a better solution here?

Is there something blindingly obvious I've just overlooked?

Would love some help!

I recommend using Generics.

We have accomplished a similar but rather complex version of this. It was a bit hard to get it right in the beginning, but we had maximum code reusability (with Java) and easy to read/contribute code.

public abstract class AbstractResource<T extends AbstractObject, K extends AbstractObjectDto> {

    static final Logger LOGGER = LoggerFactory.getLogger(AbstractResource.class);

    AbstractRepository<T> repository;
    // We have used modelmapper library to automatically convert DTO objects to database objects. But you can come up with your own solution for that. I.E implementing conversion logic on each DTO and database classes.
    ModelMapper modelMapper = new ModelMapper(); 

    // With Java Generics, one cannot access the class type directly by simply calling 'K.class'. So you need to pass the class types explicitly as well. That is if you're using modelmapper.
    private final Class<T> objectClass;
    private final Class<K> objectDtoClass;

    AbstractResource(AbstractRepository<T> repository, Class<T> objectClass, Class<K> objectDtoClass) {
        this.repository = repository;
        this.objectClass = objectClass;
        this.objectDtoClass = objectDtoClass;
    }

    ...

    @POST
    public K save(@Auth User user, @Valid K payload) {
        T databaseObject = modelmapper.map(payload, objectClass);
        T result = repository.save(databaseObject);
        K resultDto = modelMapper.map(result, objectDtoClass);
        retun resultDto;
    }
    ...
}

Then you need to create a repository class that has the necessary methods like save , getPaginated etc. for each object type, overriding AbstractRepository . And of course, Movie should extend AbstractObject class, and MovieDto should extend AbstractObjectDto class.

public class MovieRepository extends AbstractRepository<Movie> {
    ....
    Movie save(Movie movie) {...}
}

And the rest is as simple as this:

@Path("/movies")
public class MovieResource extends AbstractResource<Movie, MovieDto> {

    public MovieResource(MovieRepository repository) {
        super(repository, Movie.class, MovieDto.class);
    }
}

The reason why it fails for you is that in your example, multiple methods map to the same URL path. But if you just override a method Jersey won't complain.

I would recommend to have generic methods in your AbstractResource, where either you pass @Context UriInfo uriInfo to your method and parse its query params in a generic utility method, or use something like matrix parameters via

@Path("/{segment: .*}")
@GET
@Produces("application/json")
public Response getAll(@PathParam("segment") PathSegment segment)
...

and parse them again via a generic default method, or combination of both.

In such a way, you can default to common endpoint in many cases, or do custom preprocessing and delegate to common parsing methods for typical use cases.

If I got you right something like you wanted was attempted in the following project: https://github.com/researchgate/restler (Disclaimer: I'm a contributor there)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM