Montag, 17. Mai 2010

RESTful Java Server Faces (JSF) - Und bist du nicht willig...

Hallo,

Nachdem ich mich nun schon eine kleine Weile mit RESTful webservices unter Java beschäftigt habe und evaluiert habe, was in diesem Zusammenhang an Frameworks vorhanden ist, stand ich vor einem kleinen Problem, welches ich nun, Gott sei Dank, lösen konnte und damit einen ersten Ansatz für RESTful Java Server Pages schaffen konnte.

Um RESTful webservices in Java zu implementieren, kennt Java die funkel-nagel-neue API 'jax-ws', welche es erlaubt verschiedene Java Beans über Annotations als Web-Resourcen zu verwenden.
Hierauf können nun verschiedene Methoden implementiert werden, welche die HTTP Methoden bedienen und so z.B. einen GET-Request auf die XML Representation einer Resource behandeln können.

Nun ist es natürlich nicht schwierig auf die selbe Weise eine GET methode für HTML zu definieren.
Problematisch wird es erst, wenn man nun auch noch auf die Idee kommt, doch gerne die vielfältigen Möglichkeiten der Java Server Faces für eben jene HTML Representation der Resource zu verwenden.


Hochzeit wider Willen


Um die JSF Engine aus einem bereits bestehenden JAX-WS Aufruf heraus zu bedienen müßte man verschiedene größere Hürden überwinden:

  • Starten der JSF Engine auserhalb ihres eigenen Servlets

  • Bekanntmachung des richtigen Templates (xhtml Seite) für die entsprechende Ressource

  • Anmeldung des bereits auf anderem Wege geladenen Datenobjects an die EL der Server Faces



Die JSF Engine manuel zu starten ist wichtig, denn zu dem Zeitpunkt an dem die Methode 'GET text/xhtml' auf dem jax-ws service aufgerufen wird, befinden wir uns bereits in einem Servlet-Context und wollen diesen auch gerne beibehalten.

Gott Sei Dank ist hier guter Rat nicht teuer und nach einem kurzen Blick in die Implementation der javax.faces.webapp.FacesServlet Klasse stand ein erster Rohbau der 'GET text/xhtml' Methode:

    @GET
    @Produces("text/html")
    public void getXHtml(
            @Context ServletConfig cfg,
            @Context HttpServletRequest req,
            @Context HttpServletResponse resp) throws IOException{

        LifecycleFactory lfactory = (LifecycleFactory)
            FactoryFinder.getFactory(FactoryFinder.LIFECYCLE_FACTORY);
        Lifecycle lc = lfactory.getLifecycle(
            LifecycleFactory.DEFAULT_LIFECYCLE);

        FacesContextFactory fcfactory = (FacesContextFactory)
            FactoryFinder.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);
        FacesContext fc = fcfactory.getFacesContext(
            cfg.getServletContext(),
            req,
            resp,
            lc);

        lc.execute(fc);
        lc.render(fc);
        fc.release();

        resp.flushBuffer();
    }


Ebenso wie das JSF Servlet, initialisiere ich hier die JSF Engine indem ich zuerst einen Lifecycle und einen FacesContext lade, um anschließend 'execute', 'render' und 'release' darauf auszuführen. Zuletzt muß nur noch der Responcepuffer geflashed werden. Da wir hier direkt auf einem HTTPServletResponse Object arbeiten, ist es auch nicht nötig irgendetwas zurück zu geben.

Nun haben wir zwar erfolgreich die JSF Engine aus einem JAX-WS Webservice heraus aufgerufen, da wir der JSF Engine allerdings noch nicht mitgeteilt haben, was wir den gerne rendern würden, allerdings noch kein verwertbares Ergebnis.

Der nächste Schritt wird daher, der JSF Engine eine JSF-Seite als Template mitzugeben.
Leider ist die JSF Engine hier so sehr in sich selbst gekapselt, daß es keine programative Schnittstelle gibt um dies zu tun, ohne auf die tatsächliche Implementation zurückgreifen zu können, was ich prinzipiell vermeiden will.
Ein Trick den wir hier verwenden können ist, den HTTPServletRequest den wir der Engine übergeben anzupassen und den Aufruf des JSF-Templates und der URI mitzuliefern.
Hierfür erstellen wir uns eine Wrapper Klasse für den Request:

import [...];

public class WrappedHttpRequest implements HttpServletRequest{

    private String template;
    private HttpServletRequest wrapped;

    public WrappedHttpRequest(
            HttpServletRequest wrapped,
            String template) {
        this.wrapped = wrapped;
        this.template = template;
    }

    /*
     * delegate all methods to the wrapped request
     */
    [...]
}


Auf diesem Wrapper überschreiben wir nun die Methode getPathInfo() welche für den Sub-Pfad unterhalb des Servlet-Pfades zuständig ist. Generel gesagt: Wenn ein Servlet mit dem Pfad: http:/server/servlet/somepage angesprochen wird, so liefert diese Methode: '/somepage' zurück.
In unserem Fall, soll die Methode den Pfad zu dem gewünschten JSF-Template zurückgeben.


    public String getPathInfo() {
        return template;
    }


Nun können wir der aufgerufenen JSF-Engine das gewünschte Template mit angeben, indem wir ihr einen WrappedHTTPRequest mit dem entsprechenden Template-Pfad übergeben. Unsere neue GET Methode sieht nun wie folgt aus:


    @GET
    @Produces("text/html")
    public void getXHtml(
            @Context ServletConfig cfg,
            @Context HttpServletRequest req,
            @Context HttpServletResponse resp) throws IOException{

        LifecycleFactory lfactory = (LifecycleFactory)
            FactoryFinder.getFactory(FactoryFinder.LIFECYCLE_FACTORY);
        Lifecycle lc = lfactory.getLifecycle(
            LifecycleFactory.DEFAULT_LIFECYCLE);

        FacesContextFactory fcfactory = (FacesContextFactory)
            FactoryFinder.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);
        FacesContext fc = fcfactory.getFacesContext(
            cfg.getServletContext(),
            new WrappedHttpRequest(req, "/template.xhtml"),
            resp,
            lc);

        lc.execute(fc);
        lc.render(fc);
        fc.release();

        resp.flushBuffer();
    }


So viel zum einfachen Teil der Übung. Wenn wir unser Servlet nun aufrufen, bekommen wir tatsächlich eine wunderschön gerenderte JSF Seite angezeigt, genau wie es gedacht war.

Um allerdings irgendeinen Nutzen aus der Aktion ziehen zu können, muß nun unser eigendliches Datenobject noch irgendwie in dem jeweiligen JSF-Template verwendbar werden.
Nach längerer Wühlarbeit durch die Analen der JSF-Spezifikation (schwääre Kost) hatte ich auch hierfür eine Lösung gefunden.

JSF bietet Zugriff auf bestimmte 'Backing Beans' über eine Scriptsprache namens 'Expression Language' (EL).
Leider können wir unser Datenobject nicht einfach als ManagedBean annotieren, da eine solche durch die JSF-Engine selbst initialisiert wird und wir so keine Kontrolle mehr über das Object selbst haben.
Allerdings erlaubt die EL, in einen scope-abhängigen ELContext Variablen als Implementationen der javax.el.ValueExpression Klasse einzubinden.
Nehmen wir nun den EL-Context des FacesContext, welcher grundsätzlich im Scope REQUEST liegt so können wir sicher sein, das unser Wrapper Objekt nur diese eine Anfrage überlebt.

Um dies zu implementieren, brauchen wir natürlich zuerst einen solchen Wrapper als ValueExpression:


import [...];

public class JsfManaged extends ValueExpression{

    private static final long serialVersionUID = 2254085359874699578L;
    private Object item;

    public JsfManaged(Object item){
        this.item = item;
    }

    @Override
    public Class getExpectedType() {
        return item.getClass();
    }

    @Override
    public Class getType(ELContext context) {
        return null;
    }

    @Override
    public Object getValue(ELContext context) {
        return item;
    }

    @Override
    public boolean isReadOnly(ELContext context) {
        return true;
    }

    @Override
    public void setValue(ELContext context, Object value) {}

    @Override
    public boolean equals(Object obj) {
        if (null != obj && obj instanceof JsfManaged){
            Object i = ((JsfManaged)obj).item;
            return null == i?null == item:i.equals(item);
        }
        return false;
    }

    @Override
    public String getExpressionString() {
        return "myobject";
    }

    @Override
    public int hashCode() {
        return item.hashCode();
    }

    @Override
    public boolean isLiteralText() {
        return false;
    }

}


Da dies eine Read-Only Expression sein soll, sind die meisten Methoden nicht von weiterem Interese. Lediglich die (fett markierten) Methoden getExpectedType, getValue und equals sollte man sich etwas genauer ansehen.

Hauptsächlich die getValue Methode ist von Interresse, da hier das tatsächliche Datenobject ausgegeben wird.

Um diesen Wrapper nun für einen bestimmten Request und ein bestimmtes Datenobject einzubauen, müßen wir wiederum unsere GET methode anpassen. Für unser Beispiel hier nehmen wir einfach einmal an, daß wir das tatsächliche Datenobject über getData() erlangen können.


    @GET
    @Produces("text/html")
    public void getXHtml(
            @Context ServletConfig cfg,
            @Context HttpServletRequest req,
            @Context HttpServletResponse resp) throws IOException{

        LifecycleFactory lfactory = (LifecycleFactory)
            FactoryFinder.getFactory(FactoryFinder.LIFECYCLE_FACTORY);
        Lifecycle lc = lfactory.getLifecycle(
            LifecycleFactory.DEFAULT_LIFECYCLE);

        FacesContextFactory fcfactory = (FacesContextFactory)
            FactoryFinder.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);
        FacesContext fc = fcfactory.getFacesContext(
            cfg.getServletContext(),
            new WrappedHttpRequest(req, "/template.xhtml"),
            resp,
            lc);

        Object data = getData();
        fc.getELContext().getVariableMapper().setVariable(
            "myobject",
            new JsfManaged(data));


        lc.execute(fc);
        lc.render(fc);
        fc.release();

        resp.flushBuffer();
    }


Nun kann in unserem JSF-Template unter verwendung der EL Expression #{myobject} immer auf das tatsächliche Datenobject zugegriffen werden.

Fazit


Es ist also möglich, mit geringem Aufwand die Vorteile der JSF Architektur auch innerhalb eines JAX-WS kontrollierten WebService auszunutzen.

Leider ist JSF immernoch sehr wenig kooperationswillig, weshalb es momentan noch einiger Trickserei bedarf. Da man dies aber trozdem bewerkstelligen kann, ohne auf die JSF Implementation selbst zugreifen zu müßen, halte ich diesen Ansatz dennoch für einen sehr gangbaren Weg.

Ich würde mir für zukünftige Versionen der JSF wünschen, daß man EL und Templaterendering auf einfachere Weise auch außerhalb eines vollen JSF Kontext verwenden kann, um keine derartigen Klimmzüge mehr machen zu müßen.

Glossar


Java Server Faces (JSF) - Spezifikation

JAX-RS - RESTful Webservice Implementation in Java. - Spezifikation

REST - "Representational State Transfer". Das Architektur Paradigma hinter HTTP und dem WWW. - Definition - Auserdem gibt es noch eine gar nicht mal so schlechte Erklärung in der engischen Wikipedia.


Auf Bald.