27. prosince 2012

Java a fault handling policies v Oracle SOA Suite

Jednou ze součástí Oracle SOA Suite je tzv. Fault Management Framework, který se mmj. stará o zpracování výjimek v BPELu. Pokud během invoke aktivity nastane výjimka, framework ji odchytí a předá ji ke zpracování akci, která je definovaná ve fault policy.

Tyto politiky jsou zajímavou alternativou k odchytávání výjimek v samotném BPELu  pomocí fault handleru <catch>. Dá se na ně pohlížet jako na aspekt (ve smyslu AOP), který je deklarativně navěšený na procesu (nebo i celé kompozitní aplikaci).

No, pokud jsem řekl aspekt, pravděpodobně jsem vzbudil velká očekávání. Tak to bohužel není - sám jsem musel pohřbít některé své designové představy - má to spoustu omezení, resp. možnosti nejsou rozsáhlé. Takže co to vlastně umí?

V rámci politiky se dají definovat následující zotavné akce:
  • Retry. Nepovedený invoke se dá zopakovat. Dá se nastavit počet opakování, prodleva, exponenciální prodleva a následující zřetězená akce, pokud retry nezafunguje.
  • Human Intervention. Proces se zastaví a je možné ho manuálně obnovit z management konzole.
  • Terminate Process. Proces je ukončen. Totéž jako aktivita.exit.
  • Rethrow Fault. Chyba je vyhozena zpátky do BPEL procesu.
  • Replay Scope. Vyhodí reply fault, což způsobí znovu vykonání aktivit ve scope.
  • Java Code. Zavolá externí Java třídu. To je to, na co se budeme dále soustředit.

Problém

Mám pro klienty vystavenou službu SOAPFault, která má ve svém kontraktu definovaný SOAP Fault:
<xsd:element name="fault">
  <xsd:complexType>
    <xsd:sequence>
      <xsd:element name="resultCode"
                   type="xsd:string"/>
      <xsd:element name="error">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="errorCode"
                         type="xsd:string"/>
            <xsd:element name="errorDescription"
                         type="xsd:string"/>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
    </xsd:sequence>
  </xsd:complexType>
</xsd:element>

Pro graficky orientované to vypadá takhle:


Tento fault je pak použit ve WSDL:
<wsdl:message name="faultMessage">
    <wsdl:part name="payload" element="inp1:fault"/>
</wsdl:message>

<wsdl:portType name="SOAPFaultPort">
    <wsdl:operation name="test">
        <wsdl:input message="tns:requestMessage"/>
        <wsdl:output message="tns:replyMessage"/>
        <wsdl:fault name="testfault"
                    message="tns:faultMessage"/>
    </wsdl:operation>
</wsdl:portType>
Služba je implementovaná pomocí BPEL procesu, který v rámci orchestrace volá další službu, nazvanou BPELFault:


Tak, to byla expozice. Teď přijde kolize. V BPELu mám definovanou invoke aktivitu, která volá externí službu BPELFault. Chtěl bych použít politiku tak, aby když externí služba vrátí chybu, aby mi politika nastavila můj definovaný SOAP fault a proces ho vrátil klientovi.

Než se pustíme do implementace politiky, musíme v BPEL procesu ještě splnit dvě podmínky. Jednak v procesu definovat proměnnou, která bude mít nastavený typ zprávy jako daný fault:
<variable name="soapFault"
          messageType="ns1:faultMessage"/>
A druhak, proces musí náš fault vyhodit ven pomocí aktivity throw:
<catchAll>
  <throw name="ThrowFault"
         faultName="ns1:testfault"
         faultVariable="soapFault"/>
</catchAll>

Jak to celé funguje?

Obrázek je za tisíc slov, takže tady je BPMN diagram. Swimliny jsou trochu nečitelný :-/ takže odshora: BPEL, External WS, Fault Management Framework a Java Class.


Java

Java třída, která bude z politiky volaná musí implementovat rohraní IFaultRecoveryJavaClass. To má dvě metody, nás bude zajímat pouze handleFault, která má jako parametr IFaultRecoveryContext. Pomocí tohoto kontextu lze přistupovat k objektům v BPEL procesu, odkud vyletěla výjimka.

Z kontextu si tak vytáhneme výše uvedenou BPEL proměnnou soapFault a pomocí XPath, nebo DOMu ji naplníme. Na závěr vrátíme z metody string RETHROW, aby politika vrátila řízení zpátky do BPEL procesu, odkud je pak už vrácena SOAP fault klientovi.

(Omlouvám se, že ten kód uvádím celý. Ale kdyby náhodou to někdo (v Česku) řešil, tak ať to má trochu jednodušší :-)
package cz.swsamuraj.soa.fault;

import com.collaxa.cube.engine.fp.BPELFaultRecoveryContextImpl;

import
   oracle.integration.platform.faultpolicy.IFaultRecoveryContext;
import
   oracle.integration.platform.faultpolicy.IFaultRecoveryJavaClass;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * This class is called by fault policy. Its purpose is to set up
 * a variable which is then throwed as a SOAP fault.
 * 
 * @author Guido
 */
public class SOAPFaultHandler implements IFaultRecoveryJavaClass {

   private static final String VARIABLE = "soapFault";
   private static final String PART = "payload";
   private static final String RESULT_CODE_NAME = "resultCode";
   private static final String RESULT_CODE_VALUE = "255";
   private static final String ERROR_NAME = "error";
   private static final String ERROR_CODE_NAME = "errorCode";
   private static final String ERROR_CODE_VALUE =
                               "EXTERNAL_SERVICE_ERROR";
   private static final String ERROR_DESCRIPTION_NAME =
                               "errorDescription";
   private static final String RESULT = "RETHROW";

   @Override
   public void handleRetrySuccess(
                    IFaultRecoveryContext iFaultRecoveryContext) {
   }

   /**
    * Method for set up a BPEL variable for SOAP fault.
    * 
    * @param recoveryContext
    * @return
    */
   @Override
   public String handleFault(IFaultRecoveryContext recoveryContext) {
      if (recoveryContext instanceof BPELFaultRecoveryContextImpl) {
         BPELFaultRecoveryContextImpl bpelContext =
               (BPELFaultRecoveryContextImpl) recoveryContext;

         Element root =(Element)
                           bpelContext.getVariableData(VARIABLE,
                                                       PART, "/");
         Node soapFault = root.getFirstChild();
         String namespaceURI = soapFault.getNamespaceURI();

         bpelContext.addAuditTrailEntry(String.format(
               "Obtained a root element: {%s}%s.", namespaceURI,
               soapFault.getLocalName()));

         // removes all child nodes
         if (soapFault.hasChildNodes()) {
            int count = soapFault.getChildNodes().getLength();

            for (int i = 0; i < count; i++) {
               Node child = soapFault.getFirstChild();
               soapFault.removeChild(child);
            }
         }

         Document document = soapFault.getOwnerDocument();
         Node resultCode = createNode(document, namespaceURI,
               RESULT_CODE_NAME, RESULT_CODE_VALUE);
         soapFault.appendChild(resultCode);

         Node error = createNode(document, namespaceURI,
                                 ERROR_NAME, null);
         soapFault.appendChild(error);

         Node errorCode = createNode(document, namespaceURI,
                                     ERROR_CODE_NAME,
                                     ERROR_CODE_VALUE);
         error.appendChild(errorCode);

         Node errorDescription = createNode(document, namespaceURI,
                                            ERROR_DESCRIPTION_NAME,
                                            bpelContext.getFault()
                                                       .toString());
         error.appendChild(errorDescription);

         bpelContext.addAuditTrailEntry(String.format(
               "SOAP fault response has been set"
               + " by fault policy '%s'.",
               bpelContext.getPolicyId()));
      }

      return RESULT;
   }

   /**
    * Method creates a new {@link Node} with given parameters.
    * 
    * @param document {@link Document}
    * @param qualifiedName name of the element
    * @param namespaceURI namespace of the element
    * @return {@link Node}
    */
   private Node createNode(Document document, String namespaceURI,
         String qualifiedName, String textContent) {
      Node node = document.createElementNS(namespaceURI,
                                           qualifiedName);

      if (textContent != null) {
         node.setTextContent(textContent);
      }

      return node;
   }
}

Zajímavá je zde asi pouze metoda kontextu addAuditTrailEntry, která propisuje informace do auditního záznamu procesu, což je pak vidět v konzoli Enterprise Managera:


Definice politiky

Už máme ruce, teď mozek. Aby politika byla funkční, je potřeba vytvořit dva soubory. Jednak samotnou definici, soubor fault-policies.xml a potom navázání politiky na konkrétní kompozitiní aplikaci (nebo komponent), soubor fault-bindings.xml. Toto jsou defaultní názvy souborů. Pokud chceme jiné názvy, např. protože chceme politiky verzovat, je potřeba tuto odlišnost uvést v definici kompozitní aplikace.
<property name="oracle.composite.faultPolicyFile">
        fault-policies-v1.0.xml</property>
<property name="oracle.composite.faultBindingFile">
        fault-bindings-v1.0.xml</property>
Nebo graficky:


V definičním souboru fault-policies-v1.0.xml říkáme, že chceme odchytávat výjimku remoteFault (není to standardní chyba definovaná BPELem, ale Oracle BPEL Extension) a chceme, aby byla zpracovaná naší Java třídou:
<?xml version='1.0' encoding='UTF-8'?>
<faultPolicies xmlns="http://schemas.oracle.com/bpel/faultpolicy">
  <faultPolicy version="2.0.1" id="BpelInvokeActivityFaults-v1.0">
    <Conditions>
      <faultName
            xmlns:bpelx="http://schemas.oracle.com/bpel/extension"
            name="bpelx:remoteFault">
        <condition>
          <action ref="ora-java"/>
        </condition>
      </faultName>
    </Conditions>
    <Actions>
      <Action id="ora-java">
        <javaAction
             className="cz.swsamuraj.soa.fault.SOAPFaultHandler"
             defaultAction="ora-rethrow">
          <returnValue value="RETRHOW" ref="ora-rethrow"/>
        </javaAction>
      </Action>
      <Action id="ora-rethrow">
        <rethrowFault/>
      </Action>
    </Actions>
  </faultPolicy>
</faultPolicies>
Ve vazebním souboru fault-bindings-v1.0.xml jenom říkáme, že naše politika BpelInvokeActivityFaults-v1.0 je svázaná s celou kompozitní aplikací:
<?xml version='1.0' encoding='UTF-8'?>
<faultPolicyBindings
       version="2.0.1"
       xmlns="http://schemas.oracle.com/bpel/faultpolicy">
  <composite faultPolicy="BpelInvokeActivityFaults-v1.0"/>
</faultPolicyBindings>

Deployment do runtime

Pokud máme třídu i politické :-) soubory přímo v kompozitní aplikaci, nemusíme deployment nijak řešit - prostě kompozitní aplikaci standardně nasadíme. Pokud však chceme politiky externalizovat (protože je chceme přepoužít pro více kompozitek), je vhodné umístit XML soubory do MDS (Metadata Services repository) a Java třídu nasadit do runtimu SOA Suity:
  1. Zkompilovanou třídu zabalíme do JAR souboru.
  2. JAR soubor nakopírujeme do adresáře <ORACLE_HOME>/Oracle_SOA1/soa/modules/oracle.soa.ext_11.1.1
  3. V tomto adresáři spustíme příkaz ant.
  4. Restartujeme WebLogic.

Související články


Odkazy

14. prosince 2012

SOA Patterns, kniha

SOA je široké téma a kde kdo si do něj schová kde jakou webovou službičku. Určitě proto není na škodu se občas trochu ochytřit a zjistit, "jak se to správně dělá". Jednou z knižních alternativ, po které se dá sáhnout je počin mého oblíbeného vydavatelství Manning, které čersvě vydalo knihu SOA Patterns jejímž autorem je Arnon Rotem-Gal-Oz.

Hned na úvod je potřeba říct, že titul knihy je zavádějící. Daleko přesnější by byl název "Distributed Systems Patterns" s podtitulem "Taking SOA into Account". Nicméně kniha rozhnodně stojí za přečtení a je vhodným doplňkem k jiným "patternovým" knihám. Pokud byste chtěli opravdu SOA patterny, je pravděpodobně jediným dostupným titulem na trhu SOA Design Patterns od Thomase Erla.

Co v knize najdeme? Kniha sleduje klasické schéma návrhových vzorů, takže každý vzor je popsání pomocí sekcí: problém, řešení, technologické mapování a atributy kvality. Vzory (celkem jich je 26) jsou pak seskupeny do jednotlivých kapitol:
  • Foundation structural patterns
  • Patterns for performance, scalability and availability
  • Security and manageability patterns
  • Message exchange patterns
  • Service consumer patterns
  • Service integration patterns

Následují kapitoly s anti-patterny, case study a úplným závěrem je zajímavá kapitola, kde se SOA srovnává s jinými architekturními styly: REST, Cloud computing a Big data.

Jak už jsem psal, SOA je široké téma, takže kniha jej pokrývá pouze z části. Vzhledem k tomu, že titul vyšel u Manningu, tak nepřekvapí, že většina vzorů je "nízkoúrovňových" tzn., že je využijete, pokud bude servisní architekturu implementova from-the-scratch. Tomu odpovídají i příklady uváděné z autorovy praxe - systém pro námořní hlídkování, vyhledávač videí apod., čili dosti specifické systémy, které vyžadují dobrou performance a těžko by se na ně aplikovalo nějaké krabicové řešení.

Pokud ovšem sáhnete po nějaké hotovém/předpřipraveném řešení (a nemusí to být zrovna nějaké dělo od Oracle nebo IBM, ale klidně třeba Apache Camel nebo ServiceMix), tak už tam většina vzorů bude naimplementovaná "pod kapotou". Ovšem samozřejmě, že neškodí vědět, jak to tam sviští.

11. prosince 2012

DOM, Java a odstranění child nodes

Dneska to bude jen takový krátký. Řešil jsem nějaký problém, v rámci kterého se pracovalo s DOMem, do kterého jsem potřeboval přidat nějaké nody. Jak jsem průběžně zjistil, někdy mi do metody přicházel dokument, který už ty nody někdy obsahoval (a někdy ne).

Jako čistý řešení mi přišlo dané nody (včechno to byly child elementy jednoho nodu), pokud tam jsou, odstranit a pak je přidat s potřebnými hodnotami. K tomu jsem se rozhodl vzhledem k API, jež DOM v Javě nabízí - které mi tedy moc použitelné nepřijde.

Dokument, o kterém mluvím má (zjednodušeně) takovouhle strukturu:
<root>
    <child1/>
    <child2/>
    <child3/>
</root>
Obecně jsem čekal, že <root> element bude mít nějakou metodu, která mi vrátí kolekci child nodů. Taková metoda sice existuje - getChildNodes() - ale bohužel vrací interface NodeList:
package org.w3c.dom;

public interface NodeList {
    public Node item(int index);

    public int getLength();
}
Nevím jak vám, ale mě kolekce s takovým rozhraním přijde dost hrozná.

A teď, proč o tom vlastně píšu. Řekl jsem si, OK, získám NodeList, projdu ho v cyklu a jednotlivé nody odstraním. Takže jsem napsal něco takového:
if (root.hasChildNodes()) {
        NodeList children = root.getChildNodes();
        int count = children.getLength();

    for (int i = 0; i < count; i++) {
        Node child = children.item(i);
        root.removeChild(child);
    }
}
Načež mě překvapilo, že mi to vyhazuje NullPointerException. Ono totiž, když se odstraní element z DOMu, tak se odstraní i z daného NodeListu. Divný, ale budiž. Řešením by tedy bylo
Node child = children.item(0);
ale to se mi designově nelíbilo, takže jsem skončil s tímhle kódem:
if (root.hasChildNodes()) {
    int count = root.getChildNodes().getLength();

    for (int i = 0; i < count; i++) {
        Node child = root.getFirstChild();

        root.removeChild(child);
    }
}
A jak byste to řešili vy? Mě by se líbilo nějaké elegantní řešení.

5. prosince 2012

Custom XSLT funkce v Oracle SOA Suite

Už jste se s tím určitě setkali - nejednoznačnost globální definice boolean. (Globální myslím v rámci SOA napříč všemi systémy.) V každém jazyku je to definované trochu jinak. A proto pokud překládám hodnotu z jednoho systému (nebo rozhraní) na jiný, musím použít nějakou konverzi.

Obecně v tomto problému hodně pomůže kanonický datový model (Cannonical Data Model, CDM), ale pořád se nevyhneme tomu překladu (jenom jich bude méně).

Co jsem teď aktuálně řešil (a že nás to na projektu už nějakou chvilku občas drobně trápí), byl překlad boolean typu z XSD na typ v Oracle databázi. Jak asi víte, Oracle DB nemá typ boolean a nejčastěji se to řeší jako integer 0/1, nebo char Y/N apod. Když tak mě v komentářích opravte - nejsem Oraclista (= databázista).

V XSD je to sice jednoznačnější (viz specifikace boolean), ale záleží, jestli pracujeme s kanonickou (true, false), nebo lexikální (true, false, 1, 0) reprezentací.

Protože se tenhle problém čas od času opakoval, rozhodl jsem se napsat uživatelsky definovanou XSLT funkci pro překlad boolean na integer. Platforma, na které se pohybuji je Oracle SOA Suite.

Definice funkce

Protože celá(?) implementace Oracle SOA Suite je v Javě, tak nepřekvapí, že uživatelská XSLT funkce je taky v Javě:
public class CustomFunctions {

   public static int booleanToInt(String bool) {
      int result = 0;

      if ("true".equals(bool) || "1".equals(bool)) {
         result = 1;
      }

      return result;
   }

}
Jsou tam dvě drobná omezení. (A) Metoda musí být definovaná jako statická. (B) Jak návratová hodnota, tak parametry metody musí být pouze následujících typů:
  • java.lang.String
  • int
  • float
  • double
  • boolean
  • oracle.xml.parser.v2.XMLNodeList
  • oracle.xml.parser.v2.XMLDocumentFragment

Ve výčtu je sice uvedný boolean, ale jak jsem zjistil, v rámci XSLT transformace vstupuje do metody String. Proto je v uvedené metodě v parametru String a ne boolean.

Dále potřebujeme mapovací konfigurační soubor. Musí mít následující jméno a umístění:
META-INF/ext-mapper-xpath-functions-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<soa-xpath-functions version="11.1.1"
       xmlns="http://xmlns.oracle.com/soa/config/xpath"
       xmlns:sws="http://www.oracle.com/XSL/Transform/java/cz.swsamuraj.xslt.CustomFunctions">
   <function name="sws:booleanToInt">
      <className>
          cz.swsamuraj.xslt.CustomFunctions
      </className>
      <return type="number"/>
      <params>
         <param name="boolean" type="string"/>
      </params>
      <desc>Method transforms a boolean value to
          integer. True is converted to 1 and false
          to 0.
      </desc>
      <detail>Method transforms a boolean value to
          integer. True is converted to 1 and false
          to 0.
      </detail>
      <icon>cz/swsamuraj/xslt/Boolean.png</icon>
   </function>
</soa-xpath-functions>
Tady je důležité, aby konfigurační soubor obsahoval namespace, který splňuje následující podmínky. Začíná:
http://www.oracle.com/XSL/Transform/java/

a končí plně kvalifikováným názvem Java třídy, tj.:
cz.swsamuraj.xslt.CustomFunctions

Zbytek je předpokládám zřejmý. Pak už stačí zkompilovat třídu, mapovací soubor a ev. resources (ikona) do JAR souboru.



Nastavení IDE

Aby se dala nová funkce používat v IDE (povinný JDeveloper) je potřeba ji tam zaregistrovat:
  1. Tools -> Preferences -> SOA
  2. Tlačítkem Add přidat připravený JAR soubor.
  3. Restartovat JDeveloper.
Pak již stačí otevřít nějaký XSLT soubor a naši novou funkci najdeme v Component Palette pod položkou User Defined:



Deployment do runtime

Nyní již můžeme novou funkci v IDE vyzkoušet (a otestovat), ale aby fungovala také ve službách je potřeba výše vytvořený JAR soubor dostat také na server (SOA Suita běží na WebLogicu):
  1. JAR soubor nakopírujeme do adresáře <ORACLE_HOME>/Oracle_SOA1/soa/modules/oracle.soa.ext_11.1.1
  2. V tomto adresáři spustíme příkaz ant.
  3. Restartujeme WebLogic.

Odkazy