Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Giving Spring Some REST: Craig Walls Twitter: @habuma @springsocial

Download as pdf or txt
Download as pdf or txt
You are on page 1of 53

Giving Spring some REST

Craig Walls
craig@habuma.com
Twitter: @habuma @springsocial
http://github.com/habuma

REST in One Slide

Resources (aka, the things)


Representations
HTTP Methods (aka, the verbs)
URIs and URLs

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Why REST?
Key piece of the Modern Application puzzle
More APIs / Fewer pages
Humans and browsers consume pages
Everything can consume APIs
(incl. browsers, JS, mobile apps, other apps,etc)

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Springs REST Story


Spring MVC 3.0+
Spring HATEOAS
Spring Security for OAuth (S2OAuth)
Spring RestTemplate
Spring Social
Spring Data REST
Spring REST Shell
WebSocket/STOMP Support (Spring 4)
Spring Boot
Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Creating REST APIs

Lets write a simple REST controller...


@Controller
@RequestMapping("/books")
public class BooksController {
!
! private BookRepository bookRepository;
!
!
!
!

@Inject
public BooksController(BookRepository bookRepository) {
! this.bookRepository = bookRepository;! !
}

!
!
!
!
!
}

@RequestMapping(method=RequestMethod.GET)
public @ResponseBody List<Book> allBooks() {
! return bookRepository.findAll();
}

GET http://host/app/books
Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

How Does @ResponseBody Work?

How does it know how to write it to the


response?

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Message Converters
StringHttpMessageConverter
Reads text/* into String; writes String into text/plain
FormHttpMessageConverter
Reads/writes application/x-www-form-urlencoded from/to MultiValueMap<String,String>
ByteArrayMessageConverter
Reads */* into byte[]; writes Object as application/octet-stream
Jaxb2RootElementHttpMessageConverter
Reads/writes text/xml or application/xml from/to JAXB-annotated objects
MappingJacksonHttpMessageConverter /
MappingJackson2HttpMessageConverter
Reads/writes application/json from/to Objects
SourceHttpMessageConverter
Reads/writes text/xml/application/xml from/to javax.xml.transform.Source
ResourceHttpMessageConverter
Reads/writes org.springframework.core.io.Resource objects
[AtomFeed|RssChannel]HttpMessageConverter
Reads/writes Rome Feed and RssChannels (application/atom+xml | rss+xml)
Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Lets Add Another Endpoint

@Controller
@RequestMapping("/books")
public class BooksController {
!
...
!
!
!
!
!
}

@RequestMapping(value="/{id}", method=RequestMethod.GET)
public @ResponseBody Book bookById(@PathVariable("id") long id) {
! return bookRepository.findOne(id);
}

GET http://host/app/books/{id}
Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

What About POSTing Resources?

@Controller
@RequestMapping("/books")
public class BooksController {
!
...
!
!
!
!

@RequestMapping(method=RequestMethod.POST)
public @ResponseBody Book postBook(@RequestBody Book book) {
! return bookRepository.save(book);
}!

POST http://host/app/books
Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Can We PUT a Resource?


@Controller
@RequestMapping("/books")
public class BooksController {
!
...
!
!
!
!

@RequestMapping(value="/{id}" method=RequestMethod.PUT)
public void updateBook(@PathVariable("id") long id,
@RequestBody Book book) {
! bookRepository.save(book);
}

PUT http://host/app/books/{id}
Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Deleting a Resource

@Controller
@RequestMapping("/books")
public class BooksController {
!
...
!
!
!
!

@RequestMapping(value="{id}", method=RequestMethod.DELETE)
public void deleteBook(@PathVariable("id") long id) {
! bookRepository.delete(id);
}

DELETE http://host/app/books/{id}
Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Theres More Than the Resource...


@Controller
@RequestMapping("/books")
public class BooksController {
!
...
!
!
!
!
!
}

@RequestMapping(value="/{id}", method=RequestMethod.GET)
public @ResponseBody Book bookById(@PathVariable("id") long id) {
! return bookRepository.findOne(id);
}

What will happen if findById() returns null?


What should happen?
Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Theres More Than the Resource...


@Controller
@RequestMapping("/books")
public class BooksController {
!
...
!
!
!
!

@RequestMapping(method=RequestMethod.POST)
public @ResponseBody Book postBook(@RequestBody Book book) {
! return bookRepository.save(book);
}!

What will the HTTP status code be?


What should it be?
Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Returning a ResponseEntity
@Controller
@RequestMapping("/books")
public class BooksController {
!
...
!
!

!
!
}

@RequestMapping(value="/{id}", method=RequestMethod.GET)
public ResponseEntity<?> bookById(@PathVariable("id") long id) {
Book book = bookRepository.findOne(id);
if (book != null) {
return new ResponseEntity<Book>(book, HttpStatus.OK);
} else {
Error error = new Error("Book with ID " + id + " not found");
return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
}
}

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Returning a ResponseEntity
@Controller
@RequestMapping("/books")
public class BooksController {
!
...
!
!

!
!

@RequestMapping(value="/{id}", method=RequestMethod.GET)
public ResponseEntity<Book> bookById(@PathVariable("id") long id) {
Book book = bookRepository.findOne(id);
if (book != null) {
return new ResponseEntity<Book>(book, HttpStatus.OK);
}
throw new BookNotFoundException(id);
}
@ExceptionHandler(BookNotFoundException.class)
public ResponseEntity<Error> bookNotFound(BookNotFoundException e) {
Error error = new Error("Book with ID " + id + " not found");
return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
}

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Returning a ResponseEntity
@Controller
@RequestMapping("/books")
public class BooksController {
!
...
!
!
!
!
!
!
!
!
!

@RequestMapping(method=RequestMethod.POST)
public ResponseEntity<Book> postBook(@RequestBody Book book) {
! Book newBook = bookRepository.save(book);
! ResponseEntity<Book> bookEntity =
new ResponseEntity<Book>(newBook, HttpStatus.CREATED);
! String locationUrl =
ServletUriComponentsBuilder.fromCurrentContextPath().
! ! ! path("/books/" + newBook.getId()).build().toUriString();
! bookEntity.getHeaders().setLocation(URI.create(locationUrl));
! return bookEntity;
}

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Linking Resources

HATEOAS
Hypermedia As The Engine Of Application State
Responses carry links to related endpoints
API is self-descriptive
Client can learn about the API

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Self-Describing API
{
"links" : [
{
"rel" : "self",
"href" : "http://localhost:8080/BookApp/books/5"
},
{
"rel" : "all",
"href" : "http://localhost:8080/BookApp/books/"
},
{
"rel" : "author",
"href" : "http://localhost:8080/BookApp/authors/2"
}
],
"id" : 5,
"title" : "Spring in Action",
...
}

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Defining a Resource

public class BookResource extends ResourceSupport {


...
}

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Adding Links to a Resource


@RequestMapping(value="/{id}", method=RequestMethod.GET)
public ResponseEntity<BookResource> bookById(@PathVariable("id") long id) {
Book book = bookRepository.findOne(id);
if (book != null) {
BookResource resource = bookResourceAssembler.toResource(book);
resource.add(ControllerLinkerBuilder.linkTo(BooksController.class)
.withRel("all"));
resource.add(ControllerLinkerBuilder.linkTo(AuthorsController.class)
.slash(book.getAuthor().getId());
.withRel("author");
return new ResponseEntity<BookResource>(resource, HttpStatus.OK);
}
throw new BookNotFoundException(id);
}

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Assembling a Resource
public class BookResourceAssembler
extends ResourceAssemblerSupport<Book, BookResource> {
public BookResourceAssembler() {
super(BooksController.class, BookResource.class);
}
public BookResource toResource(Book book) {
return createResource(book);
}
public BookResource instantiateResource(Book book) {
return new BookResource(book);
}
}

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Asynchronous Controllers

Spring + WebSocket + STOMP


Spring 4.0.0.M1: Low-level WebSocket support
Spring 4.0.0.M2: Higher-level, STOMP support
Messaging, not request-handling
STOMP: Simple Text Oriented Messaging Protocol

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Handling messages
@Controller
public class PortfolioController {
...
!
!
!
!
!

@MessageMapping(value="/app/trade")
public void executeTrade(Trade trade, Principal principal) {
! trade.setUsername(principal.getName());
! this.tradeService.executeTrade(trade);
}
...

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Handling subscriptions
@Controller
public class PortfolioController {
...
!
!

@SubscribeEvent("/app/positions")
public List<PortfolioPosition> getPortfolios(Principal principal)
throws Exception {

Portfolio portfolio =
this.portfolioService.findPortfolio(principal.getName());

!
!

!
}

return portfolio.getPositions();

...
}

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Handling message exceptions


@Controller
public class PortfolioController {
...
!
!
!
!
!

@MessageExceptionHandler
@ReplyToUser(value="/queue/errors")
public String handleException(Throwable exception) {
! return exception.getMessage();
}
...

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

On the client side: Receiving messages


var socket = new SockJS('/spring-websocket-portfolio/portfolio');
var stompClient = Stomp.over(socket);
...
stompClient.subscribe("/app/positions", function(message) {
self.portfolio().loadPositions(JSON.parse(message.body));
});
stompClient.subscribe("/topic/price.stock.*", function(message) {
self.portfolio().processQuote(JSON.parse(message.body));
});
stompClient.subscribe("/queue/errors" + queueSuffix, function(message) {
self.pushNotification("Error " + message.body);
});

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

On the client side: Sending messages

var socket = new SockJS('/spring-websocket-portfolio/portfolio');


var stompClient = Stomp.over(socket);
...
var trade = {
"action" : self.action(),
"ticker" : self.currentRow().ticker,
"shares" : self.sharesToTrade()
};
stompClient.send("/app/trade", {}, JSON.stringify(trade));

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Configuring STOMP in Spring


Well...there are a lot of beans...
It gets better in 4.0.0.RC1 (see SPR-10835)...
But for now...

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Securing REST APIs

OAuth

An open standard for


authorization

Supported by Facebook,

Twitter, LinkedIn, TripIt,


Salesforce, and dozens more

Puts the user in control of

what resources are shared


http://oauth.net
Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

The Many Versions of OAuth


OAuth 1.0
TripIt, NetFlix, DropBox, Gliffy, MySpace, ...

OAuth 1.0a
Twitter, LinkedIn, Evernote, Flickr,Yammer,Yelp!, ...

OAuth 2
Facebook, Foursquare, Google, GitHub, Instagram, ...

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

OAuth 2

Much simpler than OAuth 1.0(a)


No more request token
Leverages HTTPS instead of encrypting the token
No signature or canonicalization of the request
Much simpler Authorization header
Scoped authorization
Short-lived tokens, long-lived authorization
Separate roles of authorization server/resource server
Multiple grant types
Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Authorization Code Grant

Like OAuth 1.0 flow

Starts with redirect to provider


for authorization

After authorization, redirects


back to client with code query
parameter

Code is exchanged for access


token

Client must be able to keep


tokens confidential

Commonly used for web


apps
Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Implicit Grant

Simplified authorization flow

After authorization, redirects


back to client with access
token in fragment parameter

Reduced round-trips
No refresh token support
Commonly used by inbrowser JavaScript apps or
widgets

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Resource Owner Credentials Grant

Directly exchanges users


credentials for an access
token

Useful where the client is


well-trusted by the user and
where a browser redirect
would be awkward

Commonly used with mobile


apps

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Client Credentials Grant

Directly exchanges the


clients credentials for an
access token

For accessing client-owned


resources (with no user
involvement)

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

The OAuth 2 Authorization Header


Much simpler than OAuth 1.0(a)
Differs across OAuth 2 drafts...
Drafts 14-current
Authorization: Bearer e139a950-2fc5-4822-9266-8a2b572108c5
Drafts 12-13
Authorization: BEARER e139a950-2fc5-4822-9266-8a2b572108c5
Draft 10
Authorization: OAuth e139a950-2fc5-4822-9266-8a2b572108c5
Drafts 1-9
Authorization: Token token=e139a950-2fc5-4822-9266-8a2b572108c5

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

OAuth Provider Responsibilities

Authorization server

If supporting authorization code and/or implicit grant, must


serve an authorization page

Support an authorization endpoint for all supported grant


types

Not obligated to support all grant types

Validate access tokens on requests to resource endpoints

Produce and manage tokens

Resource server
Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Spring Security OAuth (S2OAuth)

Based on Spring Security


Declarative model for OAuth

Provider-side support for authorization endpoints, token


management, and resource-level security

Also offers client-side OAuth

Implemented for both OAuth 1 and OAuth 2


http://www.springsource.org/spring-security-oauth
Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Key Pieces of S2OAuth

Authorization Server

Implemented as Spring MVC controller

Implemented as servlet filters

Handles /oauth/authorize and /oauth/token endpoints

Resource Server

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Configuring the Authorization Server


<oauth:authorization-server
client-details-service-ref="clientDetails"
token-services-ref="tokenServices">
<oauth:authorization-code />
<oauth:implicit />
<oauth:refresh-token />
<oauth:client-credentials />
<oauth:password />
</oauth:authorization-server>

<oauth:client-details-service id="clientDetails">
<oauth:client client-id="tonr" secret="secret"
resource-ids="sparklr"
authorized-grant-types="authorization_code,implicit"
authorities="ROLE_CLIENT"
scope="read,write" />
</oauth:client-details-service>

<bean id="tokenServices"
class="o.sf.security.oauth2.provider.token.DefaultTokenServices">
<property name="tokenStore" ref="tokenStore" />
<property name="supportRefreshToken" value="true" />
<property name="clientDetailsService" ref="clientDetails"/>
</bean>

<bean id="tokenStore"
class="o.sf.security.oauth2.provider.token.InMemoryTokenStore" />

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Configuring the Authorization Server


<http pattern="/oauth/token"
create-session="stateless"
authentication-manager-ref="clientAuthenticationManager"
entry-point-ref="oauthAuthenticationEntryPoint"
xmlns="http://www.springframework.org/schema/security">
<intercept-url pattern="/oauth/token"
access="IS_AUTHENTICATED_FULLY" />
! <anonymous enabled="false" />
! <http-basic entry-point-ref="oauthAuthenticationEntryPoint" />
</http>

<authentication-manager id="clientAuthenticationManager"
xmlns="http://www.springframework.org/schema/security">
! <authentication-provider user-service-ref="clientDetailsUserService" />
</authentication-manager>

<bean id="clientDetailsUserService"
class="o.sf.security.oauth2.provider.client.ClientDetailsUserDetailsService">
<constructor-arg ref="clientDetails" />
</bean>

<bean id="oauthAuthenticationEntryPoint"
class="o.sf.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
<property name="realmName" value="sparklr2" />
</bean>

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Configuring the Resource Server


<http pattern="/photos/**"
create-session="never"
entry-point-ref="oauthAuthenticationEntryPoint"
!
access-decision-manager-ref="accessDecisionManager"
!
xmlns="http://www.springframework.org/schema/security">
! <anonymous enabled="false" />
!
!
!
!

<intercept-url pattern="/photos" access="ROLE_USER,SCOPE_READ" />


<intercept-url pattern="/photos/trusted/**"
access="ROLE_CLIENT,SCOPE_TRUST" />
<intercept-url pattern="/photos/user/**" access="ROLE_USER,SCOPE_TRUST" />
<intercept-url pattern="/photos/**" access="ROLE_USER,SCOPE_READ" />

! <custom-filter ref="resourceServerFilter" before="PRE_AUTH_FILTER" />


</http>

<bean id="accessDecisionManager"
class="org.springframework.security.access.vote.UnanimousBased"
xmlns="http://www.springframework.org/schema/beans">
<constructor-arg>
<list>
<bean class="o.sf.security.oauth2.provider.vote.ScopeVoter" />
<bean class="o.sf.security.access.vote.RoleVoter" />
<bean class="o.sf.security.access.vote.AuthenticatedVoter" />
</list>
</constructor-arg>
</bean>

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Consuming REST APIs

RestTemplate

Handles boilerplate HTTP connection code


Keeps your focus on the resources

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Using RestTemplate
RestTemplate restTemplate = new RestTemplate();
Book book = new Book("Spring in Action", "Gregg Walls");
Book newBook = restTemplate.postForObject(
"http://host/app/books", book, Book.class);
book = restTemplate.getForObject(
"http://host/app/books/{id}", Book.class, newBook.getId());
book.setAuthor("Craig Walls");
restTemplate.put("http://host/app/books/{id}",
Book.class, book.getId());
restTemplate.delete("http://host/app/books/{id}",
Book.class, book.getId());

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Tweeting with RestTemplate


RestTemplate rest = new RestTemplate();
MultiValueMap<String, Object> params =
new LinkedMultiValueMap<String, Object>();
params.add("status", "Hello Twitter!");
rest.postForObject("https://api.twitter.com/1/statuses/update.json",
params, String.class);

Oh no!
WARNING: POST request for "https://api.twitter.com/1/statuses/update.json" resulted in 401 (Unauthorized); invoking error
handler
org.springframework.web.client.HttpClientErrorException: 401 Unauthorized
!
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:75)
!
at org.springframework.web.client.RestTemplate.handleResponseError(RestTemplate.java:486)
!
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:443)
!
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:401)
!
at org.springframework.web.client.RestTemplate.postForObject(RestTemplate.java:279)

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Configuring Spring Social


@Configuration
@EnableJdbcConnectionRepository
@EnableTwitter(appId="${twitter.consumerKey}", appSecret="${twitter.consumerSecret}")
@EnableFacebook(appId="${facebook.clientId}", appSecret="${facebook.clientSecret}")
public class SocialConfig {
!
! @Inject
! private ConnectionFactoryLocator connectionFactoryLocator;
!
! @Inject
! private ConnectionRepository connectionRepository;
!
!
!
!

@Bean
public ConnectController connectController() {
! return new ConnectController(connectionFactoryLocator, connectionRepository);
}

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Configuring Spring Social

<facebook:config app-id="${facebook.clientId}"
app-secret="${facebook.clientSecret}"
app-namespace="socialshowcase" />
<twitter:config app-id="${twitter.consumerKey}"
app-secret="${twitter.consumerSecret}"/>
<social:jdbc-connection-repository/>!
<bean id="connectController"
class="org.springframework.social.connect.web.ConnectController"
autowire="constructor" />

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Tweeting with Spring Social


public class TwitterTimelineController {
!
!
!
!
!
!

private final Twitter twitter;

!
!
!
!
!

@RequestMapping(value="/twitter/tweet", method=RequestMethod.POST)
public String postTweet(String message) {
! twitter.timelineOperations().updateStatus(message);
! return "redirect:/twitter";
}

@Inject
public TwitterTimelineController(Twitter twitter) {
! this.twitter = twitter;
}

Email: craig@habuma.com

Twitter: @habuma

Blog: http://www.springinaction.com

Sample Code: http://github.com/habuma

Thank you!
REST Books Sample (a work in progress)
https://github.com/habuma/rest-books

Spring HATEOAS

https://github.com/SpringSource/spring-hateoas

Spring Security for OAuth (S2OAuth)

http://www.springsource.org/spring-security-oauth

Spring Social

http://www.springsource.org/spring-security-oauth

Spring Data REST

http://www.springsource.org/spring-data/rest

Spring REST Shell

https://github.com/SpringSource/rest-shell

You might also like