errest
DESCRIPTION
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.TRANSCRIPT
![Page 1: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/1.jpg)
MONTREAL 1/3 JULY 2011
ERRestPascal RobertConatus/MacTI
![Page 2: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/2.jpg)
• What's new in ERRest
• Security
• Versioning
• HTML routing
• Debugging
• Caching (Sunday!)
• Optimistic locking (Sunday!)
• Using the correct HTTP verbs and codes (Sunday!)
The Menu
![Page 3: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/3.jpg)
What's New in ERRest
![Page 4: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/4.jpg)
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
![Page 5: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/5.jpg)
Anonymous updateprotected ERXKeyFilter filter() {
ERXKeyFilter filter = ERXKeyFilter.filterWithAttributes(); ERXKeyFilter personFilter = ERXKeyFilter.filterWithAttributes(); personFilter.include(Person.FIRST_NAME); personFilter.setAnonymousUpdateEnabled(true); filter.include(BlogEntry.PERSON, personFilter); return filter; }
curl -X PUT -d "{ title: 'New Post', person: {firstName: 'Test'} }" http://127.0.0.1/cgi-bin/WebObjects/SimpleBlog.woa/ra/posts/23.json
![Page 6: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/6.jpg)
Sort ordering on 1:N
• You can now sort a 1:N relationship
• Call ERXKeyFilter.setSortOrderings()
![Page 7: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/7.jpg)
Sort ordering ERXKeyFilter filter = ERXKeyFilter.filterWithAttributes();
ERXKeyFilter categoryFilter = ERXKeyFilter.filterWithAttributes(); categoryFilter.setSortOrderings(BlogCategory.SHORT_NAME.ascs());
filter.include(BlogEntry.CATEGORIES, categoryFilter);
![Page 8: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/8.jpg)
Ignoring unknow keys
• By default, returns status 500 if unknow attribute is found in request
• To ignore those errors, call:
yourErxKeyFilter.setUnknownKeyIgnored(true)
![Page 9: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/9.jpg)
ERXRouteController.performActionName
That method have been split in 5 methods to make it easier to override on the method.
![Page 10: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/10.jpg)
ERXRestContext
• Hold a userInfo dict + the editing context
• Can pass a different date format per controller
• Override createRestContext to do that
![Page 11: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/11.jpg)
ERXRestContextpublic 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; }}
![Page 12: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/12.jpg)
• 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
Other new stuff
![Page 13: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/13.jpg)
Security
![Page 14: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/14.jpg)
What other REST services uses?
• Twitter and Google: OAuth
• Amazon S3: signature
• Campaign Monitor: Basic Authentication
• MailChimp: API Key
![Page 15: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/15.jpg)
Security
• Basic Authentification
• Sessions
• Tokens
![Page 16: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/16.jpg)
USE SSL!
![Page 17: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/17.jpg)
Basic Auth
![Page 18: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/18.jpg)
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
![Page 19: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/19.jpg)
Implementing Basic Auth
protected void initAuthentication() throws MemberException, NotAuthorizedException { String authValue = request().headerForKey( "authorization" ); if( authValue != null ) { try { byte[] authBytes = new BASE64Decoder().decodeBuffer( authValue.replace( "Basic ", "" ) ); String[] parts = new String( authBytes ).split( ":", 2 ); String username = parts[0]; String password = parts[1]; setAuthenticatedUser(Member.validateLogin(editingContext(), username, password)); } catch ( IOException e ) { log.error( "Could not decode basic auth data: " + e.getMessage() ); e.printStackTrace(); } } else { throw new NotAuthorizedException(); } } public class NotAuthorizedException extends Exception { public NotAuthorizedException() { super(); } }
![Page 20: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/20.jpg)
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); }
![Page 21: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/21.jpg)
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 specific instance of the app
• State on the server
• Non-browser clients have to store the session ID
![Page 22: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/22.jpg)
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)
![Page 23: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/23.jpg)
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); }
![Page 24: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/24.jpg)
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
![Page 25: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/25.jpg)
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); } }
![Page 26: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/26.jpg)
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); }
![Page 27: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/27.jpg)
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
![Page 28: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/28.jpg)
Handling HTML and routes auth @Override
protected WOActionResults performHtmlActionNamed(String actionName) throws Exception { try { initCookieAuthentication(); } catch (MemberException ex) { return pageWithName(LoginPage.class); } catch (NotAuthorizedException ex) { return pageWithName(LoginPage.class); } return super.performHtmlActionNamed(actionName); } @Override protected WOActionResults performRouteActionNamed(String actionName) throws Exception { try { initTokenAuthentication(); } catch (MemberException ex) { return errorResponse(401); } catch (NotAuthorizedException ex) { return errorResponse(401); } return super.performRouteActionNamed(actionName); }
![Page 29: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/29.jpg)
Other options
• OAuth
• Custom HTTP Authentication scheme
• Digest Authentification
• OpenID
• API Key (similar to token)
![Page 30: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/30.jpg)
Versioning
![Page 31: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/31.jpg)
Versioning
• Try hard to not having to version your REST services...
• ... but life is never as planified
• 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
![Page 32: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/32.jpg)
VersioningIn 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/
![Page 33: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/33.jpg)
Versioning: the gotcha
Watch out for schema changes or other changes that can break old versions if all versions use the same database schema!
![Page 34: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/34.jpg)
HTML routing
![Page 35: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/35.jpg)
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 offline)
![Page 36: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/36.jpg)
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
• Profits!
![Page 37: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/37.jpg)
Passing data to the component
Use the ERXRouteParameter annotation to tag methods to receive data:
@ERXRouteParameter
public void setMember(Member member) { this.member = member; }
![Page 38: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/38.jpg)
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.
![Page 39: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/39.jpg)
HTML routing gotchas
• When submitting forms, you're back to the stateful request handler
• ERXRouteUrlUtils doesn't create rewritten URLs
![Page 40: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/40.jpg)
Manual HTML routing
That's easy: same as a DirectAction:
public WOActionResults indexAction() throws Throwable {
return pageWithName(Main.class); }
![Page 41: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/41.jpg)
100% RESTpublic 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(); }...
![Page 42: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/42.jpg)
HTML routing: demo
![Page 43: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/43.jpg)
Cool trick: Application Cache Manifest
• Let you specify that some URLs of your app can be available offline
• URLs in the CACHE section will be available offline until you change the manifest and remove the URLs from the CACHE section
• Use a DirectAction or a static file to create the manifest
• One cool reason to use the HTML routing stuff
![Page 44: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/44.jpg)
Cache ManifestIn your DirectAction class:
public WOActionResults manifestAction() { EOEditingContext ec = ERXEC.newEditingContext(); WOResponse response = new WOResponse(); response.appendContentString("CACHE MANIFEST\n"); 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); }
![Page 45: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/45.jpg)
Debugging
![Page 46: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/46.jpg)
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
• tcpflow : see all trafic on a network interface, can do filters
• Apache DUMPIO (mod_dumpio) : dump ALL requests and responses data to Apache's error log
![Page 47: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/47.jpg)
Debugging Demo
![Page 48: ERRest](https://reader034.vdocuments.us/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/48.jpg)
Q&A
MONTREAL 1/3 JULY 2011