21

I have the following controller method:

@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE, value = "session/{id}/exercises")
public ResponseEntity<Resources<Exercise>> exercises(@PathVariable("id") Long id) {

  Optional<Session> opt = sessionRepository.findWithExercises(id);
  Set<Exercise> exercises = Sets.newLinkedHashSet();

  if (opt.isPresent()) {
    exercises.addAll(opt.get().getExercises());
  }

  Link link = entityLinks.linkFor(Session.class)
                         .slash(id)
                         .slash(Constants.Rels.EXERCISES)
                         .withSelfRel();

  return ResponseEntity.ok(new Resources<>(exercises, link));
}

So basically I am trying to get the expose a Set<> of Exercise entities for a particular Session. When the exercises entity is empty however I get a JSON representation like this:

{
    "_links": {
        "self": {
            "href": "http://localhost:8080/api/sessions/2/exercises"
        }
    }
}

So basically there is no embedded entity, while something like the following would be preferrable:

{
    "_links": {
        "self": {
            "href": "http://localhost:8080/api/sessions/2/exercises"
        }
    }, 
    "_embedded": {
        "exercises": [] 
    }    
}

any idea how to enforce this?

Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
ChrisGeo
  • 3,807
  • 13
  • 54
  • 92

4 Answers4

19

The problem here is that without additional effort there's no way to find out that the empty collection is a collection for Exercise. Spring HATEOAS has a helper class to work around this though:

EmbeddedWrappers wrappers = new EmbeddedWrappers(false);
EmbeddedWrapper wrapper = wrappers.emptyCollectionOf(Exercise.class);
Resources<Object> resources = new Resources<>(Arrays.asList(wrapper));

An EmbeddedWrapper allows you to explicitly mark objects to be added to the Resource or Resources as embedded, potentially even manually defining the rel they should be exposed under. As you can see above the helper also allows you to add an empty collection of a given type to the _embedded clause.

Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
  • 3
    This loses generic type information (and it's not the only case when assembling resources). I think it should be `EmbeddedWrapper implements Resource`, but Resource is no interface, and that's the root of the problems with generics in Spring HATEOAS, in my opinion. – Andrea Ratto Dec 17 '15 at 08:33
  • 1
    For PagedResources result: `List embedded = Collections.singletonList(wrapper); PagedResources pagedResources = new PagedResources(embedded, metadata, links);` – leo Apr 06 '16 at 15:26
  • @ccit-spence yes, [take a look](http://stackoverflow.com/a/36455266/343802) – leo Sep 19 '16 at 16:06
  • 2
    I also stumbled upon this, I can make it work with using Object instead of my type for embeddedWrapper but I really don't like it. Why isn't this an option or the default? When using spring data rest an empty collection is the default as well. – jgeerts Nov 12 '16 at 21:27
  • 1
    @Oliver Gierke rather than this ugly workaround, why not extend the constructor instead? `new Resources<>(Exercise.class, exercises, ...);` is acceptable for me. – Hendy Irawan Dec 09 '16 at 14:13
  • 2
    Please vote for https://github.com/spring-projects/spring-hateoas/issues/522 (just click the "add your reaction" button) – Hendy Irawan Dec 09 '16 at 14:21
  • 2
    I am not happy with this workaround either. It requires some generics acrobatics to make the compiler happy. I find it odd that CrudRepository does include the empty _embedded json block while in the controller this workaround is needed to achieve the same result. – onnoweb Feb 12 '18 at 19:04
3

One can use the PagedResourceAssembler::toEmptyResource() method. For example, the following works:

Page<EWebProduct> products = elasticSearchTemplate.queryForPage(query, EWebProduct.class);

if(!products.hasContent()){
            PagedResources pagedResources = pageAssembler.toEmptyResource(products, WebProductResource.class,baseLink);
            return new ResponseEntity<PagedResources<WebProductResource>>(pagedResources, HttpStatus.OK);
}

I'd bet it works with other ResourceAssemblers as well.

mancini0
  • 4,285
  • 1
  • 29
  • 31
1

If you have a Page< T >, you can convert it like this:

 public static <T> PagedModel<EntityModel<T>> toModel(PagedResourcesAssembler<T> assembler,
                                                Page<T> page) {
        if (!page.isEmpty()) {
            return assembler.toModel(page);
        } else {
            // toEmptyModel renders the _embedded field (with an empty array inside)
            return (PagedModel<EntityModel<T>>) assembler.toEmptyModel(page, TenantSubscriptionResponseDto.class);
        }
    }

(You can obtain the PagedResourcesAssembler assembler by simply adding it as a parameter to the Controller method, and Spring will inject it).

Vlad Dinulescu
  • 1,173
  • 1
  • 14
  • 24
-1

Spring by default uses Jackson parser to serialize/deserialize json. As per http://wiki.fasterxml.com/JacksonFeaturesSerialization Jackson has a feature called WRITE_EMPTY_JSON_ARRAYS and its enabled by default. Maybe WRITE_EMPTY_JSON_ARRAYS is set to false in your config. please recheck your message converters configuration.

Thilak
  • 656
  • 7
  • 15