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

14,739

Solution 1

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

Solution 2

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"
      }
    ]
  }
Share:
14,739
Desiderantes
Author by

Desiderantes

You either die as a bug or live long enough to see yourself become a feature.

Updated on July 18, 2022

Comments

  • Desiderantes
    Desiderantes almost 2 years

    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.

  • Kamil Nękanowicz
    Kamil Nękanowicz over 7 years
    This is not acceptable. To much boilerplate. One word of advice: org.springframework.http.ResponseEntity generates boilerplate code. You'll end up with these ResponseEntity everywhere in your controller thus making it difficult to read and understand. If you want to handle special cases like errors (Not Found, Conflict, etc.), you can add a HandlerExceptionResolver to your Spring configutation. So in your code, you just throw a specific exception (NotFoundException for instance) and deside what to do in your Handler (setting the HTTP status to 404), making the Controller code more clear.
  • EralpB
    EralpB about 7 years
    "too much boilerplate code" lel it's Java.. add 2 thousand lines of XML half of which is auto-generated to this and now you reach Java's good amount of boilerplate code. to the OP: I liked this approach, I wish more people realized resources(representations) should be separate than the domain model, then you can easily roll out a new version of "Account"
  • Grigory Kislin
    Grigory Kislin over 5 years
    Testes in spring-boot 2.1. Exception is thrown in case of Resource<ManagedEntity> and PagedResources both present in response. See my solution below.