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