Desiderantes
Desiderantes

Reputation: 159

How can I simply add a link to a Spring Data REST Entity

I have my Entities with Spring Data JPA, but to generate stats about them, I use jOOQ in a Spring @Repository.

Since my methods return either a List of entities, or a Double, how can I expose them as links? Let's say I have a User entity, I want to get the following JSON:

{
  "_embedded" : {
    "users" : [ ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/api/users"
    },
    "stats" : {
      "href" : "http://localhost:8080/api/users/stats"
    }
    "profile" : {
      "href" : "http://localhost:8080/api/profile/users"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 0,
    "totalPages" : 0,
    "number" : 0
  }
} 

And in http://localhost:8080/api/users/stats I want to get a list of links with the methods I declared in my jOOQ repository. How would I approach this? Thanks.

Upvotes: 10

Views: 18087

Answers (4)

Jess
Jess

Reputation: 3715

https://docs.spring.io/spring-hateoas/docs/current/reference/html/#reference

    public class PaymentProcessor implements RepresentationModelProcessor<EntityModel<Order>> {
        @Override
        public EntityModel<Order> process(EntityModel<Order> model) {
            model.add(
                    Link.of("/payments/{orderId}").withRel(LinkRelation.of("payments")) //
                            .expand(model.getContent().getOrderId()));
            return model;
        }
    }

migrate-to-1.0.changes

ResourceSupport is now RepresentationModel

Resource is now EntityModel

Resources is now CollectionModel

PagedResources is now PagedModel

Upvotes: 2

Grigory Kislin
Grigory Kislin

Reputation: 17990

For manually create links see spring-hateoas-examples. Simplest wai is via new Resource if no DTO and extends ResourceSupport for DTO.

Links to spring-data-rest managed entity I've customized similar to links to root resource:

MyController implements ResourceProcessor<Resource<ManagedEntity>> {

   @Override
   public Resource<Restaurant> process(Resource<ManagedEntity> resource) {
       resource.add(linkTo(methodOn(MyController.class)
           .myMethod(resource.getContent().getId(), ...)).withRel("..."));
       return resource;
}

And for paged resourses

MyController implements ResourceProcessor<PagedResources<Resource<ManagedEntity>>>

The problem is when you need both, as yor cannot extends both this interface due to generic type erasure. As a hack I've created dummy ResourceController

Upvotes: 0

Najeeb Arif
Najeeb Arif

Reputation: 402

The best way to add links is to consider Spring-HATEOAS, which makes code look even cleaner.

One word of advice: Always use org.springframework.http.ResponseEntity for returning response to clients as it allows easy customisation of response.

So as your requirement is to send links in the response, then for this best practice suggested is to use a type of ResourceSupport(org.springframework.hateoas.ResourceSupport) and ResourceAssemblerSupport(org.springframework.hateoas.mvc.ResourceAssemblerSupport) to create resources which needs to be sent to the client.

For Example: If you have a model object like Account then there must be few fields which you would not like the client to know about or to be included in the responce so to exclude those attributes from response we can use ResourceAssemblerSupport class'

public TResource toResource(T t);

method to generate the resource from a model object which needs to be sent as response.

For instance we have a an Account Class like (Can be directly used for all server side interaction and operations)

@Document(collection = "Accounts_Details")

public class Account {

    @Id
    private String id;

    private String username;
    private String password;
    private String firstName;
    private String lastName;
    private String emailAddress;
    private String role;
    private boolean accountNonExpired;
    private boolean accountNonLocked;
    private boolean credentialsNonExpired;
    private boolean enabled;
    private long accountNonLockedCounter;
    private Date lastPasswordResetDate;
    private Address address;
    private long activationCode;

    public Account() {
    }

    //getters and setters
}

Now from This POJO we will create a Resource object which will be sent to the client with selected attributes.

For this We will create a Account resource which will include only the necessary fields which are viewable to client. and do that we create another class.

@XmlRootElement

public class AccountResource extends ResourceSupport {

    @XmlAttribute
    private String username;
    @XmlAttribute
    private String firstName;
    @XmlAttribute
    private String lastName;
    @XmlAttribute
    private String emailAddress;
    @XmlAttribute
    private Address address;
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    public String getEmailAddress() {
        return emailAddress;
    }
    public void setEmailAddress(String emailAddress) {
        this.emailAddress = emailAddress;
    }
    public Address getAddress() {
        return address;
    }
    public void setAddress(Address address) {
        this.address = address;
    }


}

So now this resource is what the client will see or have to work with.

After creating the blueprint of the AccountResource we need a way to convert our Model POJO to this resource and for that the suggested best practice is to create a ResourceAssemblerSupport Class and override the toResource(T t) method.

import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.hateoas.mvc.ResourceAssemblerSupport;
import org.springframework.stereotype.Component;

import com.brx.gld.www.api.controller.RegistrationController;
import com.brx.gld.www.api.model.Account;

@Component
public class AccountResourceAssembler extends ResourceAssemblerSupport<Account, AccountResource> {

    public AccountResourceAssembler(Class<RegistrationController> controllerClass,
            Class<AccountResource> resourceType) {
        super(controllerClass, resourceType);
    }

    public AccountResourceAssembler() {
        this(RegistrationController.class, AccountResource.class);
    }

    @Override
    public AccountResource toResource(Account account) {
        AccountResource accountResource =  instantiateResource(account); //or createResourceWithId(id, entity) canbe used which will automatically create a link to itself.
        accountResource.setAddress(account.getAddress());
        accountResource.setFirstName(account.getFirstName());
        accountResource.setLastName(account.getLastName());
        accountResource.setEmailAddress(account.getEmailAddress());
        accountResource.setUsername(account.getUsername());
        accountResource.removeLinks();
        accountResource.add(ControllerLinkBuilder.linkTo(RegistrationController.class).slash(account.getId()).withSelfRel());
        return accountResource;
    }

}

In the toReource Method instead of using instanriateReource(..) we must use createdResourceWithId(id, entity) and then add the custum links to the resorce, which infact is again a best practice to consider, but for the sake of demonstration i have used instantiateResource(..)

Now to use this in Controller :

@Controller
@RequestMapping("/api/public/accounts")
public class RegistrationController {

    @Autowired
    private AccountService accountService;

    @Autowired
    private AccountResourceAssembler accountResourceAssembler;

    @RequestMapping(method = RequestMethod.GET)
    public ResponseEntity<List<AccountResource>> getAllRegisteredUsers() {
        List<AccountResource> accountResList = new ArrayList<AccountResource>();
        for (Account acnt : accountService.findAllAccounts())
            accountResList.add(this.accountResourceAssembler.toResource(acnt));
        return new ResponseEntity<List<AccountResource>>(accountResList, HttpStatus.OK);
    }

/*Use the below method only if you have enabled spring data web Support or otherwise instead of using Account in @PathVariable usr String id or int id depending on what type to id you have in you db*/

    @RequestMapping(value = "{userID}", method = RequestMethod.GET)
    public ResponseEntity<AccountResource>  getAccountForID(@PathVariable("userID") Account fetchedAccountForId) {
        return new ResponseEntity<AccountResource>(
                this.accountResourceAssembler.toResource(fetchedAccountForId), HttpStatus.OK);
    }

To enable Spring Data Web support which adds few more funcationality to yo code like automatically fetching model data from DB based on the id passed like we used in the previous method.

Now returning to the toResource(Account account) method: in this first the resource object is initialised and then the desired props are set and then the links are added to the AccountResorce by using the static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo(..) method and then the controller class is passed in from which it picks teh base url and and after that the url is built using slash(..) and so on so forth. After the complete path is specified we use the rel method to specify the relation(like here we used withSelfRel() to specify the relation to be it self. For others relations we can use withRel(String relation) to be more descriptive. So in our code in the toResource method we used something like accountResource.add(ControllerLinkBuilder.linkTo(RegistrationController.class).slash(account.getId()).withSelfRel());

which will build the URL as /api/public/accounts/{userID}

Now in postman if we use a get on this url http://localhost:8080/api/public/accounts

{
    "username": "Arif4",
    "firstName": "xyz",
    "lastName": "Arif",
    "emailAddress": "[email protected]",
    "address": {
      "addressLine1": "xyz",
      "addressLine2": "xyz",
      "addressLine3": "xyz",
      "city": "xyz",
      "state": "xyz",
      "zipcode": "xyz",
      "country": "India"
    },
    "links": [
      {
        "rel": "self",
        "href": "http://localhost:8080/api/public/accounts/5628b95306bf022f33f0c4f7"
      }
    ]
  },
  {
    "username": "Arif5",
    "firstName": "xyz",
    "lastName": "Arif",
    "emailAddress": "[email protected]",
    "address": {
      "addressLine1": "xyz",
      "addressLine2": "xyz",
      "addressLine3": "xyz",
      "city": "xyz",
      "state": "xyz",
      "zipcode": "xyz",
      "country": "India"
    },
    "links": [
      {
        "rel": "self",
        "href": "http://localhost:8080/api/public/accounts/5628c04406bf23ea911facc0"
      }
    ]
  }

click on any of the link and send the get request the response will be http://localhost:8080/api/public/accounts/5628c04406bf23ea911facc0

{
    "username": "Arif5",
    "firstName": "xyz",
    "lastName": "Arif",
    "emailAddress": "[email protected]",
    "address": {
      "addressLine1": "xyz",
      "addressLine2": "xyz",
      "addressLine3": "xyz",
      "city": "xyz",
      "state": "xyz",
      "zipcode": "xyz",
      "country": "India"
    },
    "links": [
      {
        "rel": "self",
        "href": "http://localhost:8080/api/public/accounts/5628c04406bf23ea911facc0"
      }
    ]
  }

Upvotes: 5

Evgeni Dimitrov
Evgeni Dimitrov

Reputation: 22506

See this from docs

@Bean
public ResourceProcessor<Resource<Person>> personProcessor() {

   return new ResourceProcessor<Resource<Person>>() {

     @Override
     public Resource<Person> process(Resource<Person> resource) {

      resource.add(new Link("http://localhost:8080/people", "added-link"));
      return resource;
     }
   };
}

Upvotes: 8

Related Questions