Enable HAL serialization in Spring Boot for custom controller method
Asked Answered
O

3

12

I'm trying to build a RESTful API with Spring Boot using spring-boot-starter-data-rest. There are some entities: accounts, transactions, categories and users - just the usual stuff.

When I retrieve the objects at http://localhost:8080/transactions via the API that has been generated by default, all is going well an I get a list with all transactions as JSON objects like that one:

{
  "amount": -4.81,
  "date": "2014-06-17T21:18:00.000+0000",
  "description": "Pizza",
  "_links": {
    "self": {
      "href": "http://localhost:8080/transactions/5"
    },
    "category": {
      "href": "http://localhost:8080/transactions/5/category"
    },
    "account": {
      "href": "http://localhost:8080/transactions/5/account"
    }
  }
}

But now the goal is to retrieve only the latest transactions under that URL since I don't want to serialize the whole database table. So I wrote a Controller:

@Controller
public class TransactionController {
    private final TransactionRepository transactionRepository;

    @Autowired
    public TransactionController(TransactionRepository transactionRepository) {
        this.transactionRepository = transactionRepository;
    }

    // return the 5 latest transactions
    @RequestMapping(value = "/transactions", method = RequestMethod.GET)
    public @ResponseBody List<Transaction> getLastTransactions() {
        return  transactionRepository.findAll(new PageRequest(0, 5, new Sort(new Sort.Order(Sort.Direction.DESC, "date")))).getContent();
    }
}

When I now try to access http://localhost:8080/transactions there's a

java.lang.IllegalStateException: Cannot call sendError() after the response has been committed

because of the circular reference between users and accounts. When I solve this by adding a @JsonBackReference annotation to the account list in User, I can retrieve the transaction list but only with this "classic" format:

{
  "id": 5,
  "amount": -4.5,
  "date": "2014-06-17T21:18:00.000+0000",
  "description": "Pizza",
  "account": {
    "id": 2,
    "name": "Account Tilman",
    "owner": {
      "id": 1,
      "name": "Tilman"
    },
    "categories": [
      {
        "id": 1,
        "name": "Groceries"
      },
      {
        "id": 2,
        "name": "Restaurant"
      }
    ],
    "users": [
      {
        "id": 1,
        "name": "Tilman"
      }
    ]
  },
  "category": {
    "id": 2,
    "name": "Restaurant"
  }
}

No HAL links anymore, everything is getting serialized directly by jackson. I tried adding

@EnableHypermediaSupport(type = HypermediaType.HAL)

to the entity classes but that didn't get me anywhere. I just want my controller to return the same objects that the generated API does, with HAL _links instead of every reference being serialized. Any thoughts?

EDIT: OK, after thinking twice I realized that the @EnableHypermediaSupport annotation has to be added to the configuration, of course. This solves the problem of the circular references and I can remove the @JsonBackReference from User. But only the attributes of the object itself are being serialized, there is no _links section:

{
    "amount": -4.81,
    "date": "2014-06-17T21:18:00.000+0000",
    "description": "Pizza"
}

I know that I could write wrapper classes extending ResourceSupport for all my entities but this seems rather pointless. As spring-hateoas is able to magically generate the representations with the _link section for the REST interface that is created automatically there should be a way to return the same representations from a custom controller, right?

Omsk answered 1/8, 2015 at 6:7 Comment(0)
G
27

There's a lot of aspects here:

  1. I doubt that the collection resource at /transactions really returns an individual transaction as you described. Those representations are returned for item resources.

  2. If TransactionRepository already is a PageableAndSortingRepository the collection resource can be tweaked by expanding the URI template exposed in the API root for the link named transactions. By default that's a page, size and sort parameter. That means clients can request what you want to expose already.

  3. If you want to default the paging and sorting options, implementing a controller is the correct way. However, to achieve a representation like Spring Data REST exposes you need to return at least instances of ResourceSupport as this is the type the HAL mapping is registered for.

    There's nothing magically here if you think about it. A plain entity does not have any links, the ResourcesSupport and types like Resource<T> allow you to wrap the entity and enrich it with links as you see fit. Spring Data REST basically does that for you using a lot of the knowledge about the domain and repository structure that's available implicitly. You can reuse a lot of as shown below.

    There are a few helper you need to be aware of here:

    • PersistentEntityResourceAssembler - which is usually injected into the controller method. It renders a single entity in a Spring Data REST way, which means that associations pointing to managed types will be rendered as links etc.
    • PagedResourcesAssembler - usually injected into the controller instance. Takes care of preparing the items contained in the page, optionally by using a dedicated ResourceAssembler.

    What Spring Data REST basically does for pages is the following:

    PersistentEntityResourceAssembler entityAssembler = …;
    Resources<?> … = pagedResourcesAssembler.toResources(page, entityAssembler);
    

    That's basically using the PagedResourcesAssembler with the PersistentEntityResourceAssembler to render the items.

    Returning that Resources instance should give you the representation design you expected.

Gamache answered 3/8, 2015 at 7:48 Comment(8)
That was very helpful, thanks! Is the ResourcesSupport documented anywhere?Heroine
Yes: docs.spring.io/spring-hateoas/docs/current/reference/html/…Gamache
@OliverGierke Thank you very much, indeed! About your comments: (1.) Yes, the resource returns a list, of course. I just shortened the output in order to illustrate the format. (2.) Yes, I have a PagingAndSortingRepository but as you guessed, I want to (3.) provide and take away the possibility for the client to query the entire database table at once.Omsk
@OliverGierke Thanks again! It might be me, but I never considered looking for a Spring HATEOAS documentation. There is a footnote in the Spring Data REST documentation which I discovered just now. Maybe add a small section à la "If you want to create your own controller that integrates with HATEAOS, return ResourcesSupport and for details have a look at the Spring HATEAOS documentation"?Heroine
@Omsk You could also disable the export of the 'findAll()' method by annotating it with @RestResource(exported = false) and still use the query method.Heroine
@Heroine - Great idea! Care to file a ticket with our JIRA?Gamache
I'm having trouble to inject PersistentEntityResourceAssembler, it gives me "IllegalArgumentException: entities is marked @NonNull but is null". The same behavior was described in this questionImitative
PersistentEntityResourceAssembler no longer works in Boot 2.3.x. It's current method toModel or toFullResource is producing a recursion error.Boner
H
3

You do not need to create your own controller to limit query results or sort the results. Just create a query method in your repository:

public interface TransactionRepository extends MongoRepository<Transaction, String> {

    List<Transaction> findFirst10ByOrderByDateDesc();

}

Spring Data REST will automatically export it as a method resource at /transactions/search/findFirst10ByOrderByDateDesc.

Heroine answered 2/8, 2015 at 21:7 Comment(1)
Thanks, exactly what I was looking for! (The method name has to be findFirst10ByOrderByDateDesc() which makes the method resource available at localhost:8080/transactions/search/findFirst10ByOrderByDateDesc)Omsk
C
2

To use PersistentEntityResourceAssembler in the controller we should mark it as @RepositoryRestController

@RestController
@RequestMapping("/categories")
@RepositoryRestController
public class CategoryController implements ValidableController {

// dependencies

@RequestMapping(method = POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<PersistentEntityResource> create(@Valid @RequestBody CategoryForm category,
                                                       BindingResult validation,
                                                       PersistentEntityResourceAssembler resourceAssembler)
{
    validate(validation);
    Category entity = categoryConverter.convert(category);
    entity = categoryService.save(entity);
    return ResponseEntity.ok(resourceAssembler.toFullResource(entity));
}

It builds pretty nice HAL styled response

{
"createdTime": "2018-07-24T00:55:32.854",
"updatedTime": "2018-07-24T00:55:32.855",
"name": "cfvfcdfgdfdfdfs32",
"options": [
    "aaa",
    "bbb"
],
"_links": {
    "self": {
        "href": "http://localhost:8080/shop/categories/34"
    },
    "category": {
        "href": "http://localhost:8080/shop/categories/34{?projection}",
        "templated": true
    },
    "products": {
        "href": "http://localhost:8080/shop/categories/34/products"
    },
    "categories": {
        "href": "http://localhost:8080/shop/categories/34/categories{?projection}",
        "templated": true
    },
    "parent": {
        "href": "http://localhost:8080/shop/categories/34/parent{?projection}",
        "templated": true
    }
}

}

Curvet answered 23/7, 2018 at 21:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.