Logo Oracle Deutschland   Application Express Community

APEX und Webservices: Arbeiten mit APEX_WEB_SERVICE

Erscheinungsmonat APEX-Version Datenbankversion
August 2011 ab 4.0 ab 10.2

Web Services verbreiten sich mehr und mehr in den IT-Landschaften; sie sind das zentrale Element service-orientierter Architekturen (SOA). Kern dieser Architektur ist es, bestimmte, häufig benötigte Funktionalität als Web Service bereitzustellen, so dass verschiedenste Applikationen darauf zugreifen können, und zwar unabhängig von der verwendeten Technologie.

Application Express bietet bereits seit den ersten Versionen eine sehr gute Unterstützung für Web Services an. Nutzer einer Standard- oder Enterprise Edition der Datenbank finden in diesem Community-Tipp eine Anleitung zur automatischen Einrichtung eines PL/SQL-Pakets für einen Webservice - die Methoden des Web Service werden dann als Prozeduren bzw. Funktionen des Package eingerichtet. Alles geht vollautomatisch - benötigt wird lediglich die URL zur Webservice-Schnittstellenbeschreibung (WSDL).

Allerdings basiert der beschriebene Weg auf der datenbankinternen Java-Engine und ist damit auf einer OracleXE-Datenbank nicht lauffähig. Für diese Anwender,für solche, die nicht mit der Datenbankinternen JVM arbeiten oder für diejenigen, die den Webservice-Aufruf genau kontrollieren möchten, gibt es seit APEX 4.0 das PL/SQL-Paket APEX_WEB_SERVICE. Im Vergleich zu der auf Java basierenden Variante, die das PL/SQL-Paket vollautomatisch erstellt, muss man hier als Entwickler aber wesentlich mehr selbst machen. Wie es im Detail geht, wird anhand eines praktischen Beispiels erläutert.

Vorbereitungen

Zunächst müssen in der Datenbank die Voraussetzungen für eine Kommunikation mit dem Web Service geschaffen werden.

  • In einer 11g-Datenbank muss das APEX Parsing Schema die nötigen Privilegien in Form einer Netzwerk-ACL erhalten - wie das geht, ist in diesem Community-Tipp beschrieben.
  • Wenn sich Ihre Datenbank hinter einer Firewall befindet, muss der Proxy-Server in den Attributen der APEX-Applikation eingestellt werden. Alternativ kann auch die PL/SQL-Prozedur UTL_HTTP.SET_PROXY verwendet werden.

Webservice-Schnittstelle betrachten

Zum Programmieren mit dem Paket APEX_WEB_SERVICE muss man sich ein wenig mit der Schnittstelle des Web Service auseinandersetzen - als Beispiel wollen wir in diesem Community-Tipp mit dem frei zugänglichen Web Service Global Weather arbeiten.

Webservice "Global Weather"

Webservice "Global Weather"

Wie zu jedem Webservice ist auch hierfür eine Schnittstellenbeschreibung als WSDL-Dokument verfügbar. Leider sind diese auf XML basierenden Dokumente nur sehr schwer lesbar. Man hilft sich am besten mit einem Werkzeug - und hier ist die freie Software soapUI empfehlenswert (Abbildung 1).

Webservice-Werkzeug "soapUI"

Abbildung 1: Webservice-Werkzeug "soapUI"

Navigueren Sie im Menü File zu den Preferences und stellen Sie den Proxy-Server ein, wenn Sie sich hinter einer Firewall befinden. Erzeugen Sie dann ein neues Projekt, geben Sie, wie in Abbildung 1 dargestellt, die URL zum WSDL-Dokument (http://www.webservicex.com/globalweather.asmx?WSDL) als Initial WSDL/WADL ein und klicken Sie auf OK. Das Werkzeug wird das WSDL-Dokument dann laden und die Information für Sie aufbereiten. Wenn Sie Baum links den Eintrag getWeather aufklappen und Request 1 doppelklicken, erscheint auf der rechten Hälfte eine Vorlage für einen SOAP-Request zum Abholen der Wetterdaten (genau dieses XML werden Sie später bei der Arbeit mit APEX_WEB_SERVICE noch brauchen).

Webservice "GlobalWeather" in soapUI

Abbildung 2: Webservice "GlobalWeather" in soapUI

Tragen Sie nun in etwas in die Vorlage ein; nehmen Sie bspw. wie in Abbildung 3 dargestellt, Munich als CityName und Germany als CountryName. Klicken Sie dann auf den kleinen grünen Pfeil links oben - soapUI wird Ihren Request dann an den Server senden und die Antwort auf der rechten Seite darstellen.

Test des Webservice "GlobalWeather" mit soapUI

Abbildung 3: Test des Webservice "GlobalWeather" mit soapUI"

Man sieht, dass die Angaben eines Webservice-Requests in ein XML-Dokument "gepackt" werden müssen - dieses besteht aus einem SOAP-Envelope , welcher wiederum einen Header und einen Body hat. Dies ist für alle SOAP-Requests gleich. Die Angaben innerhalb des Body sind bei jedem Webservice anders und richten sich nach den Angaben im WSDL.

Die Antwort ist ebenfalls in einen Envelope verpackt, der wiederum Header und Body enthält. Auch für die Webservice-Antwort gilt, dass die Struktur innerhalb innerhalb des "Body" vom Webservice selbst abhängt - GlobalWeather liefert eine XML-Unterstuktur zurück; die Experten merken aber sofort, dass es eine sog. CDATA-Section ist; es ist also quasi "ein XML-Dokument in einem XML-Dokument". Zum Verarbeiten der XML-Antwort bietet die Datenbank alle nötigen Funktionen an - SQL-Funktionen wie EXTRACTVALUE oder EXTRACT erlauben das Extrahieren der gewünschten Informationen aus dem XML-Dokument.

Mit diesen Informationen können wir im nächsten Schritt also mit dem PL/SQL-Paket APEX_WEB_SERVICE arbeiten.

Arbeiten mit APEX_WEB_SERVICE

Als generelle Vorgehensweise empfiehlt sich sicherlich, das Paket APEX_WEB_SERVICE nicht direkt in der APEX-Applikation zu nutzen, sondern ein eigenes PL/SQL-Paket für den Web-Service zu schreiben - in diesem wird APEX_WEB_SERVICE dann verwendet. Die APEX-Anwendung arbeitet jedoch mit dem Wrapper-Paket. Für den Webservice GlobalWeather könnte das PL/SQL-Paket dann wie folgt aussehen ...

create or replace type t_cities_table as table of varchar2(4000)
/

create or replace package ws_global_weather is
  procedure set_webservice_url(
    p_new_url in varchar2
  );

  function get_webservice_url return varchar2;

  function get_weather(
    p_city_name in varchar2,
    p_country_name in varchar2
  ) return xmltype;
 
  function get_cities_by_country(
    p_country_name in varchar2
  ) return t_cities_table;
end ws_global_weather;

Die Package-Spezifikation entspricht nun recht genau der Webservice-Beschreibung; Mit Hilfe von APEX_WEB_SERVICE werden wir es nun implementieren. Die eigentliche Arbeit wird dabei von APEX_WEB_SERVICE.MAKE_REQUEST erledigt.

FUNCTION MAKE_REQUEST RETURNS XMLTYPE
 Argument Name                  Typ                     In/Out Defaultwert?
 ------------------------------ ----------------------- ------ --------
 P_URL                          VARCHAR2                IN
 P_ACTION                       VARCHAR2                IN     DEFAULT
 P_VERSION                      VARCHAR2                IN     DEFAULT
 P_ENVELOPE                     CLOB                    IN
 P_USERNAME                     VARCHAR2                IN     DEFAULT
 P_PASSWORD                     VARCHAR2                IN     DEFAULT
 P_PROXY_OVERRIDE               VARCHAR2                IN     DEFAULT
 P_WALLET_PATH                  VARCHAR2                IN     DEFAULT
 P_WALLET_PWD                   VARCHAR2                IN     DEFAULT

Hier erkennen Sie vielleicht den Parameter P_ENVELOPE wieder - hier wird das XML-Dokument, welches Sie im soapUI als Request erstellt und bearbeitet haben, übergeben. Die Antwort, die als SOAP-Envelope (Abbildung 3 rechts) gesendet wird, wird von MAKE_REQUEST als XMLTYPE zurückgegeben. APEX_WEB_SERVICE.PARSE_XML erlaubt dann das Zerlegen der Struktur - die Wetterinformationen werden also aus dem SOAP-Envelope extrahiert (in diesem Fall ergibt sich allerdings wiederum ein XML-Dokument). Die Nutzung des WebService in einem einfachen anonymen PL/SQL-Block könnte also so aussehen ..

begin
  utl_http.set_proxy('{proxy-host}:{proxy-port}');
end;
/

declare
  l_envelope clob := 
  '<?xml version="1.0" encoding="UTF-8"?>
   <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
                  xmlns:tns="http://www.webserviceX.NET"
                  xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <soap:Body>
     <tns:GetWeather>
      <tns:CityName>Munich</tns:CityName>
      <tns:CountryName>Germany</tns:CountryName>
     </tns:GetWeather>
    </soap:Body>
   </soap:Envelope>';
  l_response_xml      xmltype;
  l_weather_response varchar2(4000);
begin
  l_response_xml := apex_web_service.make_request(
     p_url => 'http://www.webservicex.com/globalweather.asmx',
     p_action => 'http://www.webserviceX.NET/GetWeather',
     p_envelope => l_envelope
  );
  l_weather_response := apex_web_service.parse_xml(
    p_xml   => l_response_xml,
    p_xpath => '//GetWeatherResult/text()',
    p_ns    => 'xmlns="http://www.webserviceX.NET"'
  );
  
  dbms_output.put_line(l_weather_response);
end;
/

<?xml version="1.0" encoding="utf-16"?>
<CurrentWeather>
  <Location>Munchen, Germany (EDDM) 48-21N 011-47E</Location>
  <Time>Aug 12, 2011 - 03:50 AM EDT / 2011.08.12 0750 UTC</Time>
  <Wind> from the W (270 degrees) at 13 MPH (11 KT):0</Wind>
  <Visibility> greater than 7 mile(s):0</Visibility>
  :

Allerdings passt dies nicht zu der Package-Spezifikation, die wir weiter oben schon festgelegt haben. Basierend auf dem anonymen Block lässt sich das Package aber nun recht schnell implementieren (Skript-Download). Der folgende Code implementiert auch die Methode getCitiesByCountry des Webservice. Hier wird die XML-Antwort des Webservice auch schon direkt in ein PL/SQL Array übernommen.

create or replace package body ws_global_weather is
  g_ws_url varchar2(500) := 'http://www.webservicex.com/globalweather.asmx';

  g_env_getweather varchar2(32767) := 
   '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                      xmlns:web="http://www.webserviceX.NET">
       <soapenv:Header/>
       <soapenv:Body>
          <web:GetWeather>
             <web:CityName>#CITYNAME#</web:CityName>
             <web:CountryName>#COUNTRYNAME#</web:CountryName>
          </web:GetWeather>
       </soapenv:Body>
    </soapenv:Envelope>';
 
  g_env_getcities varchar2(32767) := 
   '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                      xmlns:web="http://www.webserviceX.NET">
       <soapenv:Header/>
       <soapenv:Body>
          <web:GetCitiesByCountry>
             <web:CountryName>#COUNTRYNAME#</web:CountryName>
          </web:GetCitiesByCountry>
       </soapenv:Body>
    </soapenv:Envelope>';

  procedure set_webservice_url(
    p_new_url in varchar2
  ) is begin
    g_ws_url := p_new_url;
  end set_webservice_url;

  function get_webservice_url return varchar2 is  
  begin
    return g_ws_url;
  end get_webservice_url;

  function get_weather(
    p_city_name in varchar2,
    p_country_name in varchar2
  ) return xmltype is
    l_soap_response xmltype;
    l_soap_env      varchar2(32767) := g_env_getweather;

    l_weather_xml   xmltype;
  begin
    -- Parameter in SOAP Enveloper einfügen
    l_soap_env := replace(l_soap_env, '#CITYNAME#', p_city_name);
    l_soap_env := replace(l_soap_env, '#COUNTRYNAME#', p_country_name);
    
    -- SOAP-Request durchführen
    l_soap_response := apex_web_service.make_request(
      p_url         => g_ws_url,
      p_action      => 'http://www.webserviceX.NET/GetWeather',
      p_envelope    => l_soap_env
    );
    
    -- SOAP-Response parsen und das Ergebnis extrahieren
    l_weather_xml := xmltype(
     apex_web_service.parse_xml(
      p_xml   => l_soap_response,
      p_xpath => '//GetWeatherResult/text()',
      p_ns    => 'xmlns="http://www.webserviceX.NET"'
     )
    );
    return l_weather_xml;
  end get_weather;

  function get_cities_by_country(
    p_country_name in varchar2
  ) return t_cities_table is
    l_soap_response xmltype;
    l_soap_env      varchar2(32767) := g_env_getcities;

    l_cities_xml    clob;
    l_cities        t_cities_table;
  begin
    -- Parameter in SOAP Enveloper einfügen
    l_soap_env := replace(l_soap_env, '#COUNTRYNAME#', p_country_name);
    
    -- SOAP-Request durchführen
    l_soap_response := apex_web_service.make_request(
      p_url         => g_ws_url,
      p_action      => 'http://www.webserviceX.NET/GetCitiesByCountry',
      p_envelope    => l_soap_env
    );

    -- SOAP-Response parsen und das Ergebnis extrahieren
    l_cities_xml := apex_web_service.parse_xml_clob(
      p_xml   => l_soap_response,
      p_xpath => '//GetCitiesByCountryResult/text()',
      p_ns    => 'xmlns="http://www.webserviceX.NET"'
    );
    
    -- Ergebnis ist "in XML gepacktes XML" - also müssen die maskierten XML-Tags
    -- "unmaskiert" werden -> &lt; nach <, &gt; nach >, &amp; nach &
 
    l_cities_xml := dbms_xmlgen.convert(
     xmldata => l_cities_xml,
     flag    => dbms_xmlgen.entity_decode
    );

    -- Ergebnis-XML des Webservice parsen und in ein PL/SQL-Array übernehmen.
    select 
     extractvalue(value(c), '/Table/City/text()')
     bulk collect into l_cities
    from table(xmlsequence(extract(xmltype(l_cities_xml), '//Table'))) c;

    return l_cities;
  end get_cities_by_country;
end ws_global_weather;

Den Web Service wie ein PL/SQL-Paket nutzen

Das fertige PL/SQL-Paket sieht nach dem Einspielen wie folgt aus:

SQL> desc ws_global_weather

FUNCTION GET_CITIES_BY_COUNTRY RETURNS T_CITIES_TABLE
 Argument Name                  Typ                     In/Out Defaultwert?
 ------------------------------ ----------------------- ------ --------
 P_COUNTRY_NAME                 VARCHAR2                IN
FUNCTION GET_WEATHER RETURNS XMLTYPE
 Argument Name                  Typ                     In/Out Defaultwert?
 ------------------------------ ----------------------- ------ --------
 P_CITY_NAME                    VARCHAR2                IN
 P_COUNTRY_NAME                 VARCHAR2                IN
FUNCTION GET_WEBSERVICE_URL RETURNS VARCHAR2
PROCEDURE SET_WEBSERVICE_URL
 Argument Name                  Typ                     In/Out Defaultwert?
 ------------------------------ ----------------------- ------ --------
 P_NEW_URL                      VARCHAR2                IN

Die beiden Funktionen können nun wie normale PL/SQL-Funktionen verwendet werden; die Implementierung als eigenes Package ist zwar ein wenig Arbeit, lohnt sich aber bereits bei der zweiten Nutzung des Web-Service. Gerade bei der Art und Weise, wie das PL/SQL-Package erstellt wird, unterscheidet sich die hier beschrieben Arbeit mit APEX_WEB_SERVICE von der Variante basierend auf der datenbankinternen Java VM. Bei letzterer macht das jpub-Werkzeug die ganze Arbeit - mit SOAP-Envelopes muss man sich dort nicht herumschlagen. Im Gegenzug hat man bei der Variante mit APEX_WEB_SERVICE mehr Kontrolle über das Package - denn man baut es eben selbst. Die "richtige" hängt von den konkreten Anforderungen ab. Die Nutzung des Pakets erfolgt, wie schon gesagt, wie mit jedem anderen PL/SQL-Paket und funktioniert genauso wie bei der Variante basierend auf der datenbankinternen Java VM - dort beschrieben im Abschnitt Den Web Service wie ein PL/SQL-Paket nutzen. Hier einige Beispiele - zunächst werden alle "Cities" in Deutschland abgerufen:

SQL> select * from table(ws_global_weather.get_cities_by_country('Germany'));

COLUMN_VALUE
------------------------------------------------------------------------
Berlin-Schoenefeld
Dresden-Klotzsche
Erfurt-Bindersleben
Frankfurt / M-Flughafen
Muenster / Osnabrueck
Hamburg-Fuhlsbuettel
Berlin-Tempelhof
Koeln / Bonn
Duesseldorf
Munich / Riem
Nuernberg
:

Als nächstes wird das Wetter am Flughafen Hamburg abgerufen ...

SQL> select ws_global_weather.get_weather('Hamburg-Fuhlsbuettel','Germany') from dual;

WS_GLOBAL_WEATHER.GET_WEATHER('HAMBURG-FUHLSBUETTEL','GERMANY')
--------------------------------------------------------------------------------
<?xml version="1.0" encoding="CP850"?>
<CurrentWeather>
  <Location>Hamburg-Fuhlsbuettel, Germany (EDDH) 53-38N 010-00E 15M</Location>
  <Time>Aug 12, 2011 - 06:20 AM EDT / 2011.08.12 1020 UTC</Time>
  <Wind> from the W (260 degrees) at 6 MPH (5 KT):0</Wind>
  <Visibility> greater than 7 mile(s):0</Visibility>
  <SkyConditions> mostly cloudy</SkyConditions>
  <Temperature> 62 F (17 C)</Temperature>
  <DewPoint> 62 F (17 C)</DewPoint>
  <RelativeHumidity> 100%</RelativeHumidity>
  <Pressure> 29.74 in. Hg (1007 hPa)</Pressure>
  <Status>Success</Status>
</CurrentWeather>

Zurück zur Community-Seite