Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RESTEasy Reactive Links #14249

Merged
merged 1 commit into from
Mar 30, 2021
Merged

Conversation

gytis
Copy link

@gytis gytis commented Jan 12, 2021

@FroMage @geoand this is a a draft PR for RESTEasy Reactive Links extension to discuss the API and see if I'm going in a right direction here.

There are three main user facing pieces:

  • @RestLink annotation which marks which methods should be considered for link injection (similar to the RESTEasy @LinkResource)
  • @InjectRestLinks annotation which indicates a method which response should get link headers injected (it uses RestLinksProvider under the covers). Similar to the RESTEasy RESTServiceDiscovery but instead of injecting the links into an object, it injects them as headers.
  • RestLinksProvider interface that has two methods for getting relevant links based on type or object instance. We could use it to generate HAL objects in REST Data Panache or if we'd decide to implement RESTServiceDiscovery from RESTEasy Links.

All JAX-RS methods are scanned in build time when LinksContext is created. In runtime we don't need to do any scans or reflection operations like in RESTEasy Links. It's enough to perform a map lookup to get the link templates and build the actual links.

See TestResource for an example.

I still need to improve this with more testing, EJB security check and a few other bits. But since I have a roughly functioning prototype I'd like to check in now and see what should be adjusted.

@ghost ghost added area/dependencies Pull requests that update a dependency file area/rest labels Jan 12, 2021
@geoand
Copy link
Contributor

geoand commented Jan 12, 2021

Great, thanks a lot @gytis!

I'll have a look tomorrow

@geoand
Copy link
Contributor

geoand commented Jan 13, 2021

Thanks a lot for this work @gytis!

I can't really comment on the API as I don't know much about the topic in hand (@FroMage is much better suited to comment on that), but I have added a few comments about the code itself

Copy link
Member

@FroMage FroMage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some comments and questions. This looks like limited to Link headers ATM, which is fine.
I'm not too sure about the API though, I think it would help if you added examples which we can use as use-cases to verify what it does.
It looks like you're resolving path parameters directly as fields from the resources, which is probably fine, but for resteasy classic (https://docs.jboss.org/resteasy/docs/2.0.0.GA/userguide/html/LinkHeader.html) I only handled IDs. I guess this is more powerful, but requires that the path parameter name matches the entity property.

}

@Override
public void visitEnd() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like you're generating getters even if one already exists.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It did initially.
I wasn't sure how to check if a method exists from a visitor and I couldn't use an index, because it doesn't get updated. So I've added an extra mapping here https://github.com/quarkusio/quarkus/pull/14249/files/a6de4a1aa44bc8a1102de72835ba98a1355d5c8d#diff-938ce5408bf74bba0f73f4922b625306f3a9deced50ca5afcde36c03d431f3abR49 to make sure that only one getter is generated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I usually collect signatures in visitMethod

.getHeaders()
.getValues("Link");
assertThat(links).containsOnly(
Link.fromUriBuilder(UriBuilder.fromUri(testWithLinksUrl).path("/1")).rel("getById").build().toString(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO according to https://www.iana.org/assignments/link-relations/link-relations.xhtml this should be self for the rel.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that because we cannot infer all the possible rel values I would simply default it to the method name and allow it to be overwritten with an annotation (@RestLink(rel="self")).

.getValues("Link");
assertThat(links).containsOnly(
Link.fromUriBuilder(UriBuilder.fromUri(testWithLinksUrl).path("/1")).rel("getById").build().toString(),
Link.fromUri(testWithLinksUrl).rel("list").build().toString(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again according to IANA this should probably be collection.

assertThat(links).containsOnly(
Link.fromUriBuilder(UriBuilder.fromUri(testWithLinksUrl).path("/1")).rel("getById").build().toString(),
Link.fromUri(testWithLinksUrl).rel("list").build().toString(),
Link.fromUri(testWithoutLinksUrl).rel("listWithoutLinks").build().toString());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But then, not sure what to do about this. Need to check if we can have multiple similar relations.

@Path("/with-links/{id}")
@Produces(MediaType.APPLICATION_JSON)
@RestLink
@InjectRestLinks(RestLinkType.INSTANCE)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what instance refers to here: we do have an instance returned, so…

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment by default TYPE links are injected i.e. get all and create.

@GET
@Path("/with-links")
@Produces(MediaType.APPLICATION_JSON)
@RestLink(entityType = TestRecord.class, rel = "list")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be optional, in which case we can infer that the entity type is TestRecord (from the return type List<TestRecord> and same for the rel which should be collection.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have this implemented yet, but inferring an entity type from a generic is in my todo. So for now this is a workaround.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for the rel, this annotation allows to define any rel you want.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface InjectRestLinks {

RestLinkType value() default RestLinkType.TYPE;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder what purpose this serves. Can you show me use-cases? The example test could deduce that from the method return type.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that this would allow user to specify which links does he want to be injected.
E.g. if a DELETE operation returns a deleted entity, you don't necessarily want to return the links specific for that entity (self, update, delete) because it doesn't exist any more. But you might still want to return the links for getting all and creating new entities.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I see, but that feels special-casy.

for (Method method : instance.getClass().getMethods()) {
if (method.getName().equals(pathParameterInfo.getValueGetter())) {
try {
return method.invoke(instance);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can get rid of this reflection by generating bytecode that invokes it for a given instance like this:

public class $$Order$Id implements Getter<Order,Long> {
 public static Long get(Order order){
  return order.$$generated_getter_id();
 }
}

And you instantiate those getters at static init and can just invoke them without reflection when you need to.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I'll add that.

@gytis
Copy link
Author

gytis commented Jan 20, 2021

Thanks for the comments @FroMage.

I added some comments and questions. This looks like limited to Link headers ATM, which is fine.

For now I've added two things. A RestLinksProvider bean, which returns a collection of links based on type or instance. And a Link headers injection, which simply takes the links from the provider and adds link headers. We could inject other types by reusing the same provider. I just didn't want to add too many features without discussing the API.

I'm not too sure about the API though, I think it would help if you added examples which we can use as use-cases to verify what it does.

This is just a draft to discuss the API. I don't expect it to be final and I'm happy to change it. That's the whole point.

It looks like you're resolving path parameters directly as fields from the resources, which is probably fine, but for resteasy classic (https://docs.jboss.org/resteasy/docs/2.0.0.GA/userguide/html/LinkHeader.html) I only handled IDs. I guess this is more powerful, but requires that the path parameter name matches the entity property.

Yes I've added a possibility to resolve any entity field. I haven't yet added an override option to the @RestLink annotation. With that the path parameter won't need to match the field name.
Do you think this is too much and I should stick to the ID only?

@FroMage
Copy link
Member

FroMage commented Jan 20, 2021

Yes I've added a possibility to resolve any entity field. I haven't yet added an override option to the @RestLink annotation. With that the path parameter won't need to match the field name.
Do you think this is too much and I should stick to the ID only?

No, I think it's an interesting approach, and more powerful. Let's try it.

@gytis gytis force-pushed the resteasy-reactive-links branch 2 times, most recently from 4f58ec1 to 00d7bfb Compare February 10, 2021 17:34
@ghost ghost added the area/core label Feb 10, 2021
@gytis
Copy link
Author

gytis commented Feb 11, 2021

@FroMage @geoand, sorry for a slight delay in the update but I think I've covered all the comments. However, I still have a couple of points that need to be cleared up.

  1. For now I left the RestLinkType enum to specify whether type or instance links should be injected. @FroMage had some reservations regarding this point, so I'm open for a debate about that.

  2. How sophisticated do we want the Entity type resolution from a generic type to be? For now I only check one level of a single argument parameterised type (e.g. Link, Collection, Set). Does any of you have experience with this? Should we make this deduction more sophisticated? Or maybe we should step back an only check a predefined set of interfaces like RESTEasy Links does?

@geoand
Copy link
Contributor

geoand commented Feb 12, 2021

My comments have been addressed, the rest I can really comment on since I don't have any experience with links

Copy link
Member

@FroMage FroMage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@FroMage
Copy link
Member

FroMage commented Feb 12, 2021

How sophisticated do we want the Entity type resolution from a generic type to be? For now I only check one level of a single argument parameterised type (e.g. Link, Collection, Set). Does any of you have experience with this? Should we make this deduction more sophisticated? Or maybe we should step back an only check a predefined set of interfaces like RESTEasy Links does?

Just handle collections and special types on a single level, yeah.

@gytis gytis force-pushed the resteasy-reactive-links branch from 00d7bfb to 0ce15a3 Compare February 16, 2021 12:18
@gytis gytis marked this pull request as ready for review February 16, 2021 12:20
@gytis
Copy link
Author

gytis commented Feb 16, 2021

I've just squashed the commits and added the extension file.

@gytis gytis force-pushed the resteasy-reactive-links branch 2 times, most recently from 5af2ef5 to bcc5339 Compare February 16, 2021 17:35
@quarkus-bot quarkus-bot bot added area/devtools Issues/PR related to maven, gradle, platform and cli tooling/plugins area/documentation labels Feb 16, 2021
Base automatically changed from master to main March 12, 2021 15:55
@gytis gytis force-pushed the resteasy-reactive-links branch 2 times, most recently from 665dd43 to e212cbc Compare March 15, 2021 12:05
@geoand
Copy link
Contributor

geoand commented Mar 26, 2021

Ah damn... We forgot to merge and now there are conflicts :(

Can you rebase Gytis so we can get this in?

@geoand geoand added the triage/waiting-for-ci Ready to merge when CI successfully finishes label Mar 26, 2021
@gytis gytis force-pushed the resteasy-reactive-links branch from e212cbc to ff688c4 Compare March 26, 2021 11:26
@geoand geoand merged commit 7f41b56 into quarkusio:main Mar 30, 2021
@quarkus-bot quarkus-bot bot added this to the 1.14 - main milestone Mar 30, 2021
@geoand
Copy link
Contributor

geoand commented Apr 21, 2022

@Sgitario would you be interested in writing a guide for RESTEasy Reactive links?
The idea being that this module although it's currently only a building block for REST Data Panache, it could however provide a general solution for JAX-RS Paging.

@maxandersen
Copy link
Member

related to #9052 ?

@geoand
Copy link
Contributor

geoand commented Apr 21, 2022

Related, but we might want something ever higher level

@geoand geoand removed the triage/waiting-for-ci Ready to merge when CI successfully finishes label Apr 21, 2022
@Sgitario
Copy link
Contributor

@Sgitario would you be interested in writing a guide for RESTEasy Reactive links? The idea being that this module although it's currently only a building block for REST Data Panache, it could however provide a general solution for JAX-RS Paging.

I really like the idea. Count on me to do this.

@geoand
Copy link
Contributor

geoand commented Apr 22, 2022

Thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/core area/dependencies Pull requests that update a dependency file area/devtools Issues/PR related to maven, gradle, platform and cli tooling/plugins area/documentation area/panache area/rest
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants