spring-projects / spring-data-rest

Simplifies building hypermedia-driven REST web services on top of Spring Data repositories
https://spring.io/projects/spring-data-rest
Apache License 2.0
920 stars 563 forks source link

Use Entity ResourceProcessor when Serializing Projection [DATAREST-713] #1082

Open spring-projects-issues opened 8 years ago

spring-projects-issues commented 8 years ago

Mathias D opened DATAREST-713 and commented

I am using spring-data-rest 2.4.1 to expose a entity as a rest resource.

I also implemented a ResourceProcessor to add a custom link to the resource

@Component
public class MyEntityResourceProcessor implements ResourceProcessor<Resource<MyEntity>> {
    @Override
    public Resource<MyEntity> process(Resource<MyEntity> resource) {
        resource.add(linkTo(methodOn(CustomController.class).getFeatures(resource.getContent().getId())).withRel("customRel"));
        return resource;
    }
}

This works fine for the single item resource. But I also have setup a ExcerptProjection that reduces the attributes shown in the collection resource:

@Projection(name = "myExcerptProjection", types = MyEntity.class)
interface MyExcerptProjection {
    String getName();
    String getSlogan();
}

When the projection is used my MyEntityResourceProcessor is not invoked and the custom link is missing.

I can bring in the link by implementing a ResourceProcessor for the projection like so:

public class MyEntityProjectionResourceProcessor implements ResourceProcessor<Resource<MyExcerptProjection>>

But I would like to avoid this because:

By default I would expect spring data rest to use the ResourceProcessor of the Entity the Projection belongs to


Affects: 2.4.1 (Gosling SR1)

Reference URL: http://stackoverflow.com/questions/33501648/excerpt-projection-and-custom-links-from-resourceprocessor

1 votes, 4 watchers

spring-projects-issues commented 8 years ago

Oliver Drotbohm commented

I am not sure how this is supposed to work. If you use a projection, there simply is not Resource<MyEntity> but a Resource<MyExcerptProjection>, so we simply can't just invoke the other processor

spring-projects-issues commented 8 years ago

Mathias D commented

So you are saying this is works as designed? So what would you recommend to access the id of the entity to generate links?

Currently my projections contains the id annotated with @JsonIgnore to be able to access it in the ResourceProcessor.

@Projection(name = "myExcerptProjection", types = MyEntity.class)
interface MyEntityExcerptProjection {

    //this is needed in ResourceProcessor to build the link
    @JsonIgnore Long getId();

    //... other getters
}

I do not know how the implementation looks like but the projections could keep a reference to the entity and the resource assembler could use this information to bring in the ResourceProcessor of the corresponding Entity?!

Also I think you could use the projection annotation to know which entity the projection belongs to...

spring-projects-issues commented 8 years ago

Oliver Drotbohm commented

Sort of, yes. In case you're using a projection there simply will never be a Resource<MyEntity> but a Resource<MyEntityProjection>. That makes pretty obvious that a ResourceProcessor<MyEntity> can't be invoked as this would cause ClassCastExceptions as soon as you access getContent().

Couldn't your entity and your projection implement / extend a common interface that has the methods you need?

spring-projects-issues commented 8 years ago

Mathias D commented

Thanks for the quick feedback - good to know that we did not do anything wrong. We thought about the common interface - maybe we go this way. Also the ResourceProcessor is not too complex so we might just live with the code duplication

spring-projects-issues commented 8 years ago

Mathias D commented

See previous comments /discussion - do you want to close the issue?

spring-projects-issues commented 8 years ago

Mathias D commented

I just had a look at the code regarding the issue I described. I saw that the ProxyProjectionFactory is also assigning the TargetAware interface to the proxy that is created. So I can cast the projection instance to TargetAware and access the underlying entity.

I think the ResourceProcessors are applied in org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler#invokeProcessorsFor

So we could add the links generated from the entity resource processor to the projection resources like this:

private Object invokeProcessorsFor(Object value, TypeInformation<?> targetType) {

        Object currentValue = value;
        boolean handleTargetAwareResourceProcessor = (currentValue instanceof Resource)
                && (((Resource<?>) currentValue).getContent() instanceof  TargetAware);
        Object targetValue = null;
        if (handleTargetAwareResourceProcessor) {
            TargetAware targetAwareValue = (TargetAware) ((Resource<?>) currentValue).getContent();
            targetValue = new Resource(targetAwareValue.getTarget());
        }
        // Process actual value
        for (ProcessorWrapper wrapper : this.processors) {
            if (wrapper.supports(targetType, currentValue)) {
                currentValue = wrapper.invokeProcessor(currentValue);
            }
            if (handleTargetAwareResourceProcessor && wrapper.supports(targetType, targetValue)) {
                targetValue = wrapper.invokeProcessor(targetValue);
                ((Resource<?>) currentValue).add(((Resource<?>) targetValue).getLinks());
            }
        }

        return currentValue;
    }

What do you think? Is it worth a pull request? I would like to implement it if you think it has chances to be accepted.

spring-projects-issues commented 8 years ago

Oliver Drotbohm commented

I am afraid it's not. While this hack certainly makes it work somehow I see a couple of downsides:

Again, I am actually not arguing the implementation here but the additional conceptual complexity and inconsistencies introduced are not worth it IMO. I prefer a clean consistent way of working with some minor overhead left to some users over a incredibly sophisticated approach that exposes some inconsistencies in some cases which just puzzle most users

spring-projects-issues commented 8 years ago

Mathias D commented

Thanks for the feedback - I think I was not seeing the whole picture. It was worth a try and interesting to see how it works internally. From my point of view you can close the issue then

spring-projects-issues commented 7 years ago

Petar Tahchiev commented

Guys, any update here? We have a lot of different projections and this hack means we must have 10-12 different ResourceProcessor-s with the same duplicated code inside. It would be really nice if there was a way to specify that this ResourceProcessor should be invoked for every entity or projection, something like this:

public class MyEntityResourceProcessor implements ResourceProcessor<Projection<Resource<MyEntity>>> {
spring-projects-issues commented 7 years ago

Oliver Drotbohm commented

If it's literally the same code, isn't introducing a common interface and referring to that a reasonable solution?

spring-projects-issues commented 7 years ago

Petar Tahchiev commented

Hi Olver, let me see if I follow this through. So let's say we have:

projection A -> renders JSON A
projection B -> renders JSON B
......
projection Z -> renders JSON Z

I assume what you are saying is to introduce a new interface MySuperProjection and then have:

projectionA extends MySuperProjection -> renders JSON A
projectionB extends MySuperProjection -> renders JSON B
.....
projectionZ extends MySuperProjection -> renders JSON Z

And then define two resource processors: one for the entity class, and another one for the MySuperProjection.

My conclusion is:

Again it would be really nice if we could define in the syntax of the class that this ResourceProcessor is to be invoked for the resource itself, as well as for all the projections:

public class MyEntityResourceProcessor implements ResourceProcessor<Projection<Resource<MyEntity>>> {

This way one could inspect the Projection object and get the name of the projection and construct their own logic inside:


if (projectionName == null) {
    // invoked on resource directly (no projection)
}

if (projectionName.equals("search")) {
      //do something
} else if (projectionName.equals("other")) {
     // do something else...
}
spring-projects-issues commented 7 years ago

Oliver Drotbohm commented

I still have a hard time Imagine this to work as you expect us to call a method Resource<MyEntity> process(Resource<MyEntity> resource) but all we have at this point is a Resource<ProjectionA>, right? How's that supposed to work from an invocation point of view?

AresEkb commented 1 year ago

The following works for me:

@Component
public class ProjectionProcessor implements RepresentationModelProcessor<EntityModel<TargetAware>> {

    private final RepresentationModelProcessorInvoker processorInvoker;

    public ProjectionProcessor(@Lazy RepresentationModelProcessorInvoker processorInvoker) {
        this.processorInvoker = processorInvoker;
    }

    @Override
    public EntityModel<TargetAware> process(EntityModel<TargetAware> entityModel) {
        TargetAware content = entityModel.getContent();
        if (content != null) {
            entityModel.add(processorInvoker.invokeProcessorsFor(EntityModel.of(content.getTarget())).getLinks());
        }
        return entityModel;
    }

}