Closed drdamour closed 6 years ago
Can you elaborate what you mean by that? What exactly are you referring to with "name of the collection"? Relation names are abstracted away by a RelProvider
and we ship two implementations: one based on the Evo Inflector library and one that creates a default …List
for collection relations and uses the lower case simple class name for the item relations.
You're free to simply implement a RelProvider
on your own, implement supports(Class<?> type)
to select the types you want to be responsible for and register your implementation as Spring bean. This will cause the rendering infrastructure (e.g. the custom Jackson renderers we deploy) to call your implementation when calculating the relation names for representations.
That can work as a workaround...but it is possible that i would have two collections of the same type with different semantic meaning.
So in the HAL format i could have something like this at /customer/123
{
_links : [
"favorite products" : [
{href : "/products/998"},
{href : "/products/777"}
],
"purchased products" : [
{href : "/products/998"},
{href : "/products/444"},
{href : "/products/333"},
{href : "/products/222"},
{href : "/products/111"},
{href : "/products/555"},
{href : "/products/666"},
]
]
}
but in my protocol i might implement something like the zoom protocol to get things embedded. so a url like /customer/123?expand=purchased products&size=4 should give back something like this:
{
_links : [
"favorite products" : [
{href : "/products/998"},
{href : "/products/777"}
],
"purchased products" : [
{href : "/products/998"},
{href : "/products/444"},
{href : "/products/333"},
{href : "/products/222"},
{href : "/products/111"},
{href : "/products/555"},
{href : "/products/666"},
]
],
_embedded : [
{
"purchased products" [
{
_links : { self : { href : "/products/998"} },
someProductProperty : "someValue"
},
{
_links : { self : { href : "/products/444"} },
someProductProperty : "someValue"
},
{
_links : { self : { href : "/products/333"} },
someProductProperty : "someValue"
},
{
_links : { self : { href : "/products/222"} },
someProductProperty : "someValue"
}
]
}
],
page : { ...}
}
the embedded name should be the same as the link. So yes i could do this for one of the links, but the other at URL /customer/123?expand=favorite products&size=4 would be out of luck. and you could technically do both simultaniously (but paging both would be pretty strange).
Personally i want to just name the embedded paged collection items or page items as someTypeNameList isn't semantically meaningful enough. Your workaround will probably get me there.
Traverson (the JS variety) attempts to perform this optimization (look for link relation name in _embedded if it's provided). The auto-generated name does not match what would be generated in a link list, and so Traverson can't find _embedded links.
+1 to drdamour's remarks, including the inadequacy of driving the name via types alone.
It would be useful for example to be able to do something like
PagedResources<PersonResource> pagedResources =
new PagedResources("employees", employeeDtos, pageMetadata, links);
and have that show up as
"_embedded": {
"employees": [ ... ]
}
rather than
"_embedded": {
"personResourceList": [ ... ]
}
+1
For those who didn't get Olivers answer straight away, like me:
If you want the plural form of the class' name rather than uncapitalized class.getSimpleName + "List", you just need to use the EvoInflectorRelProvider instead of the standard DefaultRelProvider.
If you use Spring Boot, just add the following dependency
<dependency>
<groupId>org.atteo</groupId>
<artifactId>evo-inflector</artifactId>
<version>1.2</version>
</dependency>
otherwise you need to also add the following bean to your configuration class
@Bean
public RelProvider relProvider() {
return new EvoInflectorRelProvider();
}
These results in returning
"_embedded": {
"users": [{
"name": "Marvin"
}]
}
instead of
"_embedded": {
"userList": [{
"name": "Marvin"
}]
}
As documented in the reference documentation. :smile:
lol. I totally missed the point 3. 😄
+1.
Also add to OP's comment, what's the rationale to have PagedResources returned under _embedded with class name? Is it possible to return just the array of data like non-HAL format?
Like for /v1/users/
just return
"_embedded":
[
{
"name": "Marvin"
},
{
"name": "Alex"
}
]
This way client doesn't have to know the property name to look for under _embedded.
The HAL specification.
Even if we can have, for example the name 'users' instead of the name 'userResourceList', we still have a name varying for each resource type. It'd be helpful to configure a static name, common to all resource types, like 'items' for example.
@stephaneeybert having one static name for all collections would be very easy with the solution that @olivergierke suggested first.
Implement your custom RelProvider which will always return items
for the collection name and item
for the item name and always returns true for the method supports(Class<?> type)
. You can check out the DefaultRelProvider.
To be honest, I would not do that because the client is supposed to know what it was asking for anyway. And a resource type reflects a resource which itself should be identifiable and recognizable. Having everything named items sounds like the complete opposite of that.
@marvinrichter I just found out about the @Relation(collectionRelation = RESTConstants.EMBEDDED_COLLECTION_NAME)
and implemented it.
But reading your advice I shall revert and keep the default naming in place.
That depends on what you want to achieve. If there are only some resources you want to name items
then the approach with @Relation is the way to go. Think of it as some kind of overruling the default behavior.
But if you want to have all collections named items
by default then I would suggest to write your own RelProvider which enforces this default behavior.
I for myself like the pluralized forms and use the EVO Inflector. So instead of having items
or userList
I get users
.
Given the project supports both annotation overrides and writing your own RelProvider, I’m closing this issue.
@gregturn the specific scenario I outlined in
https://github.com/spring-projects/spring-hateoas/issues/175#issuecomment-43690320 remains unsupported and relprovided nor annotations supports it. can we please reopen
@drdamour It would like to read a proper english with no obvious spelling mistakes, and if possible, punctuation and accentuation. As to your request, may I suggest you provide the maintainer with a bit more explaination ?
Again, using EvoInflectorRelProvider as a guide, write your own. Or use @Relation.
@gregturn as outlined in https://github.com/spring-projects/spring-hateoas/issues/175#issuecomment-43690320 neither of those suggestions work when you need two of the same type embedded within the same resource, there's no way to distinguish between them. perhaps if @relation was on the getter or field instead of just the type it could work.
@stephaneeybert what is missing from https://github.com/spring-projects/spring-hateoas/issues/175#issuecomment-43690320 ?
@drdamour I'm reading your comment again and again to try to understand what you want to do. For example; that phrase "but in my protocol i might implement something like the zoom protocol to get things embedded." is not so easy to grasp. From what I could guess, you are trying to have in the _embedded node a list of items that doesn't match what you have in the _links node. Why do you want to filter out part of the content of the _embedded node ? Can't you leave that to the client consuming the API response ?
zoom protocol just allows client have more control on what is and is not embedded, and irrelevant to the underlying issue but was a real world example i had at the time. if you look again I'll think you'll see everything embedded in the example was also a link and not divergent as suggested by you.
again the issue is the available rel defining methods are limited to the type being embeeded, and I often have the same type being embedded for distinct rels. eg favorite-products and purchased-products on a user resource
RelProvider and the annotation give you the power to name the collection.
Link rels you control.
Therefore you can do all this.
@gregturn can you explain how since relprovider's signature only has the class as a variable (and not something like the field name in it's method signature)...how can I return a different rel for two collections of the same type?
Here's how you'd create an _embedded
clause with objects of different type under the same link relation:
class First {
public String getName() {
return "name";
}
}
class Second {
public String getOtherName() {
return "otherName";
}
}
EmbeddedWrappers wrappers = new EmbeddedWrappers(false);
List<Object> elements = new ArrayList<>();
LinkRelation relation = LinkRelation.of("common");
elements.add(wrappers.wrap(new First(), relation));
elements.add(wrappers.wrap(new Second(), relation));
CollectionModel<Object> model = new CollectionModel<>(elements);
For the HAL module this renders:
{
"_embedded" : {
"common" : [ {
"name" : "name"
}, {
"otherName" : "otherName"
} ]
}
}
but my problem is the reverse...two getters of the SAME type with different rels.
looks like EmbeddedWrappers would work for that case too now. cool.
Yes, if an explicit LinkRelation
is provided there's no implicit resolution – and thus grouping – going on. Does that mean we consider you use case covered?
trying it out
@RestController
public class ThingController {
class Product {
private final String name;
Product(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Result extends EntityModel<Result> {
private final List<Object> allProducts;
public Result(
List<Object> allProducts
){
this.allProducts = allProducts;
}
public List<Object> getAllProducts() {
return allProducts;
}
}
@GetMapping("/")
RepresentationModel stuff(){
EmbeddedWrappers wrappers = new EmbeddedWrappers(false);
List<Object> elements = new ArrayList<>();
elements.add(wrappers.wrap(new Product("Product1a"), LinkRelation.of("all")));
elements.add(wrappers.wrap(new Product("Product2a"), LinkRelation.of("purchased")));
elements.add(wrappers.wrap(new Product("Product1b"), LinkRelation.of("all")));
return new CollectionModel(elements);
}
}
gave me
{
_embedded: {
purchased: {
name: "Product2a"
},
all: [
{
name: "Product1a"
},
{
name: "Product1b"
}
]
}
}
so that is pretty nice...but when switched the return to return new Result(elements)
i didn't get hal back...but i supsect that is just some limitation of the hateoas serializer. i can work with that.
thanks!
ah yeah i see now, changing to RepresentationalModel and changing the getter to public
List<Object> getContent() {
return allProducts;
}
content property is special to jackson serializer looks like...but getContent is there on entity model too.
oh man i'm gonna have to forget so much of ResourceSupport
I'd love to keep this focused on actual tickets and actionable items here. For general discussion and questions, please refer to StackOverflow.
Short: yes, all media type specific serialization is based on the RepresentationModel
type hierarchy. This is to make sure we're not interfering with other HttpMessageConverter
and ObjectMapper
instances that might be configured in the system. By deciding to return a RepresentationModel
you sort of opt-in to the hypermedia support.
@drdamour I'm experimenting on a fluent API to build RepresentationModel
objects. What do you think of something like this for building an embedded structure like you depicted up above?
@RestController
static class ProductController {
LinkRelation favoriteProducts = LinkRelation.of("favorite products");
LinkRelation purchasedProducts = LinkRelation.of("purchased products");
@GetMapping("/products")
public RepresentationModel<?> all() {
EmbeddedModelBuilder builder = ModelBuilder //
.embed() //
.rootLink(linkTo(methodOn(ProductController.class).all()).withSelfRel());
PRODUCTS.keySet().stream() //
.map(id -> new EntityModel<>(PRODUCTS.get(id), new Link("http://localhost/products/{id}").expand(id))) //
.forEach(productEntityModel -> {
if (productEntityModel.getContent().isFavorite()) {
builder //
.embed(favoriteProducts) //
.entityModel(productEntityModel) //
.rootLink(productEntityModel.getRequiredLink(SELF).withRel(favoriteProducts));
}
if (productEntityModel.getContent().isPurchased()) {
builder //
.embed(purchasedProducts) //
.entityModel(productEntityModel) //
.rootLink(productEntityModel.getRequiredLink(SELF).withRel(purchasedProducts));
}
});
return builder.build();
}
}
It yields:
{
"_embedded" : {
"favorite products" : [ {
"someProductProperty" : "someValue",
"_links" : {
"self" : {
"href" : "http://localhost/products/777"
}
}
}, {
"someProductProperty" : "someValue",
"_links" : {
"self" : {
"href" : "http://localhost/products/998"
}
}
} ],
"purchased products" : [ {
"someProductProperty" : "someValue",
"_links" : {
"self" : {
"href" : "http://localhost/products/111"
}
}
}, {
"someProductProperty" : "someValue",
"_links" : {
"self" : {
"href" : "http://localhost/products/222"
}
}
}, {
"someProductProperty" : "someValue",
"_links" : {
"self" : {
"href" : "http://localhost/products/333"
}
}
}, {
"someProductProperty" : "someValue",
"_links" : {
"self" : {
"href" : "http://localhost/products/444"
}
}
}, {
"someProductProperty" : "someValue",
"_links" : {
"self" : {
"href" : "http://localhost/products/555"
}
}
}, {
"someProductProperty" : "someValue",
"_links" : {
"self" : {
"href" : "http://localhost/products/666"
}
}
}, {
"someProductProperty" : "someValue",
"_links" : {
"self" : {
"href" : "http://localhost/products/998"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://localhost/products"
},
"purchased products" : [ {
"href" : "http://localhost/products/111"
}, {
"href" : "http://localhost/products/222"
}, {
"href" : "http://localhost/products/333"
}, {
"href" : "http://localhost/products/444"
}, {
"href" : "http://localhost/products/555"
}, {
"href" : "http://localhost/products/666"
}, {
"href" : "http://localhost/products/998"
} ],
"favorite products" : [ {
"href" : "http://localhost/products/777"
}, {
"href" : "http://localhost/products/998"
} ]
}
}
A plain old collection where the embedded link relation is a based on the domain type:
@GetMapping("/authors")
RepresentationModel<?> collection() {
return ModelBuilder //
.collection() //
.entity(new Author("Greg L. Turnquist", null, null)) //
.link(linkTo(methodOn(EmbeddedController.class).authorDetails(1)).withSelfRel())
.link(linkTo(methodOn(EmbeddedController.class).collection()).withRel("authors")) //
.entity(new Author("Craig Walls", null, null)) //
.link(linkTo(methodOn(EmbeddedController.class).authorDetails(2)).withSelfRel())
.link(linkTo(methodOn(EmbeddedController.class).collection()).withRel("authors")) //
.entity(new Author("Oliver Drotbhom", null, null)) //
.link(linkTo(methodOn(EmbeddedController.class).authorDetails(2)).withSelfRel())
.link(linkTo(methodOn(EmbeddedController.class).collection()).withRel("authors")) //
.rootLink(linkTo(methodOn(EmbeddedController.class).collection()).withSelfRel()) //
.build();
}
produces
{
"_embedded" : {
"authors" : [ {
"name" : "Greg L. Turnquist",
"_links" : {
"self" : {
"href" : "http://localhost/author/1"
},
"authors" : {
"href" : "http://localhost/authors"
}
}
}, {
"name" : "Craig Walls",
"_links" : {
"self" : {
"href" : "http://localhost/author/2"
},
"authors" : {
"href" : "http://localhost/authors"
}
}
}, {
"name" : "Oliver Drotbhom",
"_links" : {
"self" : {
"href" : "http://localhost/author/2"
},
"authors" : {
"href" : "http://localhost/authors"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://localhost/authors"
}
}
}
And a single item representation:
@GetMapping("/other-author")
RepresentationModel<?> singleItem() {
return ModelBuilder //
.entity(new Author("Alan Watts", "January 6, 1915", "November 16, 1973")) //
.link(new Link("/people/alan-watts")) //
.build();
}
produces
{
"name" : "Alan Watts",
"born" : "January 6, 1915",
"died" : "November 16, 1973",
"_links" : {
"self" : {
"href" : "/people/alan-watts"
}
}
}
overall it seems fine. some comments pulling from how i've used resources:
ModelBuilder.generateLinksForRel("purchased").fromEmbeddedRel(REL_SELF).embed(thingWithSelfRel)
. what do you do when you do that an the embedding thing has no self rel. if embed took two params as prev suggested, then this would be much better. ModelBuilder.maintainLinksForEmbedded(true).embed(link, embeddedModel)
or ModelBuilder.maintainLinksForEmbedded(false).embed(embeddedModel.getLink(REL_SELF).withRel("purchased"), embeddedModel)
former would keep the _links in hal, latter would ignore them.entity(e).collection().embed(sub1).embed(sub2).???
_embedded: { 'x': []}
This indicates to the client that yes i know you wanted this sub resource...but there just isn't any..vs straight up absence which sometimes implies it just couldn't be fulfilled but go ahead and ask for it via a link. I've tried to get away form this pattern and instead have clients look for the links...but then they crash on NPEs...so i go back to making a service that is weird to help client devs that aren't safe. I think you might need to support such a case...but maybe not necessary in a builder. just a ramble about a useful anit-pattern.that's my immediate feedback.
I've opened a new ticket to focus the design of the representation builder.
@gregturn You'd have the link to the newly opened ticket ?
I guess that’s not visible on your phone.
The Resources type (and any derived types, ie PagedResource) should allow you to name the collection that will eventually be the embedded set. It's auto named now, but the api developer should really have control over this.