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