Skip to content

Name embedded collections #175

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

Closed
drdamour opened this issue May 5, 2014 · 38 comments
Closed

Name embedded collections #175

drdamour opened this issue May 5, 2014 · 38 comments
Assignees

Comments

@drdamour
Copy link

drdamour commented May 5, 2014

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.

@odrotbohm
Copy link
Member

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.

@odrotbohm odrotbohm self-assigned this May 18, 2014
@drdamour
Copy link
Author

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.

@fiddlerpianist
Copy link
Contributor

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.

@williewheeler
Copy link

+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": [ ... ]
}

@gjrwebber
Copy link

+1

@marvinrichter
Copy link

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"
    }]
}

@odrotbohm
Copy link
Member

As documented in the reference documentation. 😄

@marvinrichter
Copy link

lol. I totally missed the point 3. 😄

@zhengl7
Copy link

zhengl7 commented Jan 12, 2016

+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.

@odrotbohm
Copy link
Member

The HAL specification.

@stephaneeybert
Copy link

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.

@marvinrichter
Copy link

@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 itemfor 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.

@stephaneeybert
Copy link

@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.

@marvinrichter
Copy link

marvinrichter commented Sep 2, 2018

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.

@gregturn
Copy link
Contributor

gregturn commented Sep 2, 2018

Given the project supports both annotation overrides and writing your own RelProvider, I’m closing this issue.

@gregturn gregturn closed this as completed Sep 2, 2018
@drdamour
Copy link
Author

drdamour commented Sep 2, 2018

@gregturn the specific scenario I outlined in
#175 (comment) remains unsupported and relprovided nor annotations supports it. can we please reopen

@stephaneeybert
Copy link

@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 ?

@gregturn
Copy link
Contributor

gregturn commented Sep 2, 2018

Again, using EvoInflectorRelProvider as a guide, write your own. Or use @relation.

@drdamour
Copy link
Author

drdamour commented Sep 3, 2018

@gregturn as outlined in #175 (comment) 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.

@drdamour
Copy link
Author

drdamour commented Sep 3, 2018

@stephaneeybert what is missing from #175 (comment) ?

@stephaneeybert
Copy link

@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 ?

@drdamour
Copy link
Author

drdamour commented Sep 4, 2018

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

@gregturn
Copy link
Contributor

gregturn commented Sep 4, 2018

RelProvider and the annotation give you the power to name the collection.

Link rels you control.

Therefore you can do all this.

@drdamour
Copy link
Author

drdamour commented Sep 4, 2018

@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?

@odrotbohm
Copy link
Member

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"
    } ]
  }
}

@drdamour
Copy link
Author

drdamour commented Mar 5, 2019

but my problem is the reverse...two getters of the SAME type with different rels.

@drdamour
Copy link
Author

drdamour commented Mar 5, 2019

looks like EmbeddedWrappers would work for that case too now. cool.

@odrotbohm
Copy link
Member

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?

@drdamour
Copy link
Author

drdamour commented Mar 6, 2019

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!

@drdamour
Copy link
Author

drdamour commented Mar 6, 2019

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

@odrotbohm
Copy link
Member

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.

@gregturn
Copy link
Contributor

gregturn commented Mar 8, 2019

@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"
    } ]
  }
}

@gregturn
Copy link
Contributor

gregturn commented Mar 8, 2019

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"
    }
  }
}

@gregturn
Copy link
Contributor

gregturn commented Mar 8, 2019

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"
    }
  }
}

@drdamour
Copy link
Author

overall it seems fine. some comments pulling from how i've used resources:

  • i've learned from past mistakes to avoid nested embeds when possible, but does rootLink go all the way to the root, or the immediate parent...you'll probably want both. i'd be looking for .parent() and .root() more than rootLink() or parentLink()..especailly that self case on the collection, rootLink is weird there cause wy would i add a self to the root in the context of a subresource except in the case of a literal construction...in which case i'd much rather call link(new SelfLink()) before calling .collection()
  • I think of it as a special case of HAL that things are embedded with rel only. There was a poster on hal forums who asked why we don't embed by url, something that is better for compatibility with http2 and uri templates (you could embed common template completions) that i thought was pretty smart. fundamentally a sub resource can only be embedded by way of some hypermedia control, a link being the easiest by far, but templated links not far behind. Maybe it goes against a pattern of builders to have two params, but to me, i must embed a sub resource with a link together in pairs. so i'd have embed take two params, the entity and the link to embed it with (which in hal ONLY cares about the rel...but in others may care about urls or even more). But i get how that could be a pretty abstract concept, and if someone just wants to build HAL they'd question why they have to have a whole link with href just to output a rel string in the serialization.
  • rel's distancing themselves from order has always been weird to me...i get that item doesn't imply order, but is the order those rels appear in the document important? i've always thought so..and in practice it's necessary to work that way (cause order happens to matter to UIs/UX..a lot). As such, i'd be a bit concerned over the way the embed and root are added/removed in pairs in different methods to maintain order. I'd want that to be less manual and prefer it to be more an option of the builder which would do the linking from self. but even thinking of what to name that would be awful... 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.
  • for the collection case, how do you set properties on the collection? I guess you start with entity() but then how do you get back to the entity? entity(e).collection().embed(sub1).embed(sub2).???
  • it's come up a few times where i want to embed an empty set. _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.

@gregturn
Copy link
Contributor

I've opened a new ticket to focus the design of the representation builder.

@stephaneeybert
Copy link

@gregturn You'd have the link to the newly opened ticket ?

@gregturn
Copy link
Contributor

I guess that’s not visible on your phone.

#864.

odrotbohm added a commit that referenced this issue May 7, 2020
HalModelBuilder expose HAL-idiomatic API to set up representations. That includes embeds, previews and syntactic sugar around the inclusion of potentially empty collections as embeds.

Related tickets: #175, #193, #270, #920.
Original pull request: #1273.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants