Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring-boot + REST + HATEOAS + HAL

I followed the spring.io Pivotal tutorials to get a REST API with a MySQL DB off the ground and things are progressing nicely. However I found a behavior that I haven't been able to configure or work around.

When I use the built-in functionality to retrieve my Resources from the PagingAndSortingRepository, the resulting REST is automatically paged and encapsulated with useful HAL links (_links, self, search, linked Resources, etc). I want to use that.

When I implemented my Controller to customize the PostMapping behavior and introduce sanity checks, validation and etc, the GetMapping stopped working. So I re-implemented a GetMapping that leveraged my Service layer.

Doing so unfortunately broke the HATEOAS that was previously provided.

What I would like is to be able to customize the PostMapping, but retain the GetMapping exactly like the default. If possible I would love to avoid having to write it myself since I know the framework can supply it.

Any way to do that?

Controller:

@RestController
public class PartyMemberController {

    @Autowired
    PartyMemberService partyMemberService;

    @RequestMapping(method = RequestMethod.GET, value = "/partyMembers")
    public ResponseEntity<Iterable<PartyMember>> getAllPartyMembers() {
        Iterable<PartyMember> partyMemberList = partyMemberService.getAll();
        return new ResponseEntity<>(partyMemberList, HttpStatus.OK);
    }

    @RequestMapping(method = RequestMethod.POST, value = "/partyMembers")
    public ResponseEntity<PartyMember> addEmployee(@Valid @RequestBody PartyMember partyMember) {
        if (partyMemberService.exists(partyMember)) {
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        }

        partyMember = partyMemberService.save(partyMember);
        return new ResponseEntity<PartyMember>(partyMember, HttpStatus.CREATED);
    }
}

Resulting JSON

    [
      {
        "id": 2,
        "ward": {
          "id": 1,
          "name": "Mercier",
          "wardNumber": 42,
          "numberOfMembers": 3
        },
        "firstName": "Cindy",
        "lastName": "Tremblay",
        "partyMemberId": "12-1234-09876",
        "primaryPhone": "514-555-2323",
        "postalAddress": "1155 Robert-Bourassa, Montreal, Quebec, Canada, H3B3A7",
        "emailAddress": null,
        "secondaryPhone": null,
        "bestTimeToContact": null,
        "bestWayToContact": null,
        "membershipExpiry": null,
        "dateOfBirth": null,
        "donationEntries": [],
        "note": null
      },
      {
        "id": 3,
        "ward": {
          "id": 1,
          "name": "Mercier",
          "wardNumber": 42,
          "numberOfMembers": 3
        },
        "firstName": "Robert",
        "lastName": "Paulson",
        "partyMemberId": "12-1234-54321",
        "primaryPhone": "514-555-1212",
        "postalAddress": "440 Rue Saint-Pierre, App 5, Montreal, Quebec, Canada, H2Y2M5",
        "emailAddress": "[email protected]",
        "secondaryPhone": null,
        "bestTimeToContact": null,
        "bestWayToContact": null,
        "membershipExpiry": null,
        "dateOfBirth": null,
        "donationEntries": [],
        "note": null
      },
      {
        "id": 4,
        "ward": {
          "id": 1,
          "name": "Mercier",
          "wardNumber": 42,
          "numberOfMembers": 3
        },
        "firstName": "Richard",
        "lastName": "Schnobb",
        "partyMemberId": "12-4321-09876",
        "primaryPhone": "514-555-2323",
        "postalAddress": "440 Rue Saint-Pierre, App 5, Montreal, Quebec, Canada, H2Y2M5",
        "emailAddress": null,
        "secondaryPhone": null,
        "bestTimeToContact": null,
        "bestWayToContact": null,
        "membershipExpiry": null,
        "dateOfBirth": null,
        "donationEntries": [],
        "note": null
      }
    ]

Default JSON (Notice the _embedded, _links, etc). This what I want to have as a result.

{
  "_embedded" : {
    "partyMembers" : [ {
      "firstName" : "Cindy",
      "lastName" : "Tremblay",
      "partyMemberId" : "12-1234-09876",
      "primaryPhone" : "514-555-2323",
      "postalAddress" : "1155 Robert-Bourassa, Montreal, Quebec, Canada, H3B3A7",
      "emailAddress" : null,
      "secondaryPhone" : null,
      "bestTimeToContact" : null,
      "bestWayToContact" : null,
      "membershipExpiry" : null,
      "dateOfBirth" : null,
      "donationEntries" : [ ],
      "note" : null,
      "_links" : {
        "self" : {
          "href" : "http://127.0.0.1:8080/partyMembers/2"
        },
        "partyMember" : {
          "href" : "http://127.0.0.1:8080/partyMembers/2"
        },
        "ward" : {
          "href" : "http://127.0.0.1:8080/partyMembers/2/ward"
        }
      }
    }, {
      "firstName" : "Robert",
      "lastName" : "Paulson",
      "partyMemberId" : "12-1234-54321",
      "primaryPhone" : "514-555-1212",
      "postalAddress" : "440 Rue Saint-Pierre, App 5, Montreal, Quebec, Canada, H2Y2M5",
      "emailAddress" : "[email protected]",
      "secondaryPhone" : null,
      "bestTimeToContact" : null,
      "bestWayToContact" : null,
      "membershipExpiry" : null,
      "dateOfBirth" : null,
      "donationEntries" : [ ],
      "note" : null,
      "_links" : {
        "self" : {
          "href" : "http://127.0.0.1:8080/partyMembers/3"
        },
        "partyMember" : {
          "href" : "http://127.0.0.1:8080/partyMembers/3"
        },
        "ward" : {
          "href" : "http://127.0.0.1:8080/partyMembers/3/ward"
        }
      }
    }, {
      "firstName" : "Richard",
      "lastName" : "Schnobb",
      "partyMemberId" : "12-4321-09876",
      "primaryPhone" : "514-555-2323",
      "postalAddress" : "440 Rue Saint-Pierre, App 5, Montreal, Quebec, Canada, H2Y2M5",
      "emailAddress" : null,
      "secondaryPhone" : null,
      "bestTimeToContact" : null,
      "bestWayToContact" : null,
      "membershipExpiry" : null,
      "dateOfBirth" : null,
      "donationEntries" : [ ],
      "note" : null,
      "_links" : {
        "self" : {
          "href" : "http://127.0.0.1:8080/partyMembers/4"
        },
        "partyMember" : {
          "href" : "http://127.0.0.1:8080/partyMembers/4"
        },
        "ward" : {
          "href" : "http://127.0.0.1:8080/partyMembers/4/ward"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://127.0.0.1:8080/partyMembers{?page,size,sort}",
      "templated" : true
    },
    "profile" : {
      "href" : "http://127.0.0.1:8080/profile/partyMembers"
    },
    "search" : {
      "href" : "http://127.0.0.1:8080/partyMembers/search"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 3,
    "totalPages" : 1,
    "number" : 0
  }
}
like image 890
ETN Avatar asked Oct 18 '18 22:10

ETN


1 Answers

To keep Spring doing most of the work for you, you should use the annotation @RepositoryRestController. More explanations in the docs:

Sometimes, you may want to write a custom handler for a specific resource. To take advantage of Spring Data REST’s settings, message converters, exception handling, and more, use the @RepositoryRestController annotation instead of a standard Spring MVC @Controller or @RestController. Controllers annotated with @RepositoryRestController are served from the API base path defined in RepositoryRestConfiguration.setBasePath, which is used by all other RESTful endpoints (for example, /api).


In order to simplify the generation of HATEOAS resources (with the _links you mentioned) you can take advantage of ResourceAssemblerSupport.

As the mapping from an entity to a resource type will have to be used in multiple places it makes sense to create a dedicated class responsible for doing so. The conversion will of course contain very custom steps but also a few boilerplate ones. (...) Spring Hateoas now provides a ResourceAssemblerSupport base class that helps reducing the amount of code needed to be written

like image 72
Gustavo Passini Avatar answered Sep 22 '22 00:09

Gustavo Passini