package er.extensions.appserver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOActionResults; import com.webobjects.appserver.WOApplication; import com.webobjects.appserver.WOComponent; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WORequest; import com.webobjects.appserver.WOResponse; import com.webobjects.appserver.WOSession; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSNotification; import com.webobjects.foundation.NSNotificationCenter; import com.webobjects.foundation.NSSelector; import er.extensions.eof.ERXConstant; import er.extensions.foundation.ERXThreadStorage; /** * Allows you to develop your app using component actions while still providing bookmarkable URLs. * It should be considered <b>highly</b> experimental and it uses a few very dirty shortcuts, but no private API to work it's magic. * The main problems may be garbage collection or space requirements. You might be better off to compress the responses. * <br> * The mode of operation is as follows; given a component action in a typical page: * <br> * <pre><code> * public WOComponent myAction() { * WOComponent nextPage = pageWithName("Main"); * nextPage.takeValueForKey(Integer.valueOf(100), "someValue"); * return nextPage; * } * * </code></pre> * then Main could be implemented something like this: * <pre><code> * public class Main extends WOComponent implements ERXComponentActionRedirector.Restorable { * static Logger log = Logger.getLogger(Main.class); * * public Integer someValue = Integer.valueOf(10); * * public Main(WOContext aContext) { * super(aContext); * } * * // this page has a "Increment Some Value" link to itself which just doubles the current value * public WOComponent addAction() { * someValue = Integer.valueOf(someValue.intValue()*2); * log.info(someValue); * return this; * } * * public String urlForCurrentState() { * return context().directActionURLForActionNamed("Main$Restore", new NSDictionary(someValue, "someValue")); * } * public static class Restore extends WODirectAction { * public Restore(WORequest aRequest) { * super(aRequest); * } * public WOActionResults defaultAction() { * WOComponent nextPage = pageWithName("Main"); * Number someValue = context().request().numericFormValueForKey("someValue", new NSNumberFormatter("#")); * if(someValue != null) { * nextPage.takeValueForKey(someValue, "someValue"); * } * return nextPage; * } * } * } * </code></pre> * But this is just one possibility. It only locates all the code in one place. <br> * * The actual workings are: * <ul> * <li>You create a page with typical component action links * <li>URL in browser: /cgi-bin/WebObjects/myapp.woa/ * <li>Links on page: /cgi-bin/WebObjects/myapp.woa/0.1.2.3 * </ul> * When you click on a link, the request-response loop gets executed, but instead of returning the response, we save it in a * session-based cache and return a redirect instead. The current page is asked for the URL for the redirect when it implements * the Restorable interface.<br> * So the users browser receives redirection to a "reasonable" URL like "/article/1234/edit?wosid=..." or * "../wa/EditArticle?__key=1234&wosid=...". This URL is intercepted and looked up in the cache. If found, the stored * response is returned, else the request is handled normally. * <p> * The major thing about this class is that you can detach URLs from actions. For example, it is very hard to create a * direct action that creates a page that uses a Tab panel or a collapsible component because you need to store a * tremendous amount of state in the URL. With this class, you say: "OK, I won't be able to totally restore everything, * but I'll show the first page with everything collapsed."<br> * * For all of this to work, your application should override the request-response loop like: * <pre><code> * public WOActionResults invokeAction(WORequest request, WOContext context) { * WOActionResults results = super.invokeAction(request, context); * ERXComponentActionRedirector.createRedirector(results); * return results; * } * * public void appendToResponse(WOResponse response, WOContext context) { * super.appendToResponse(response, context); * ERXComponentActionRedirector redirector = ERXComponentActionRedirector.currentRedirector(); * if(redirector != null) { * redirector.setOriginalResponse(response); * } * } * * public WOResponse dispatchRequest(WORequest request) { * ERXComponentActionRedirector redirector = ERXComponentActionRedirector.redirectorForRequest(request); * WOResponse response = null; * if(redirector == null) { * response = super.dispatchRequest(request); * redirector = ERXComponentActionRedirector.currentRedirector(); * if(redirector != null) { * response = redirector.redirectionResponse(); * } * } else { * response = redirector.originalResponse(); * } * return response; * } * </code></pre> * If you are using ERXApplication, you should set the * <code>er.extensions.ERXComponentActionRedirector.enabled=true</code> property instead. * * @author ak * */ public class ERXComponentActionRedirector { private static final Logger log = LoggerFactory.getLogger(ERXComponentActionRedirector.class); /** implemented by the pages that want to be restorable */ public static interface Restorable { /** This method will be called directly after invokeAction(), so any temporary variables * should have the same setting as they had when the action was invoked. * @return url for the current state. */ public String urlForCurrentState(); } /** the original response */ protected WOResponse originalResponse; /** the redirection response */ protected WOResponse redirectionResponse; /** the session id for the request */ protected String sessionID; /** the url for the redirected request */ protected String url; /** static cache to hold the responses. They are stored on a by-session basis. */ protected static final NSMutableDictionary responses = new NSMutableDictionary(); /** stores the redirector in the cache. * @param redirector The redirector to store. */ protected static void storeRedirector(ERXComponentActionRedirector redirector) { synchronized (responses) { NSMutableDictionary sessionRef = (NSMutableDictionary)responses.objectForKey(redirector.sessionID()); if(sessionRef == null) { sessionRef = new NSMutableDictionary(); responses.setObjectForKey(sessionRef, redirector.sessionID()); } sessionRef.setObjectForKey(redirector, redirector.url()); } log.debug("Stored URL: {}", redirector.url()); } /** * @param request The request * @return the previously stored redirector for the given request */ public static ERXComponentActionRedirector redirectorForRequest(WORequest request) { ERXComponentActionRedirector redirector = null; synchronized (responses) { redirector = (ERXComponentActionRedirector)responses.valueForKeyPath(request.sessionID() + "." + request.uri()); } if(redirector != null) { log.debug("Retrieved URL: {}", redirector.url()); } else { log.debug("No Redirector for request: {}", request.uri()); } return redirector; } /** Creates and stores a Redirector if the given results implement Restorable. * @param results */ public static void createRedirector(WOActionResults results) { ERXThreadStorage.removeValueForKey("redirector"); if(results instanceof WOComponent) { WOComponent component = (WOComponent)results; WOContext context = component.context(); if(context.request().requestHandlerKey().equals("wo")) { if(component instanceof Restorable) { ERXComponentActionRedirector r = new ERXComponentActionRedirector((Restorable)component); ERXComponentActionRedirector.storeRedirector(r); } else { log.debug("Not restorable: {}, {}", context.request().uri(), component); } } } } /** Uses ERXThreadStorage with the key "redirector". * @return the currently active Redirector in the request-response loop. */ public static ERXComponentActionRedirector currentRedirector() { return (ERXComponentActionRedirector)ERXThreadStorage.valueForKey("redirector"); } /** constructs the redirector from the Restorable. * @param r - Restorable component used to construct a redirector */ public ERXComponentActionRedirector(Restorable r) { WOComponent component = (WOComponent)r; WOContext context = component.context(); sessionID = component.session().sessionID(); url = r.urlForCurrentState(); if(context.session().storesIDsInURLs()) { String argsChar = url.indexOf("?") >= 0? "&" : "?"; String sessionIdKey = WOApplication.application().sessionIdKey(); if(url.indexOf(sessionIdKey + "=") < 0) { url = url + argsChar + sessionIdKey + "=" +sessionID; argsChar = "&"; } if(url.indexOf("wocid=") < 0) { url = url + argsChar + "wocid=" + context.contextID(); } } redirectionResponse = WOApplication.application().createResponseInContext(context); redirectionResponse.setHeader(url, "location"); redirectionResponse.setStatus(302); ERXThreadStorage.takeValueForKey(this, "redirector"); } /** @return the redirection response. */ public WOResponse redirectionResponse() { return redirectionResponse; } /** @return the URL with which the component can be restored. */ public String url() { return url; } /** @return the session ID for the Redirector. */ public String sessionID() { return sessionID; } /** @return the original response. */ public WOResponse originalResponse() { return originalResponse; } /** * Sets the original response. * * @param value the original response. */ public void setOriginalResponse(WOResponse value) { originalResponse = value; } /** * Observer class manages the responses cache by watching the session. * Registers for sessionDidTimeout to maintain its data. */ public static class Observer { protected Observer() { NSSelector sel = new NSSelector("sessionDidTimeout", ERXConstant.NotificationClassArray); NSNotificationCenter.defaultCenter().addObserver(this, sel, WOSession.SessionDidTimeOutNotification, null); } /** * Removes the timed out session from the internal array. * session. * @param n {@link WOSession#SessionDidTimeOutNotification} */ public void sessionDidTimeout(NSNotification n) { String sessionID = (String) n.object(); responses.removeObjectForKey(sessionID); } } @SuppressWarnings("unused") private static Observer observer; static { observer = new Observer(); } }