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

_______________________________________________
devs mailing list
[email protected]
http://lists.xwiki.org/mailman/listinfo/devs

Reply via email to