CzechIdM poskytuje webovou službu pro práci s workflow

Práce na systému CzechIdM neustávají. Stále hledáme možnosti, jak tento systém dále zlepšit a rozšířit. Tento článek popisuje vývoj webové služby (WS) v rámci tohoto projektu. První část článku je zaměřena na vývoj samotné webové služby, přičemž jsou zde popsány i některé obecné a technologické aspekty týkající se vývoje WS. Druhá část se věnuje vytvoření vzorového klienta, který používá implementované webové metody.

Úvod

Webové služby jsou asi nejpoužívanější implementací servisně orientovaných architektur. Jedná se o propojení technologií SOAP a HTTP, XML, UDDI a WSDL. SOAP a HTTP zajišťují komunikační spojení a slouží pro přenášení zpráv. SOAP patří do aplikační vrstvy modelu TCP/IP. Je to bezestavový protokol, který je nezávislý na protokolu komunikačním. Nejčastěji je však pro komunikaci použit protokol HTTP (popř. HTTPS). SOAP definuje strukturu přenášených zpráv. Zprávy jsou strukturována jako XML dokumenty. Pro registraci a vyhledávání webových služeb slouží UDDI. Popis funkcionality, umístění služeb a způsob komunikace je popsán ve WSDL, což je standard konsorcia W3C, a opět má formu XML dokumentu. Pro vývoj webových služeb a jejich klientů v jazyce Java je k dispozici JAX-WS aplikační rozhraní. Webové služby jsou zde definovány jako obyčejné POJO třídy (případně jako EJB), přičemž jsou označeny příslušnými anotacemi. JAX-WS je platformně nezávislé. Lze tedy komunikovat i se službami implementovanými v jiném jazyce než je Java. Na následujícím obrázku (oficiální dokumentace The Java 5 EE Tutorial) je zobrazena architektura komunikace klienta a webové služby.

Implementace WS

Při implementaci webových služeb máme k dispozici dva přístupy, kterými se můžeme vydat. První možností je nejprve vytvořit WSDL dokument, který popisuje danou službu. Z něho potom vygenerujeme rozhraní webové služby, které následně implementujeme. Druhý přístup je přesně opačný. Vytvoříme implementační Java třídy označené příslušnými anotacemi a z nich poté vygenerujeme WSDL. Ať už si vybereme jakoukoliv cestu, vždy bychom měli obdržet stejný výsledek. Další výhody přináší použití vývojového IDE, které vývoj dále zjednodušuje.

V našem případě jsme se vydali druhou cestou. Navrhli jsme koncové rozhraní webové služby (kód zobrazen níže) a následně ho implementovali. WSDL lze obecně vygenerovat buď pomocí IDE nebo pomocí příkazu wsgen (součástí JAX-WS RI). Příkaz wsgen generuje potřebné artefakty ze zdrojových kódů (konkrétně z koncového rozhraní). Výstupními artefakty se zde myslí JAXB třídy, které jsou vyžadovány pro marshalling/unmarshalling obsahu zpráv. Pokud chceme vygenerovat také WSDL, tak to musíme explicitně uvézt (přepínač „-wsdl“). V našem případě jsme WSDL generovat ani nemuseli. Jako aplikační server byl použit JBoss AS 5.1.0, který po nasazení aplikace na server sám všechny potřebné výstupy (včetně WSDL) vygeneruje. Server JBoss totiž obsahuje JBossWS framework, který implementuje JAX-WS specifikaci.

package eu.bcvsolutions.idm.app.ws;

import javax.jws.WebMethod;
import javax.jws.WebService;

/**
 * Rozhraní deklarující metody webové služby pro spouštění workflow.
 *
 * @author Jaromír Mlejnek
 *
 */
@WebService(name = "WFLauncherSEI",
		targetNamespace = "http://ws.app.idm.bcvsolutions.eu/")
public interface WFLauncherSEI {

	/**
	 * Metoda pro asynchronní spouštění workflow.
	 *
	 * @param name jméno daného workflow
	 * @param variables parametry daného workflow (HashMap<String, Object>) zapouzdřené v instanci třídy InputWS.
	 * @return Případný výstup (standardní i chybový).
	 */
	@WebMethod(operationName = "launchWorkflowAsynchronously", action = "urn:LaunchWorkflowAsynchronously")
	public OutputWS launchWorkflowAsynchronously(String name, InputWS variables);

	/**
	 * Metoda pro synchronní spouštění workflow. Metoda čeká na dokončení běhu daného workflow
	 * a navrací jeho případný výsledek.
	 *
	 * @param name jméno daného workflow
	 * @param variables parametry daného workflow (HashMap<String, Object>) zapouzdřené v instanci třídy InputWS.
	 * @return Výstup běhu daného Workflow; případně ohlášení vzniklé chyby.
	 */
	@WebMethod(operationName = "launchWorkflowSynchronously", action = "urn:LaunchWorkflowSynchronously")
	public OutputWS launchWorkflowSynchronously(String name, InputWS variables);

	/**
	 * Metoda pro přihlašování klienta.
	 *
	 * @param username uživatelské jméno klienta
	 * @param passwd heslo klienta
	 * @return V případě úspěšného přihlášení vrací TRUE; jinak FALSE.
	 */
	@WebMethod(operationName = "login", action = "urn:Login")
	public boolean login(String username, String password);

	/**
	 * Metoda pro odhlášení daného klienta.
	 *
	 * @return V případě úspěšného odhlášení vrací TRUE; jinak FALSE.
	 */
	@WebMethod(operationName = "logout", action = "urn:Logout")
	public boolean logout();
}

Výpis kódu výše odpovídá koncovému rozhraní naší webové služby. Rozhraní je anotováno anotací @WebService, které oznamuje, že se jedná o deklaraci WS. Atribut „name“ slouží pro specifikování názvu WS. „TargetNamespace“ určuje jmenný prostor použitý pro WSDL a XML elementy generované danou webovou službou. Jednotlivé metody jsou anotovány anotací @WebMethod. Tato anotace musí označovat všechny veřejné metody, které mají být dostupné klientům, přičemž dané metody nesmí být deklarovány jako „static“ a „final“. Podívejme se nyní, co dané metody vykonávají. Úloha metod „login“ a „logout“ je zřejmá již z jejich názvů. Autentizace se provádí předáním uživatelského jména a hesla, přičemž daná metoda informuje klienta u úspěšnosti přihlášení. Při odhlašování je klient informován, zda daná operace proběhla úspěšně. Zbývající dvě metody slouží pro spouštění workflow. Workflow je XML soubor obsahující zdrojový kód v jPDL a skripty v BeanShellu. Jedná se tedy o zdrojové kódy aplikační logiky. Použití workflow je zajímavé v tom, že po jeho nahraní na aplikační server není nutné daný server restartovat. Tím je možné „za běhu“ měnit (doplňovat) funkcionalitu aplikace. Další předností workflow je to, že lze aktuální stav jeho provádění uložit do databáze a po splnění definované podmínky opět z databáze obnovit a pokračovat v jeho běhu. Workflow je možné spouštět synchronně i asynchronně. Při synchronním spuštění se dané workflow provede a klientovi je navrácen případný výstup. Asynchronní způsob pouze spustí workflow a nečeká na jeho dokončení. Dané metody přijímají jméno workflow a případné další parametry pro workflow. Jméno je předáno jako klasický String řetězec a parametry jsou „zabaleny“ v instanci třídy InputWS. Výstup je předán jako instance třídy OutputWS. Třídy InputWS a OutputWS slouží pouze pro předávání vstupu a výstupu. Ještě je potřeba dodat, že klient musí být před spuštěním workflow samozřejmě přihlášen a musí vlastnit odpovídající oprávnění.

JAXB a použití java.lang.Object jako parametru webových metod

Jak již bylo uvedeno výše, pro vstup a výstup webových metod jsou použity speciální třídy InputWS a OutputWS. Nabízí se otázka, proč nepředávat parametry (a návratovou hodnotu) přímo. Problémem však je to, že my chceme, aby bylo možné předávat daným metodám libovolné serializovatelné objekty, a to JAX-WS úplně neumožňuje. Pokud bychom deklarovali například metodu pro synchronní spouštění workflow následovně,

@WebMethod(operationName = "launchWorkflowSynchronously", action = "urn:LaunchWorkflowSynchronously")
public OutputWS launchWorkflowSynchronously(String name, Object variables);

a poté jí při volání předali jako parametr „variales“ například HashMap<String, Object>, tak bychom obdrželi následující chybu.

 javax.xml.ws.WebServiceException: javax.xml.bind.MarshalException
 - with linked exception:
 [javax.xml.bind.JAXBException: class java.util.HashMap nor any of its super class is known to this context.]

Problémem je, že při transformaci parametru „variables“ na XML (to zajišťuje JAXB; součást JAX-WS) nejsou explicitně uvedeny třídy, které se zde mohou použít. Navíc chceme mít možnost předávat jako parametr „variables“ libovolné objekty, takže nemůžeme přímo odpovídající třídy referencovat. Proto pro vstup používáme pomocnou třídu InputWS (pro výstup používáme obdobnou třídu OutputWS, protože i zde chceme mít možnost navracet libovolné objekty). Tato třída provádí transformaci předaného objektu do podoby bajtového pole a poté naopak objekt z pole rekonstruuje. Třída OutputWS má jako členskou proměnnou navíc další bajtové pole, protože jedno je použito pro přenášení standardního výstupu a druhé pro případný chybový výstup. Kód třídy InputWS je zobrazen níže.

package eu.bcvsolutions.idm.app.ws;

import eu.bcvsolutions.idm.app.ws.Transform;
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlType;

/**
 * Třída zapouzdřující vstupní parametry webových metod. Parametry jsou transformovány
 * do bajtového pole. To umožňuje předávat webovým službám libovolné objekty (potomky
 * třídy java.lang.Object), které jsou přenášeny přes síť a následně zrekonstruované pomocí JAXB.
 *
 * @author Jaromír Mlejnek
 *
 */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "inputWS", propOrder = {
    "byteArray"
})
public class InputWS implements Serializable {

	private static final long serialVersionUID = 1L;

	private byte[] byteArray;

	/**
	 * Implicitni konstruktor.
	 */
	public InputWS() {
		this.byteArray = null;
	}	

	/**
	 * Konstruktor vytvářející instanci třídy InputWS zapouzdřující objekt input.
	 *
	 * @param input objekt, který bude transformován do bajtového pole.
	 */
	public InputWS(Object input) {
		this.byteArray = Transform.objectToByteArray(input);
	}

	/**
	 * Metoda pro nastavení objektu.
	 *
	 * @param obj objekt, který bude transformován do bajtového pole.
	 */
	public void setObject(Object obj) {
		this.byteArray = Transform.objectToByteArray(obj);
	}

	/**
	 * Metoda navracející objekt.
	 *
	 * @return Zrekonstruovaný objekt. Pokud není přenášen žádný objekt, tak navrací null.
	 */
	public Object getObject() {
		if (this.byteArray == null) {
			return null;
		} else {
			return Transform.byteArrayToObject(this.byteArray);
		}
	}
}

Vytvoření klienta webové služby

Nyní, když již máme implementovanou webovou službu, se podíváme, jak vytvořit jejího klienta. I zde za nás může velké množství práce vykonat vývojové IDE, které nám vygeneruje potřebné artefakty. Případně můžeme použít příkaz „wsimport“, který taktéž vygeneruje vše potřebné. Jediné, co je potřeba, je znát URI WSDL souboru dané webové služby. Syntaxe příkazu wsimport je následující:

wsimport -keep <WSDL_URI>

 


kde přepínač „-keep“ určuje, že se mají vygenerovat i zdrojové kódy (jinak se vygenerují pouze *.class soubory). Nyní se podíváme, k čemu jednotlivé vygenerované soubory v našem případě slouží.

  • InputWS.java – implementace třídy zapouzdřující vstup metod launchWorkflowAsynchronously a launchWorkflowSynchronously. Zde se upraví implementace tzv. getrů a setrů tak, jak je to u stejné třídy na straně webové služby.
  • LaunchWorkflowAsynchronously.java – třída specifikující vstupní parametry webové metody launchWorkflowAsynchronously.
  • LaunchWorkflowAsynchronouslyResponse.java – třída specifikující návratové hodnoty webové metody launchWorkflowAsynchronously.
  • LaunchWorkflowSynchronously.java – třída specifikující vstupní parametry webové metody launchWorkflowSynchronously.
  • LaunchWorkflowSynchronouslyResponse.java – třída specifikující návratové hodnoty webové metody launchWorkflowSynchronously.
  • Login.java – třída specifikující vstupní parametry webové metody login.
  • LoginResponse.java – třída specifikující návratové hodnoty webové metody login.
  • Logout.java – třída specifikující vstupní parametry webové metody logout.
  • LogoutResponse.java – třída specifikující návratové hodnoty webové metody logout.
  • ObjectFactory.java – tato třída je vyžadována JAXB a obsahuje tovární metody pro všechny JAXB-mapované třídy.
  • OutputWS.java – implementace třídy zapouzdřující výstup metod launchWorkflowAsynchronously a launchWorkflowSynchronously. Zde se upraví implementace tzv. getrů a setrů tak, jak je to u stejné třídy na straně webové služby.
  • package-info.java – určuje jmenný prostor pro vygenerované třídy v odpovídajícím balíčku.
  • WFLauncherBeanService – třída dědící od třídy javax.xml.ws.Service, je zde určeno URI WSDL souboru webové služby a implementuje metody pro vytvoření zástupného proxy objektu (označen jako „port“), nad kterým budou volány webové metody.
  • WFLauncherSEI.java – rozhraní deklarující signaturu dostupných webových metod.

Nyní stačí už pouze implementovat třídu pro spouštění klientské aplikace (v našem případě se jedná o obyčejnou desktopovou Java aplikaci).

package eu.bcvsolutions.idm.app.ws;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;

/**
 * Hlavní třída klientské aplikace spouštějící webové metody.
 *
 * @author Jaromír Mlejnek
 *
 */
public final class Main {

    private static final QName SERVICE_NAME = new QName("http://ws.app.idm.bcvsolutions.eu/", "WFLauncherBeanService");

    private Main() {
    }

    public static void main(String args[]) throws Exception {
        URL wsdlURL = WFLauncherBeanService.WSDL_LOCATION;
        if (args.length > 0) {
            File wsdlFile = new File(args[0]);
            try {
                if (wsdlFile.exists()) {
                    wsdlURL = wsdlFile.toURI().toURL();
                } else {
                    wsdlURL = new URL(args[0]);
                }
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
        }

        WFLauncherBeanService ss = new WFLauncherBeanService(wsdlURL, SERVICE_NAME);
        WFLauncherSEI port = ss.getWFLauncherBeanPort();         

        {
        System.out.println("Invoking login...");
        String username = "admin";
        String password = "";
        boolean isLogged = port.login(username, password);
        System.out.println("login.result=" + isLogged);

        }
        {
        System.out.println("Invoking launchWorkflowSynchronously...");
        String workflowNameSyn = "testWS.wf";
        InputWS paramSyn = null;

        OutputWS outputSyn = port.launchWorkflowSynchronously(workflowNameSyn, paramSyn);
        Object stdOutSyn = outputSyn.getStdOutput();
        Object errOutSyn = outputSyn.getErrOutput();
        if (stdOutSyn != null) {
        	System.out.println("StdOut: " + stdOutSyn.toString());
        }
        if (errOutSyn != null) {
        	System.out.println("ErrOut: " + errOutSyn.toString());
        }

        }
        {
        System.out.println("Invoking launchWorkflowAsynchronously...");
        String workflowNameAsyn = "testWS.wf";
        InputWS paramAsyn = new InputWS();

        OutputWS outputAsyn = port.launchWorkflowAsynchronously(workflowNameAsyn, paramAsyn);        

    	Object stdOutAsyn = outputAsyn.getStdOutput();
        Object errOutAsyn = outputAsyn.getErrOutput();
        if (stdOutAsyn != null) {
        	System.out.println("StdOut: " + stdOutAsyn.toString());
        }
        if (errOutAsyn != null) {
        	System.out.println("ErrOut: " + errOutAsyn.toString());
        }

        }
        {
        System.out.println("Invoking logout...");
        boolean _logout__return = port.logout();
        System.out.println("logout.result=" + _logout__return);
        }

        System.exit(0);
    }
}

Pokud nyní klientskou aplikaci spustíme, tak dostaneme následující výstup.

Invoking login...
login.result=true
Invoking launchWorkflowSynchronously...
ErrOut: You have to login firstly.
Invoking launchWorkflowAsynchronously...
ErrOut: You have to login firstly.
Invoking logout...
logout.result=true

Z výstupu je vidět, že klient se úspěšně přihlásil, pak chtěl spustit workflow, ale nezdařilo se to, protože údajně nebyl přihlášen. V čem je chyba? Jde o to, že není udržována relace (session).

Udržování session

Webové služby jsou z podstaty věci bezestavové (angl. stateless), a to z toho důvodu, že jejich komunikace je založena na protokolu HTTP. Proto server implementující webové služby považuje implicitně každou klientskou žádost (request) za novou interakci, a to i v případě, že všechny žádosti směřují od jedné klientské aplikace. Je zřejmé, že v mnoha případech je toto chování nevhodné. Příkladem může být přihlášení se k webové službě a následné spuštění metody vyžadující autentizaci přihlášením (viz. výše). Při přihlašování pošle klient serveru požadavek (request), server nastaví tzv. cookie pro dané spojení a pošle ho spolu s odpovědí (response) klientovi. Avšak JAX-WS klient implicitně cookie ignoruje a tedy další klientský požadavek bere server jako novou interakci s ním. Z toho důvodu server neprovede klientem požadovanou metodu, protože v kontextu nové session není klient autentizován. Pokud se udržování session povolí, tak klient spolu s dalšími žádostmi odesílá serveru i dané cookie, čímž server udržuje klientskou session. Cookie obsahuje jednoznačný identifikátor (označován jako SESSIONID, SESSID apod.), který je na začátku session náhodně vygenerován.

Nyní se dostáváme k tomu, jakým způsobem zajistit, aby JAX-WS klientská aplikace neignorovala vracené cookie a připojovala ho ke všem svým dalším requestům. Řešení je překvapivě snadné. Stačí v třídě Main.java nastavit v kontextu zasílaných requestů WS vlastnost SESSION_MAINTAIN_PROPERTY u proxy objektu na TRUE.

.
.
WFLauncherBeanService ss = new WFLauncherBeanService(wsdlURL, SERVICE_NAME);
WFLauncherSEI port = ss.getWFLauncherBeanPort();  

 //Zajistí použití stejného sessionId při běhu klientské aplikace. 
 ((BindingProvider)port).getRequestContext().put(BindingProvider.SESSION_MAINTAIN_PROPERTY,true); 

{
System.out.println("Invoking login...")
.
.

Pokud znovu spustíme klienta, tak již lze vidět, že vše proběhlo dle očekávání, protože session je udržováno během celé doby běhu klientské aplikace.

Invoking login...
login.result=true
Invoking launchWorkflowSynchronously...
Invoking launchWorkflowAsynchronously...
Invoking logout...
logout.result=true

V tomto článku jsme si ukázali, že vytvoření webové služby a jejího klienta není snad až tak těžké :-).