[xwiki-devs] [PROPOSAL] Actions2.0
Hi, A couple weeks ago I proposed a technique for allowing servlets to be registered with the component manager. There was some tentative agreement that the idea had merit so I began researching it. In the process of trying to solve this problem I think I have stumbled on a workable Actions2.0 model. The really short version: @Component @Named("sayhello") public class HelloWorldAction implements Action { @Action.Runner public void run(ClientPrintWriter cpw) { cpw.println("Hello World!"); } } The Action router gets a request for localhost:8080/xwiki/sayhello/ and loads your Action from the ComponentManager, it examines the class and sees your annotated method and looks at it's arguments, since the method takes a ClientPrintWriter, the action router gets a list of all ActionProviders from the ComponentManager and examines them to find one which provides it. It then uses the same technique to recursively resolves that provider's requirements. Once all requirements have been satisfied, the router calls each provider and finally your Action, each time passing the required dependencies using the Method#invoke() function. Here's an example of an ActionProvider which provides a PrintWriter: @Component @Named("ServletPrintWriterProvider") public class ServletPrintWriterProvider implements ActionProvider { @Action.Runner public void run(ServletResponse resp, Callback<ClientPrintWriter> cpwCallback) { cpwCallback.call(new PrintWriter(resp.getOutputStream())); } } Obviously one could be registered which worked with Portlet, or even command line invocation. HelloWorldAction doesn't require much. Benefits of this design: 1. You only get what you need. Why should I wait for the XWikiContext or the ExecutionContext to be populated so that I can serve the user a static piece of js or a favicon? 2. Legacy code can coexist with modern code. If there is an ActionProvider which provides an XWikiContext then all legacy code is about 8 lines away from comparability. Same is true for code which absolutely needs a ServletRequest and ServletResponse, they just need to require them. 3. This API doesn't try to own you. You're not tied down to a context with specific values in it. If you are requiring a PotatoeContext and it turns out not to be good enough, write a CarrotContext and begin requiring that, the Action model is still the same. Devil's Advocacy: 1. Why does it map implementations by their classes? What if someone wants to register 2 providers of OutputStream? * Notice I used ClientPrintWriter rather than PrintWriter in the example. If you write a provider, it is best practice to extend a class or interface and provide your extension so that anyone who is using your provider will have to 'import' the class which you provide. I want to avoid this: Object x = context.get("IHaveNoIdeaWhereThisIsDefined"); We can change this without breaking the API by adding optional "hints" to the @Action.Runner annotation. 2. Good job cjd, you just rewrote the ComponentManager. * I spent quite some time wrestling with whether this should be done in the CM, from a pragmatic PoV, the problem is you end up needing to create things on a per-request basis which abuses the ECM and will hold the initialization (global) lock for a long time. From a design PoV it's wrong because dependency injection is meant to inject long lived (often singleton) machinery objects whereas this is for request scoped data objects. 3. Why the silly Callbacks? * An obvious thought is that an ActionProvider should just have a get() method like any other provider which returns the object in question. The pragmatic issue with that is suppose you need to do something expensive like hit the database to get an object and you get another object for free while doing it, do you throw the other one away and then when the caller needs the other one, they call another provider and do the expensive operation over again? This solution allows a provider to pull in multiple callbacks and call each one, thus providing multiple objects. A second more subtle reason is that ActionProviders are allowed to return their provisions asynchronously which means we could implement some very exciting optimizations at the storage level. We don't have to do that route but I don't want to close the door on it. 4. Nobody has ever done this before * That's why it's going to work. Actually this design draws heavily on Asynchronous Module Definition which is explained here: http://requirejs.org/docs/whyamd.html 5. Magic! Arrest this sorcerer! * It's valid to call this magic. It's also valid to call any kind of dependency injection magic. @Inject private InterfaceWhichDoesNotExplainMuch youWillNeverFigureOutWhereTheImplementationIs; is as bad as this or worse. One way we can minimize the magic and keep the benefits is to make our ActionProviders provide custom classes which are defined close to the ActionProvider. ViewingUser or CurrentDocumentAuthorUser are much more self explanatory than injecting "User" even if both classes extend User. 5. ... * Help me try to break this design. Where's the code: Coming soon, I have most of a PoC hacked together but it currently has a slightly different API which I scrapped in favor of the one defined here. I just want to get discussion going as early as possible. Prove me wrong Caleb
participants (1)
-
Caleb James DeLisle