Click here to Skip to main content
13,195,379 members (66,050 online)
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

4.8K views
1 bookmarked
Posted 9 May 2016

Don’t Hate the HATEOAS Part Deux: Springtime for HATEOAS

, 9 May 2016
Rate this:
Please Sign up or sign in to vote.
How to implement HATEOAS using Spring-Data-REST and Spring-HATEOAS

In the much belated conclusion to my series on HATEOAS, we will be diving into how to implement HATEOAS using Spring-Data-REST and Spring-HATEOAS. It is springtime for HATEOAS!

I put together a functioning project that will demonstrate the code examples I have below as well as a few other features. The project can be found at https://github.com/in-the-keyhole/hateoas-demo-II. JDK 8 and Maven are required, but otherwise no external dependencies are needed to run the project.

Serving a Resource

Interacting with a web service via its resources is one of the core design constraints of REST. Using Spring-Data and Spring-MVC, it is not too difficult to start serving up a resource. You’ll need to add a Repository for the entity you want to serve and implement a controller to serve it. Spring-Data-REST however, makes this process even easier and provides a richer resource in the process (i.e., adding hypermedia markup).

@RepositoryRestResource
public interface ItemRepo extends CrudRepository<Item, Long> {
}

And it is as simple as that. If you boot up your Spring-Boot app and navigate to http://localhost:8080/items (and have done some of the other necessary configurations as well), you should get JSON returns that looks something like this:

{
  "_embedded" : {
    "items" : [ {
      "name" : "Independence Day",
      "description" : "Best. Movie. Speech. Ever!",
      "price" : 10.0,
      "type" : "Movies",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/items/21"
        },
        "item" : {
          "href" : "http://localhost:8080/api/items/21"
        }
      }
    },
	...
	]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/items/"
    },
    "profile" : {
      "href" : "http://localhost:8080/profile/items"
    },
    "search" : {
      "href" : "http://localhost:8080/items/search"
    }
  }
}

Along with the easy-to-demonstrate GET functionality, Spring-Data-REST also adds the ability to PUT (Spring-Data-REST for some reason decided to use PUT for both create and update) and DELETE a resource, as well as retrieve a resource by its ID. This is a lot of functionality for just two lines of code!

Pagination and Sorting

Resources will often have a lot of records. Typically, you would not want to return all of those records on request due to the high resource cost at all levels. Pagination is a frequently used solution to address this issue and Spring-Data-REST makes it extremely easy to implement.

Another common need is the ability to allow clients to sort the returns from a resource, and here again Spring-Data-REST is to the rescue. To implement this functionality with the Item resource, we need to change from extending a CrudRepository to a PagingAndSortingRepository like so:

@RepositoryRestResource
public interface ItemRepo extends PagingAndSortingRepository<Item, Long> {
}

When restarting the application and returning to http://localhost:8080/items, our returns initially look the same, but near the bottom of the page, we see some new JSON objects:

{
  ...    
  "_links" : {
    "first" : {
      "href" : "http://localhost:8080/items?page=0&size=20"
    },
    "self" : {
      "href" : "http://localhost:8080/items"
    },
    "next" : {
      "href" : "http://localhost:8080/items?page=1&size=20"
    },
    "last" : {
      "href" : "http://localhost:8080/items?page=1&size=20"
    },
    "profile" : {
      "href" : "http://localhost:8080/profile/items"
    },
    "search" : {
      "href" : "http://localhost:8080/items/search"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 23,
    "totalPages" : 2,
    "number" : 0
  }
}

Spring-Data-REST renders hypermedia controls for navigating through the pages of the returns for a resource; last, next, prev, and first when applicable (note: Spring-Data-REST is using a 0-based array for pagination). If you look closely, you will also notice how Spring-Data-REST allows the client to manipulate the number of returns per page (.../items?size=x). Finally, sorting has also been added and can be accomplished with URL parameters: .../items?sort=name&name.dir=desc.

Searching a Resource

So we are serving a resource, paginating the returns, and allowing clients to sort those returns. These are all very useful, but often clients will want to search a specific subset of a resource. This is another task Spring-Data-REST makes extremely simple.

@RepositoryRestResource
public interface ItemRepo extends PagingAndSortingRepository<Item, Long> {

	List<Item> findByType(@Param("type") String type);

	@RestResource(path = "byMaxPrice")
	@Query("SELECT i FROM Item i WHERE i.price <= :maxPrice")
	List<Item> findItemsLessThan(@Param("maxPrice") double maxPrice);

	@RestResource(path = "byMaxPriceAndType")
	@Query("SELECT i FROM Item i WHERE i.price <= :maxPrice AND i.type = :type")
	List<Item> findItemsLessThanAndType(@Param("maxPrice") 
		double maxPrice, @Param("type") String type);
}

Above are a few queries that users may want to search items by: an item’s type, the max price of an item, and then those two parameters combined. Navigating to http://localhost:8080/items/search, Spring-Data-REST renders all the search options available as well as how to interact with them. The pagination and sorting functionality available at the root resource endpoint is enabled when interacting with the search endpoints as well!

...
    "findItemsLessThan" : {
      "href" : "http://localhost:8080/items/search/byMaxPrice{?maxPrice}",
      "templated" : true
    },
    "findByType" : {
      "href" : "http://localhost:8080/items/search/findByType{?type}",
      "templated" : true
    },
    "findItemsLessThanAndType" : {
      "href" : "http://localhost:8080/items/search/byMaxPriceAndType{?maxPrice,type}",
      "templated" : true
    },
...

Changing the Shape of a Resource

There will be times when it is beneficial to change the shape of an entity an endpoint serves; you may want to flatten out an object tree, hide fields, or change the name of fields to maintain a contract. Spring-Data-REST offers the functionality to manipulate the shape with projections.

First, we will need to create an interface and annotate it with @Projection:

@Projection(name = "itemSummary", types = { Item.class })
public interface ItemSummary {
	String getName();
	String getPrice();
}

This will allow Spring-Data-REST to serve our Item entity in the ItemSummary shape upon request: http://localhost:8080/api/items/1?projection=itemSummary. If we want to make ItemSummary the default shape, we return when hitting the /items endpoint that can be accomplished by adding the excerptProjection to the @RepositoryRestResource annotation on ItemRepo.

@RepositoryRestResource(excerptProjection = ItemSummary.class)
public interface ItemRepo extends PagingAndSortingRepository<Item, Long> {

Now when we hit ../items, our returns look like this:

...
{
      "name" : "Sony 55 TV",
      "price" : "1350.0",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/items/2"
        },
        "item" : {
          "href" : "http://localhost:8080/api/items/2{?projection}",
          "templated" : true
        }
      }
}
...

Customizing a Resource’s Endpoint

The name of an entity may not always be desirable as the name of a resource’s endpoint; it may not conform to legacy needs, you may need to prefix a resource’s endpoint, or simply a different name is wanted. Spring-Data-REST offers hooks for all of these needs.

For changing a resource’s name:

@RepositoryRestResource(collectionResourceRel = "merchandise", path = "merchandise")
public interface ItemRepo extends PagingAndSortingRepository<Item, Long> {
}

And adding a base path:

@Configuration
public class RestConfiguration extends RepositoryRestConfigurerAdapter {

	@Override
	public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
		config.setBasePath("api");
	}
}

Now instead of Item entities being served at ../items, they will be served from ../api/merchandise.

Securing a Resource

Security is a very important and complex topic. Even entire talks barely scratch the surface. So consider this portion a minor abrasion on the subject.

Hiding Fields

As mentioned in the previous section, projections are one way of hiding fields. Another, more secure, way is to use @JsonIgnore on a field like below to prevent it from being returned:

public class Item implements Serializable, Identifiable<Long> {
	@JsonIgnore
	@Column(name = "secret_field")
	private String secretField;
}

Restricting Access over HTTP

There might be cases where functionality should not be accessible over HTTP at all, no matter who you are. That can be accomplished with @RestResource(exported = false), which tells Spring-Data-REST not to publish that resource or portion of resource at all to the web. This can be set at both the Type and Method level. The Type level can also be overridden at the Method level if you want to broadly deny but then explicitly define what should be accessible.

Method level:

public interface OrderRepo extends CrudRepository<Order, Long> {

	@Override
	@RestResource(exported = false)
	<S extends Order> S save(S entity);
}

Type level, with method level override:

@RestResource(exported = false)
public interface OrderRepo extends CrudRepository<Order, Long> {

	@Override
	@RestResource(exported = true)
	<S extends Order> S save(S entity);
}

An alternative method (if you so desire) is to instead extend the Repository interface and only define the methods you want clients to have access to.

public interface PaymentRepo extends Repository<Payment, Long> {
	Payment findOne(Long id);

	<S extends Payment> S save(S entity);
}

Restricting Access by Role

You might also want to limit functionality to only certain types of users.

@RepositoryRestResource(collectionResourceRel = "merchandise", path = "merchandise")
public interface ItemRepo extends PagingAndSortingRepository<Item, Long> {
	@PreAuthorize("hasRole('ADMIN')")
	<S extends Item> S save(S entity);

	@PreAuthorize("hasRole('ADMIN')")
	<S extends Item> Iterable<S> save(Iterable<S> entities);
}

While I don’t think it is strictly required, due to some funky interaction possibly with Spring-MVC filters, some additional URL configuration is required to get role-based security working. (I spent many hours researching this issue.) However, implementing multiple layers of security is generally a good practice anyways, so this isn’t necessarily wrong either:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
	@Override
	@Autowired
	public void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN")//
				.and().withUser("user").password("password").roles("USER");
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.csrf().disable()
				.antMatcher("/merchandise").authorizeRequests().antMatchers
					(HttpMethod.POST).hasAnyRole("ADMIN")//
				.and().antMatcher("/merchandise").authorizeRequests().antMatchers
					(HttpMethod.PUT).hasAnyRole("ADMIN")//
				.and().antMatcher("/**").authorizeRequests().antMatchers
					(HttpMethod.DELETE).denyAll()//
				.and().antMatcher("/merchandise").authorizeRequests().antMatchers
					(HttpMethod.GET).permitAll()//
				.and().antMatcher("/**").authorizeRequests().anyRequest().authenticated()
				.and().httpBasic();
	}
}

Like @RestResource, @PreAuthorize can also be placed at the Type level and overridden at the Method level.

@PreAuthorize("hasRole('USER')")
public interface OrderRepo extends CrudRepository<Order, Long> {
}

Additional Customization with Spring-HATEOAS

Up to this point, I have demonstrated all the features of Spring-Data-REST and how it makes implementing a HATEOAS service a breeze. Alas, there are limits to what you can do with Spring-Data-REST. Luckily, there is another Spring project, Spring-HATEOAS, to take up the ground from there.

Spring-HATEOAS eases the process of adding hypermedia markup to a resource and is useful for handling custom interactions between resources. For example, adding an item to an order:

@RequestMapping("/{id}")
public ResponseEntity<Resource<Item>> viewItem(@PathVariable String id) {
  Item item = itemRepo.findOne(Long.valueOf(id));
  
  Resource<Item> resource = new Resource<Item>(item);
  if (hasExistingOrder()) {
  	// Provide a link to an existing Order
	resource.add(entityLinks.linkToSingleResource(retrieveExistingOrder()).withRel("addToCart"));
  } else {
  	// Provide a link to create a new Order
  	resource.add(entityLinks.linkFor(Order.class).withRel("addToCart"));
  }
  resource.add(entityLinks.linkToSingleResource(item).withSelfRel());
  return ResponseEntity.ok(resource);
}

With this, we have overwritten the default /merchandise/(id) functionality that Spring-Data-REST provides and will now return this result:

{
  "name" : "Samsung 55 TV",
  "description" : "Samsung 55 LCD HD TV",
  "price" : 1500.0,
  "type" : "Electronics",
  "_links" : {
    "addToCart" : {
      "href" : "http://localhost:8080/api/orders"
    },
    "self" : {
      "href" : "http://localhost:8080/api/merchandise/1{?projection}",
      "templated" : true
    }
  }
}

So our client code can now render a link allowing a user to easily add an item to their cart or create a new cart and add an item to it.

Conclusions

HATEOAS is an often overlooked portion of the REST specification, mostly because it can be quite time consuming to implement and maintain. Spring-Data-REST and Spring-HATEOAS greatly reduce both the time to implement and time to maintain, making HATEOAS much more practical to implement in your RESTful service.

I was only able to touch on some of the features Spring-Data-REST and Spring-HATEOAS have to offer. For a full description of their respective feature set, I recommend checking out the reference docs linked below. If you have any questions or need further explanation, please feel free to ask to in the comments section below.

Additional Resources

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Keyhole Software
Keyhole Software
United States United States
This member doesn't quite have enough reputation to be able to display their biography and homepage.
Group type: Organisation

3 members


You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 3 Pin
Member 1236439010-May-16 21:33
memberMember 1236439010-May-16 21:33 
Praise:) Pin
Member 125126869-May-16 9:31
memberMember 125126869-May-16 9:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.171019.1 | Last Updated 9 May 2016
Article Copyright 2016 by Keyhole Software
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid