Learn what's new in Project Wonder's ERRest framework. Also, see some tips about security and versioning for your REST services, and learn how you can use HTML routing to build Web apps with ERRest.
2. The Menu
⢠What's new in ERRest ⢠Debugging
⢠Security ⢠Caching (Sunday!)
⢠Versioning ⢠Optimistic locking (Sunday!)
⢠HTML routing ⢠Using the correct HTTP verbs
and codes (Sunday!)
4. Anymous updates
⢠No need to send the ids of nested objects anymore
⢠Call ERXKeyFilter.setAnonymousUpdateEnabled(true)
⢠If 1:N relationship, will replace existing values for all nested
objects
8. Ignoring unknow keys
⢠By default, returns status 500 if unknow attribute is found in
request
⢠To ignore those errors, call:
yourErxKeyFilter.setUnknownKeyIgnored(true)
9. ERXRouteController.performActi
onName
That method have been split in 5 methods to make it easier to
override on the method.
10. ERXRestContext
⢠Hold a userInfo dict + the editing context
⢠Can pass a different date format per controller
⢠Override createRestContext to do that
11. ERXRestContext
public class BlogEntryController extends BaseRestController {
...
@Override
protected ERXRestContext createRestContext() {
ERXRestContext restContext = new ERXRestContext(editingContext());
restContext.setUserInfoForKey("yyyy-MM-dd", "er.rest.dateFormat");
restContext.setUserInfoForKey("yyyy-MM-dd", "er.rest.timestampFormat");
return restContext;
}
}
12. Other new stuff
⢠More strict HTTP status code in responses
⢠Support for @QueryParam, @CookieParam and
@HeaderParam for JSR-311 annotations
⢠Indexed bean properties are supported in bean class descriptions
⢠updateObjectWithFilter will update subobjects
18. Basic Auth
⢠Pros:
⢠99.9% of HTTP clients can work with it
⢠Easy to implement
⢠Cons:
⢠It's just a Base64 representation of your credentials!
⢠No logout option (must close the browser)
⢠No styling of the user/pass box
20. Implementing Basic Auth
@Override
public WOActionResults performActionNamed(String actionName, boolean throwExceptions) {
// This is if you don't want to use Basic Auth for HTML apps
if (!(ERXRestFormat.html().name().equals(this.format().name()))) {
try {
initAuthentication();
} catch (UserLoginException ex) {
WOResponse response = (WOResponse)errorResponse(401);
response.setHeader("Basic realm="ERBlog"", "WWW-Authenticate");
return response;
} catch (NotAuthorizedException ex) {
WOResponse response = (WOResponse)errorResponse(401);
response.setHeader("Basic realm="ERBlog"", "WWW-Authenticate");
return response;
}
}
return super.performActionNamed(actionName, throwExceptions);
}
21. Sessions
⢠Pros:
⢠Can store other data on the server-side (but REST is suppose to be
stateless)
⢠Easy to implement
⢠Cons:
⢠Timeouts...
⢠Sessions are bind to a speciďŹc instance of the app
⢠State on the server
⢠Non-browser clients have to store the session ID
22. Login with a session
curl -X GET http://127.0.0.1/cgi-bin/WebObjects/App.woa/ra/users/login.json?username=auser&password=md5pass
public Session() {
setStoresIDsInCookies(true);
}
public WOActionResults loginAction() throws Throwable {
try {
String username = request().stringFormValueForKey("username");
String password = request().stringFormValueForKey("password");
Member member = Member.validateLogin(session().defaultEditingContext(), username, password);
return response(member, ERXKeyFilter.filterWithNone());
} catch (MemberException ex) {
return errorResponse(401);
}
}
(This only works on a version of ERRest after June 9 2011)
23. Login with a session
protected void initAuthentication() throws MemberException, NotAuthorizedException {
if (context().hasSession()) {
Session session = (Session)context()._session();
if (session.member() == null) {
throw new NotAuthorizedException();
}
} else {
throw new NotAuthorizedException();
}
}
@Override
public WOActionResults performActionNamed(String actionName, boolean throwExceptions) {
try {
initAuthentication();
} catch (MemberException ex) {
return pageWithName(Login.class);
} catch (NotAuthorizedException ex) {
return pageWithName(Login.class);
}
return super.performActionNamed(actionName, throwExceptions);
}
24. Tokens
⢠Pros:
⢠No timeout based on inactivity (unless you want to)
⢠Cons:
⢠More work involved
⢠Client must request a token
⢠Can store the token in a cookie, Authorization header or as a
query argument
25. Login with a token
curl -X GET http://127.0.0.1/cgi-bin/WebObjects/App.woa/ra/members/login.json?username=auser&password=md5pass
public static final ERXBlowfishCrypter crypter = new ERXBlowfishCrypter();
public WOActionResults loginAction() throws Throwable {
try {
String username = request().stringFormValueForKey("username");
String password = request().stringFormValueForKey("password");
Member member = Member.validateLogin(editingContext(), username, password);
String hash = crypter.encrypt(member.username());
if (hash != null) {
return response(hash, ERXKeyFilter.filterWithAll());
}
} catch (MemberException ex) {
return errorResponse(401);
}
}
26. Login with a token
public static final ERXBlowfishCrypter crypter = new ERXBlowfishCrypter();
protected void initTokenAuthentication() throws MemberException, NotAuthorizedException {
String tokenValue = this.request().cookieValueForKey("someCookieKeyForToken");
if (tokenValue != null) {
String username = crypter.decrypt(tokenValue);
Member member = Member.fetchMember(editingContext(), Member.USERNAME.eq(username));
if (member == null) {
throw new NotAuthorizedException();
}
} else {
throw new NotAuthorizedException();
}
}
@Override
public WOActionResults performActionNamed(String actionName, boolean throwExceptions) {
try {
initTokenAuthentication();
} catch (MemberException ex) {
return pageWithName(Login.class);
} catch (NotAuthorizedException ex) {
return pageWithName(Login.class);
}
return super.performActionNamed(actionName, throwExceptions);
}
27. Browser vs System-to-System
It near impossible to have a REST backend with security that
works well with both browsers-based and "system-to-system"
applications.
⢠For browser apps: use cookies
⢠For system-to-system: use the Authorization header
31. Versioning
⢠Try hard to not having to version your REST services...
⢠... but life is never as planiďŹed
⢠Use mod_rewrite and ERXApplication._rewriteURL to make it
easier
⢠Use mod_rewrite even if you are not versionning! It makes
shorter and nicer URLs
32. Versioning
In Apache config:
RewriteEngine On
RewriteRule ^/your-service/v1/(.*)$ /cgi-bin/WebObjects/YourApp-v1.woa/ra$1 [PT,L]
RewriteRule ^/your-service/v2/(.*)$ /cgi-bin/WebObjects/YourApp-v2.woa/ra$1 [PT,L]
In Application.java:
public String _rewriteURL(String url) {
String processedURL = url;
if (url != null && _replaceApplicationPathPattern != null && _replaceApplicationPathReplace != null) {
processedURL = processedURL.replaceFirst(_replaceApplicationPathPattern, _replaceApplicationPathReplace);
}
return processedURL;
}
In the Properties of YourApp-v1.woa:
er.extensions.ERXApplication.replaceApplicationPath.pattern=/cgi-bin/WebObjects/YourApp-v1.woa/ra
er.extensions.ERXApplication.replaceApplicationPath.replace=/your-service/v1/
In the Properties of YourApp-v2.woa:
er.extensions.ERXApplication.replaceApplicationPath.pattern=/cgi-bin/WebObjects/YourApp-v2.woa/ra
er.extensions.ERXApplication.replaceApplicationPath.replace=/your-service/v2/
33. Versioning: the gotcha
Watch out for schema changes or other changes that can break
old versions if all versions use the same database schema!
35. HTML routing?
⢠Power of ERRest + WO/EOF + clean URLs!
⢠Like DirectActions, but with a lot of work done for you
⢠Useful for small public apps that can be cacheable (or accessible
ofďŹine)
36. Automatic routing: damn easy
⢠Create a REST controller for your entity and set
isAutomaticHtmlRoutingEnabled() to true
⢠Create a <EntityName><Action>Page (eg, MemberIndexPage.wo)
component
⢠Register your controller
⢠Your component must implements IERXRouteComponent
⢠Run your app
⢠ProďŹts!
37. Passing data to the component
Use the ERXRouteParameter annotation to tag methods to
receive data:
@ERXRouteParameter
public void setMember(Member member) {
this.member = member;
}
38. Automatic HTML routing
If the <EntityName><Action>Page component is not found, it will default
back to the controller and try to execute the requested method.
39. HTML routing gotchas
⢠When submitting forms, you're back to the stateful request
handler
⢠ERXRouteUrlUtils doesn't create rewritten URLs
40. Manual HTML routing
That's easy: same as a DirectAction:
public WOActionResults indexAction() throws Throwable {
return pageWithName(Main.class);
}
41. 100% REST
public Application() {
ERXRouteRequestHandler restRequestHandler = new ERXRouteRequestHandler();
requestHandler.insertRoute(new ERXRoute("Main","", MainController.class, "index"));
...
setDefaultRequestHandler(requestHandler);
}
public class MainController extends BaseController {
public MainController(WORequest request) {
super(request);
}
@Override
protected boolean isAutomaticHtmlRoutingEnabled() {
return true;
}
@Override
public WOActionResults indexAction() throws Throwable {
return pageWithName(Main.class);
}
@Override
protected ERXRestFormat defaultFormat() {
return ERXRestFormat.html();
}
...
43. Cool trick: Application Cache
Manifest
⢠Let you specify that some URLs of your app can be available
ofďŹine
⢠URLs in the CACHE section will be available ofďŹine until you
change the manifest and remove the URLs from the CACHE
section
⢠Use a DirectAction or a static ďŹle to create the manifest
⢠One cool reason to use the HTML routing stuff
44. Cache Manifest
In your DirectAction class:
public WOActionResults manifestAction() {
EOEditingContext ec = ERXEC.newEditingContext();
WOResponse response = new WOResponse();
response.appendContentString("CACHE MANIFESTn");
response.appendContentString("CACHE:n");
response.appendContentString(ERXRouteUrlUtils.actionUrlForEntityType(this.context(), Entity.ENTITY_NAME, "index",
ERXRestFormat.HTML_KEY, null, false, false) + "n");
response.appendContentString("NETWORK:n");
response.setHeader("text/cache-manifest", "Content-Type");
return response;
}
In your component:
<wo:WOGenericContainer elementName="html" manifest=$urlToManifest" lang="en" xmlns="http://www.w3.org/1999/xhtml">
public String urlForManifest() {
return this.context().directActionURLForActionNamed("manifest", null);
}
46. Debugging REST problems
⢠curl -v : will display all headers and content, for both request and
response
⢠Firebug and WebKit Inspector : useful to see the
XMLHttpRequest calls
⢠tcpďŹow : see all traďŹc on a network interface, can do ďŹlters
⢠Apache DUMPIO (mod_dumpio) : dump ALL requests and
responses data to Apache's error log