ERRest
-
Upload
wo-community -
Category
Technology
-
view
4.837 -
download
2
description
Transcript of ERRest
![Page 1: ERRest](https://reader034.fdocuments.in/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/1.jpg)
MONTREAL 1/3 JULY 2011
ERRestPascal RobertConatus/MacTI
![Page 2: ERRest](https://reader034.fdocuments.in/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.fdocuments.in/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/3.jpg)
What's New in ERRest
![Page 4: ERRest](https://reader034.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/13.jpg)
Security
![Page 14: ERRest](https://reader034.fdocuments.in/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.fdocuments.in/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/15.jpg)
Security
• Basic Authentification
• Sessions
• Tokens
![Page 16: ERRest](https://reader034.fdocuments.in/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/16.jpg)
USE SSL!
![Page 17: ERRest](https://reader034.fdocuments.in/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/17.jpg)
Basic Auth
![Page 18: ERRest](https://reader034.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/30.jpg)
Versioning
![Page 31: ERRest](https://reader034.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/34.jpg)
HTML routing
![Page 35: ERRest](https://reader034.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/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.fdocuments.in/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/42.jpg)
HTML routing: demo
![Page 43: ERRest](https://reader034.fdocuments.in/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.fdocuments.in/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.fdocuments.in/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/45.jpg)
Debugging
![Page 46: ERRest](https://reader034.fdocuments.in/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.fdocuments.in/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/47.jpg)
Debugging Demo
![Page 48: ERRest](https://reader034.fdocuments.in/reader034/viewer/2022051412/54bb83d44a7959780f8b45c8/html5/thumbnails/48.jpg)
Q&A
MONTREAL 1/3 JULY 2011