Hi,
SOAP is something I put on my balls.
von Charles "cowboyd" Lowell aus Austin in Texas im DrunkAndRetired Podcast, Folge 85.
das ist wirklich keine dumme Aussage.
Viele sagen zur Zeit, dass SOAP für die meisten Fälle einen völligen overkill darstellt, obwohl es zugegebnermassen eine gute Tool-Unterstützung hat.
Meistens möchte man:
- Daten in xml packen.
- Diese Daten über http oder https an einen Server schicken
- vom Server xml als Antwort zurückbekommen
Nur wird heute die SOAP Spec als etwas kritisiert, bei der alles denkbare konfigurierbar sein soll.
Und genau das führt natürlich zu sehr viel Komplexität.
Das zur Zeit fortschrittlichste und einfachste SOAP Framework generiert aus diesem unschuldigen remote Kommunikationsinterface.
package de.kingmedia.daw.bo;
import java.util.Date;
import java.util.List;
/**
* @author Axel
*
*/
public interface DiscountService {
String requestDiscount(String idOffer, String nameAgent, int discountReq, boolean urgency, String note);
DiscountRequest retrieveRequestDiscountResponse(String idOffer);
List <DiscountRequest> retrieveRequestDiscountListing(Date beginDate, Date endDate, String status);
}
dieses Monster-WSDL:
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions targetNamespace="http://kingmedia.de/DiscountService" xmlns:tns="http://kingmedia.de/DiscountService" xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="http://bo.daw.kingmedia.de" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenc11="http://schemas.xmlsoap.org/soap/encoding/" xmlns:soapenc12="http://www.w3.org/2003/05/soap-encoding" xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
<wsdl:types>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://kingmedia.de/DiscountService">
<xsd:element name="retrieveRequestDiscountResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="1" name="in0" nillable="true" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="retrieveRequestDiscountResponseResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="1" name="out" nillable="true" type="ns1:DiscountRequest"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="requestDiscount">
<xsd:complexType>
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="1" name="in0" nillable="true" type="xsd:string"/>
<xsd:element maxOccurs="1" minOccurs="1" name="in1" nillable="true" type="xsd:string"/>
<xsd:element maxOccurs="1" minOccurs="1" name="in2" type="xsd:int"/>
<xsd:element maxOccurs="1" minOccurs="1" name="in3" type="xsd:boolean"/>
<xsd:element maxOccurs="1" minOccurs="1" name="in4" nillable="true" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="requestDiscountResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="1" name="out" nillable="true" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="retrieveRequestDiscountListing">
<xsd:complexType>
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="1" name="in0" type="xsd:dateTime"/>
<xsd:element maxOccurs="1" minOccurs="1" name="in1" type="xsd:dateTime"/>
<xsd:element maxOccurs="1" minOccurs="1" name="in2" nillable="true" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="retrieveRequestDiscountListingResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="1" name="out" nillable="true" type="ns1:ArrayOfDiscountRequest"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://bo.daw.kingmedia.de">
<xsd:complexType name="DiscountRequest">
<xsd:sequence>
<xsd:element minOccurs="0" name="m_beginDate" type="xsd:dateTime"/>
<xsd:element minOccurs="0" name="m_discountAllowed" type="xsd:int"/>
<xsd:element minOccurs="0" name="m_discountReq" type="xsd:int"/>
<xsd:element minOccurs="0" name="m_history" nillable="true" type="ns1:ArrayOfDiscountRequest"/>
<xsd:element minOccurs="0" name="m_idAgreeement" nillable="true" type="xsd:string"/>
<xsd:element minOccurs="0" name="m_idOffer" nillable="true" type="xsd:string"/>
<xsd:element minOccurs="0" name="m_nameAgent" nillable="true" type="xsd:string"/>
<xsd:element minOccurs="0" name="m_note" nillable="true" type="xsd:string"/>
<xsd:element minOccurs="0" name="m_status" nillable="true" type="xsd:string"/>
<xsd:element minOccurs="0" name="m_urgency" type="xsd:boolean"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="ArrayOfDiscountRequest">
<xsd:sequence>
<xsd:element maxOccurs="unbounded" minOccurs="0" name="DiscountRequest" nillable="true" type="ns1:DiscountRequest"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
</wsdl:types>
<wsdl:message name="requestDiscountResponse">
<wsdl:part name="parameters" element="tns:requestDiscountResponse">
</wsdl:part>
</wsdl:message>
<wsdl:message name="retrieveRequestDiscountResponseResponse">
<wsdl:part name="parameters" element="tns:retrieveRequestDiscountResponseResponse">
</wsdl:part>
</wsdl:message>
<wsdl:message name="retrieveRequestDiscountListingResponse">
<wsdl:part name="parameters" element="tns:retrieveRequestDiscountListingResponse">
</wsdl:part>
</wsdl:message>
<wsdl:message name="requestDiscountRequest">
<wsdl:part name="parameters" element="tns:requestDiscount">
</wsdl:part>
</wsdl:message>
<wsdl:message name="retrieveRequestDiscountResponseRequest">
<wsdl:part name="parameters" element="tns:retrieveRequestDiscountResponse">
</wsdl:part>
</wsdl:message>
<wsdl:message name="retrieveRequestDiscountListingRequest">
<wsdl:part name="parameters" element="tns:retrieveRequestDiscountListing">
</wsdl:part>
</wsdl:message>
<wsdl:portType name="DiscountServicePortType">
<wsdl:operation name="retrieveRequestDiscountResponse">
<wsdl:input name="retrieveRequestDiscountResponseRequest" message="tns:retrieveRequestDiscountResponseRequest">
</wsdl:input>
<wsdl:output name="retrieveRequestDiscountResponseResponse" message="tns:retrieveRequestDiscountResponseResponse">
</wsdl:output>
</wsdl:operation>
<wsdl:operation name="requestDiscount">
<wsdl:input name="requestDiscountRequest" message="tns:requestDiscountRequest">
</wsdl:input>
<wsdl:output name="requestDiscountResponse" message="tns:requestDiscountResponse">
</wsdl:output>
</wsdl:operation>
<wsdl:operation name="retrieveRequestDiscountListing">
<wsdl:input name="retrieveRequestDiscountListingRequest" message="tns:retrieveRequestDiscountListingRequest">
</wsdl:input>
<wsdl:output name="retrieveRequestDiscountListingResponse" message="tns:retrieveRequestDiscountListingResponse">
</wsdl:output>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="DiscountServiceHttpBinding" type="tns:DiscountServicePortType">
<wsdlsoap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="retrieveRequestDiscountResponse">
<wsdlsoap:operation soapAction=""/>
<wsdl:input name="retrieveRequestDiscountResponseRequest">
<wsdlsoap:body use="literal"/>
</wsdl:input>
<wsdl:output name="retrieveRequestDiscountResponseResponse">
<wsdlsoap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
<wsdl:operation name="requestDiscount">
<wsdlsoap:operation soapAction=""/>
<wsdl:input name="requestDiscountRequest">
<wsdlsoap:body use="literal"/>
</wsdl:input>
<wsdl:output name="requestDiscountResponse">
<wsdlsoap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
<wsdl:operation name="retrieveRequestDiscountListing">
<wsdlsoap:operation soapAction=""/>
<wsdl:input name="retrieveRequestDiscountListingRequest">
<wsdlsoap:body use="literal"/>
</wsdl:input>
<wsdl:output name="retrieveRequestDiscountListingResponse">
<wsdlsoap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="DiscountService">
<wsdl:port name="DiscountServiceHttpPort" binding="tns:DiscountServiceHttpBinding">
<wsdlsoap:address location="http://127.0.0.1:8080/DAWProto1/services/DiscountService"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
Klar. Das wird von Tools generiert. Aber es führt eine Komplexität in die Sache ein, die für sehr viele Fälle gar nicht nötig ist.
Warum nicht einfach selbst
- ein bischen xml generieren
- das gegen den Server senden (geht seit Notes5 mit Java oder anderen Mitteln, aber nicht mit Vanilla LotusScript)
- der Server sendet als Response xml zurück, das der Client verarbeiten kann.
Sowohl auf Client als auch auf Server Seite ist das mit Lotus Notes möglich. Mit Hilfe von Java oder anderen Mitteln. Nicht aber mit Vanilla LotusScript.
In der Folge (nicht heute) wird die Möglichkeit einer Alternativ-Implementierung ohne SOAP mit Notes7 dargestellt.
Gruß Axel
Proof of concept mässig geht das nämlich ohne dieses ganze SOAP Gedöns über HTTP.
Ich werd das noch full cycle ausarbeiten.
Das heisst:
Vom Notes Client wird mit Hilfe von LS2J und apache.jakarta.HTTPClient basierend auf (erstmal) basic authentification ein Post Request mit HTTP gegen einen Domino Server gesendet, der dann die gewünschten Daten ausliest und der dann xml zurückliefert. Das xml ist eingepackt in html. Ich werd aber in die nsf nicht die benötigten jakarta apache jars dazupacken, weil die zu groß sind. Müssen dann interessierte schon selber machen. Ich erklär dann auch wie.
Hier erstmal das (funktionierende) Proof of Concept.
Der LotusScript Notes Agent poxProducer (sendet das eingesendete xml einfach zurück, Verarbeitung muss noch programmiert werden):
Sub Initialize
Dim s As New NotesSession
Dim doc As NotesDocument
Set doc = s.DocumentContext()
'Print "remoteUser=" + doc.Remote_User(0)
Print doc.Request_Content(0) ' hier stehen die per HTTP-POST Request eingesandten Daten drinnen.
End Sub
Webservice Requester: (aus einem Eclipse Projekt):
package de.aja.http.client;
import java.io.IOException;
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.params.HttpMethodParams;
public class WebConnector {
HttpClient client = new HttpClient();
PostMethod method = null;
public void initClient(String userName, String pwd) {
// basic authentification :-)
client.getState().setCredentials(
new AuthScope(null, 80, null),
new UsernamePasswordCredentials(userName, pwd)
);
}
public void createMethod(String url) {
method = new PostMethod(url);
method.setDoAuthentication( true );
//method.addParameter("foo", "bar");
method.setRequestEntity(new StringRequestEntity("<foo><bar>value</bar></foo>"));
}
public String connect() {
try {
// set per default -> retries 3 times when recoverable error is thrown
client.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
new DefaultHttpMethodRetryHandler());
client.executeMethod(method);
// nicht die feine englische ... for domino its simpler but to process a stream or byte (get.
return method.getResponseBodyAsString();
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
// release the connection
if (method!= null) {
method.releaseConnection();
}
}
}
}
Der client code von dieser Klasse sieht so aus (ein mit Junit geschriebener Integrationstest). Da ist ziemlich viel Eclipse generierter Boilerplate code dabei wichtig ist die Methode testConnect:
package de.aja.http.client;
import org.apache.log4j.Logger;
import junit.framework.TestCase;
public class WebConnectorTest extends TestCase {
/**
* Logger for this class
*/
private static final Logger logger = Logger.getLogger(WebConnectorTest.class);
WebConnector cut = new WebConnector();
public WebConnectorTest(String arg0) {
super(arg0);
}
protected void setUp() throws Exception {
super.setUp();
}
protected void tearDown() throws Exception {
super.tearDown();
}
public void testConnect() {
cut.initClient("admin Axel", "voll_geheim");
cut.createMethod("http://127.0.0.1/PoxServer.nsf/poxProducer");
String res = cut.connect();
logger.debug("retrieved-->\n" + res);
}
}
ssl liesse sich auch locker einbinden.
Ich hab mir überlegt, das auch der client als Lotus Notes Agent implementiert wird. Das ist realistischer. LS2J bringt ein paar wenige Komplexitäten mit sich, die für das Beispiel nicht nötig sind.
Wenn ihr in der realen Welt den Client für einen Webservice, der z.B. mit einer php Anwendung kommuniziert, dann werdet ihr das am wahrscheinlichsten in einem Agenten machen.
Was soll jetzt der Webservice inhaltlich tun. Stock Quotes wie sonst immer ??? Nein. ;D
Ich implementiere das als Math-Service für simple Rechenoperationen mit beliebig vielen Termen (spelling?), Klammern aber ohne Division. Auf vielen Blogs (z.B. bileblog, basicthinking) wird man heute schon mit solchen Fragen als spamschutz konfrontiert.
Z.B.:
Berechne [zehn] + [neun](ohne Taschenrechner)
Der business Nutzen des Webservice ist zugegeben nicht sehr hoch, aber als Beispiel sehr brauchbar.
Die einzelnen wiederverwendbaren Teile werden so aussehen. Das ist ein highlevel Überblick über die einzelnen Teile des POX Webservice.
| Client | Server |
| 1. Client liest Daten aus einem Notes Dokument und erzeugt daraus ein xml Dokument (Java) | |
| 2. Client sendet das erzeugte xml Dokument per HTTP an den Server (Java) | |
| 3. Server nimmt xml entgegen (LotusScript Agent) |
| 4. Server parst das xml mit der SAX API und erzeugt daraus Datenstruktur für die Weiterverarbeitung (LotusScript) |
| 5. Server verarbeitet die Daten und generiert daraus seinerseits xml, das an den Client zurückgeschickt wird (mit LotusScript print statements) |
| 6. Client parst seinerseits das vom Server zurückgesendete xml mit der SAX Api (Java) und erzeugt daraus aplikationsspezifische Datenstrukturen als Ergebnis | |
| 7. Client schreibt das Ergebnis in das Notes Dokument | |
Das auszutauschende xml wird etwa diese Struktur haben (Beispiel):
client sendet an server:
<?xml version="1.0"?>
<math-in>
<number parentesis="left">8</number>
<operator>+</operator>
<number parentesis="right">2</number>
<operator>*</operator>
<number>4</number>
</math-in>
server sendet zurück:
<?xml version="1.0"?>
<math-out>
40
</math-out>
Im Agent "poxServer" der unten angehängten Datenbank ist Punkt "4. Server parst das xml in eine Datenstruktur" mit SAX Parsing implementiert. Als LotusScript. Die SAX-API eignet sich sehr gut, um xml zu lesen. Man kann damit nicht xml schreiben oder editieren. Die SAX-API wurde in sehr vielen Programmiersprachen implementiert. Wenn man SAX-Parsing in LotusScript kann, ist es ein kleiner Schritt das auch in C#, Java, C++, Ruby oder was auch immer zu beherrschen.
SAX ist eine eventgesteuerte API. Man kann sich das so ähnlich vorstellen wie Lotus Notes ein Dokument verarbeitet. Öffnest du ein xml Dokument im Notes Client, dann werden:
Eine Menge Maskenevents aufgerufen (QueryOpen, PostOpen) und die Formeln der einzelnen Felder von links oben nach rechts unten werden berechnet.
So ähnlich verhält es sich mit dem Handler eines xml Parsers.
Es gibt verschiedene Events, die man anmelden kann. Zum Beispiel:
On Event SAX_StartDocument from saxParser Call SAXStartDocument
On Event SAX_StartElement From saxParser Call SAXStartElement
On Event SAX_Characters From saxParser Call SAXCharacters
On Event SAX_EndElement From saxParser Call SAXEndElement
Die komplette Liste findet ihr irgendwo in der Notes Designer Dokumentation.
Im Parsingprozess werden dann die angemeldeten Unterroutinen aufgerufen.
(z.B. Sub SAXEndElement (Source As Notessaxparser, Byval ElementName As String))
Also das ist ein xml Dokument:
<?xml version="1" encoding="UTF-8"?>
<math-in>
<number parentesis="left">8</number>
<operator>+</operator>
<number parentesis="right">2</number>
<operator>*</operator>
<number>4</number>
</math-in>
Der SAX Parser geht das von oben bis unten durch und ruft bei definierten "Stellen" angemeldete Subroutinen auf. Er macht das automatisch. Wir müssen nur auf ihn in den angemeldeten Subroutinen reagieren.
Mit den oben Beispielhaft angegebenen Events, sieht das so aus:
1. Parsing beginnt-> Ruft Call SAXStartDocument auf.
2. kommt an den öffnenden Tag <math> -> Ruft SAXStartElement auf
3. Kommt an den öffnenden Tag <number> -> Ruft SAXStartElement auf
4. Findet Text zwischen den öffnenden und schliessenden Tags <number> und </number>-> Ruft SAXCharaters auf.
5. Findet den schliessenden Tag </number> -> Ruft SAXSEndElement auf
3. Kommt an den öffnenden Tag <operator> -> Ruft SAXStartElement auf
4. Findet Text zwischen den öffnenden und schliessenden Tags <operator> und </operator>-> Ruft SAXCharaters auf.
5. Findet den schliessenden Tag </operator> -> Ruft SAXSEndElement auf
Und so weiter. Bis zum letzten schliessenden Tag </math>
Nun gibt es z.B. in den <number> tags Attribute. Was ist damit?
<number parentesis="left">
Nun die einzelnen angemeldeten Event-Subroutinen besitzen definierte Parameter.
Zum Beispiel:
Sub SAXStartElement (Source As Notessaxparser,_
Byval elementname As String, Attributes As NotesSaxAttributeList)
...
End Sub
In jedem Subroutinenaufruf von SAXStartElement wird
a) der Name des Tags als String übergeben (bei <math> wäre das math.
b) die Attribute des Tags als NotesSaxAttributeList übergeben, wobei NotesSaxAttributeList ein einfaches LotusScript Objekt ist. In der angehängten Datenbank könnt ihr euch anschauen wie das genauer verarbeitet wird.
Es ist zugegeben ein bischen gewöhnungsbedürftig, aber zum Lesen von xml Dokumenten ist die SAX Api oft einfacher als die DOM-Api oder traditionelles String-Parsing. Und zwar bedeutend.
Ich beschreibe jetzt noch ein wenig konkrete Implementierung des Agenten poxParser in der angehängten Datenbank:
Der Agent holt sich von der Funktion getStrMockIn() as String ein XML-Dokument als String zum Testen. Dieser String wird in einen NotesStream geladen (sehr einfach)
Set stream = session.CreateStream
stream.WriteText(strIn)
Dann wird die Funktion
Function xmlProcessIn (streamIn As NotesStream) As Variant
aufgerufen, die das SAXParsing steuert.
Das eigentlich SAXParsing findet dann in
Class MySaxContentHandler
in den Declarations statt.
Ein Objekt dieser Klasse wird von xmlProcessIn erzeugt. Der Konstruktor
Sub new (streamIn As NotesStream)
wird aufgerufen. Dort wird ein saxParser as NotesSAXParser Objekt erzeugt, die events werden angemeldet.
Sub new (streamIn As NotesStream)
' the data to be retrieved later is saved here
Redim Preserve mathElem(0)
Set sAXParser = session.CreateSAXParser(streamIn)
' the events to be observed during parsing. calls the subroutines below.
On Event SAX_StartElement From saxParser Call SAXStartElement
On Event SAX_Characters From saxParser Call SAXCharacters
On Event SAX_EndElement From saxParser Call SAXEndElement
On Event SAX_Error From saxParser Call SAXError
On Event SAX_FatalError From saxParser Call SAXFatalError
On Event SAX_Warning From saxParser Call SAXWarning
End Sub
von xmlProcessIn wird dann als nächstes der ParsingProzess gestartet.
xmlProcessIn:
Set instMySaxContentHandler = New MySaxContentHandler(streamIn)
instMySaxContentHandler.process
In Class MySaxContentHandler:
Function process () As Variant
saxParser.process
End Function
Die Kontrolle geht nun auf den SAXParser über. Er ruft nun die angemeldeten Event-Funktionen Call SAXStartElement, Call SAXCharacters, etc auf, sobald er während des Parsens von oben nach unten auf ein entsprechendes "Event" "stösst" (z.B. öffnender Tag, Text zwischen öffnenden und schliessenden Tags, schliessender Tag, etc).
In den aufgerufenen Subroutinen wird nun ein Array des ebenfalls in den Declaration definierten Types Math Element erzeugt
Type MathElement
name As String
attribute As String
value As String
End Type
In der Instanz der Klasse MySaxContentHandler ist dieser Array eine Public Member-Variable:
Class MySaxContentHandler
Public mathElem() As MathElement
[...]
Hat der SAXParser sämtliche Elemente von oben nach unten durchgearbeitet fällt die Programmkontrolle wieder zurück auf die oben erwähnte Function xmlProcessIn zurück, die den Parsing Prozess angestossen hat. Diese Funktion gibt dann noch die Werte des Arrays mathElem aus.
For i= 1 To Ubound(instMySaxContentHandler.mathElem())
currentMathElement = instMySaxContentHandler.mathElem(i)
'Msgbox instMySaxContentHandler.arrValues(i), MB_ICONINFORMATION, instMySaxContentHandler.arrKeys(i)
strDebug = strDebug & Chr$(13) & Chr$(10) & |MathElement(| & Cstr(i) & |)-->name="| & currentMathElement.name &_
|" value="| & currentMathElement.value & |" attribute="| & currentMathElement.attribute & |"|
'& |"|
Next
Msgbox strDebug, MB_ICONINFORMATION, "result"
Interessierte sollten einfach den Agenten mal ausprobieren.
Das war Schritt 4:
4. Server parst das xml in eine Datenstruktur
Input: xml Dokument als String
Output: Datenstruktur in LotusScript (hier wirds erstmal ein Array eines Types sein)
Nicht für jeden der angesprochenen Schritte muss so viel Code geschrieben werden.
Gruß Axel
Julians Lösung benutzt keine 3rd party openSource Bibliotheken wie jakarta.commons HTTPClient.
Das mindert natürlich den Aufwand für das Deployment.
Auf der anderen Seite ist das ziemlich low level und das kann leicht dazu führen, dass bestimmte Features nicht unterstützt sind und Fehler drin sind.
Die zentralen Klassen befinden sich in den Scriptlibraries. (Z.B. URLFetcher).
Aus meiner Sicht wird in
public InputStream getUrlAsStream (String urlToGet)
das writer-Objekt nicht korrekt geschlossen.
if (postParams.length() > 0) {
con.setDoOutput(true);
OutputStreamWriter writer = new OutputStreamWriter(con.getOutputStream());
writer.write(postParams);
writer.flush();
}
Hier müsste nach flush() noch ein close() kommen. So was kann echt in Betrieb zu Problemen führen.
Die Lösung von Julian unterstützt eine Menge Features (basic authentification, ssl, proxies). Ich vermisse unterschiedliche Encoding schemes wie ISO-8859-1 oder UTF-8. In Webservice Projekten kommt es schnell vor, dass man sich damit beschäftigen muß. Aber auch dieser Support liesse sich leicht einbauen. Ich hab mich jedenfalls auf Julians blog höflich zu Wort gemeldet und werde vermutlich selbst eine Alternative bloggen, sobald das hier fertig ist.
Julian bietet die Datenbank als fertige nsf zum Download an.
Einfach auf data-root* eines Dominoserver kopieren. Unterzeichnen, Datei, Extras, Java Debug Console öffnen und den Agenten "Echo Webservice Test (Java)" starten und in Java Debug Console schauen. Dieser Agent benutzt die Skriptbibliothek URLFetcher und spricht den Domino-7-Webservice EchoTest derselben Datenbank an.
Folgendes wird zurückgeliefert:
Als response:
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
<soapenv:Body>
<ns1:ECHOResponse soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:ns1="urn:DefaultNamespace"><ECHOReturn xsi:type="xsd:string">ECHO</ECHOReturn></ns1:ECHOResponse>
</soapenv:Body>
</soapenv:Envelope>
Als fetcher.getHeadersReceived():
HTTP HEADERS RECEIVED
HTTP/1.1 200 OK
Server: Lotus-Domino
Date: Sat, 09 Jun 2007 07:58:04 GMT
Content-Type: text/xml; charset=utf-8
Content-Length: 515
Wie man bei response sieht, wird bei Webservices eine Menge "Paketmaterial" geliefert, um eine einfache Information einzupacken.
Inhalt:
"Paketmaterial":
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
<soapenv:Body>
<ns1:ECHOResponse soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:ns1="urn:DefaultNamespace"><ECHOReturn xsi:type="xsd:string"> </ECHOReturn></ns1:ECHOResponse>
</soapenv:Body>
</soapenv:Envelope>
Performance-mässig muss diese ziemlich massive Packetierung nicht schlimm sein. Bei doc/literal Type Webservices steigt der Anteil der Information an der Gesamtnachricht mit dem Umfang der gesendeten Information. In dem Beispiel wird ja einfach nur ein String zurückgeliefert. In der realen Welt werden aber mit Webservices komplexere Datenstrukturen ausgetauscht (größerer Umfang der gesendeten Information).
Ein wirkliches Problem besteht aber darin, dass die Information aus der komplexen Nachricht erst einmal geparsed werden muss. Und dies sollte aus meiner Sicht in einer eigenen Schicht geschehen. In der Java-Welt gibts hierfür fertige Frameworks wie JAXB, die man aber aus meiner Sicht in Domino nicht einsetzen kann. Man kann aber ein eigenes Framework schreiben, das in Folge geschehen soll.
* Der Grund für "in data-root" legen ist einfach, dass die endpointURLs in den Beispielen hartcodiert sind:
String endpoint = "http://localhost/URLFetcher.nsf/EchoTest?OpenWebService";
Die Datenbank URLFetcher.nsf hat in der ACL Default als Manager stehen.
Was passiert nun, wenn ich default auf "Kein Zugriff" setze und einen technischen User (hier: admin Axel) auf Managerzugriff?
Auf der Java Console erscheint das hier:
Returning NULL
There was an error: null
java.io.IOException
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:643)
at UrlFetcher.getUrlAsStream(UrlFetcher.java:389)
at UrlFetcher.getUrlAsString(UrlFetcher.java:450)
at JavaAgent.NotesMain(JavaAgent.java:28)
at lotus.domino.AgentBase.runNotes(Unknown Source)
at lotus.domino.NotesThread.run(Unknown Source)
Caused by: java.io.IOException: Server returned HTTP response code: 401 for URL: http://localhost/URLFetcher.nsf/EchoTest?OpenWebService
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:841)
at sun.net.www.protocol.http.HttpURLConnection.getHeaderFieldKey(HttpURLConnection.java:1546)
at UrlFetcher.getUrlAsStream(UrlFetcher.java:375)
... 4 more
HTTP HEADERS RECEIVED
HTTP/1.1 401 Unauthorized
Server: Lotus-Domino
Date: Sat, 09 Jun 2007 11:17:29 GMT
Connection: close
Expires: Tue, 01 Jan 1980 06:00:00 GMT
Content-Type: text/html; charset=US-ASCII
Content-Length: 146
WWW-Authenticate: Basic realm="/"
Cache-control: no-cache
Der obere Teil zeigt, dass es bzgl. Errorhandling eindeutig Raum für Verbesserungen gibt.
Im Stacktrace findet sich kein Hinweis auf das Problem -> User ist wg. ACL nicht autorisiert.
Die Klasse URLFetcher enthält eine Methode, um mit HTTP Basic Authentification umzugehen.
Nämlich:
/**
* For web sites that require basic authentication, encode the username
* and password and add the authentication header. Note that this
* method will have to be called again if you call clearHeaders().
* Also, if the website uses session-based authentication, you normally
* have to add the session header yourself.
*/
public void setBasicAuthentication (String username, String password) {
addHeaderValue("Authorization",
createAuthHeader(username, password));
}
Leider funktioniert das mit Julians code nicht.
Zumindest nicht, wenn ich Notes7 benutze.
Ich bekomme folgenden Fehler:
java.lang.IllegalArgumentException: Illegal character(s) in message header value: BasicQWRtaW4gQXhlbDprZW5ud29ydA==
Eine Erklärung findet sich hier:
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4615330
Das von Julian benutzte encodeBuffer hängt offenbar \n characters an und das kann nicht funktionieren.
Auch der Workaround encode durch encodeBuffer in URLFetcher.java zu ersetzen funktioniert nicht.
Ich werde in Folge eine eigene Lösung mit apache.jakarta.HTTPClient schreiben und das Problem melden.
HA. Nun hab ich doch einen Workaround gefunden
URLFetcherJava.createAuthHeader in der SkriptBibliothek muss wie folgt aussehen ->
/**
* Helper to base64-encode the authorization strings.
*/
private String createAuthHeader (String username, String password) {
String auth = "";
if (username == null) { username = ""; }
if (password == null) { password = ""; }
try {
sun.misc.BASE64Encoder encoder = new sun.misc.BASE64Encoder();
String str = username + ":" + password;
// patch start
auth = "Basic " + encoder.encode(str.getBytes());
// patch end
} catch (Exception e) {
}
return auth;
}
Im Code des Agenten "Echo Web Service Test (Java)" muss die letzte der folgenden Zeilen eingefügt werden, um das nach der ACL Änderung wieder ans laufen zu bringen. Die letzte Zeile ist neu.
public void NotesMain() {
try {
// MODIFY: make sure the endpoint is right, and make sure that
// the local http task is running if you do this on localhost
String endpoint = "http://localhost/URLFetcher.nsf/EchoTest?OpenWebService";
UrlFetcher fetcher = new UrlFetcher();
fetcher.setBasicAuthentication("Admin Axel", "kennwort") ;
Julians Klasse URLFetcher ist schlauer-weise also so aufgebaut, dass das Feature "Unterstützung von Basic Authentification" durch eine einfache set-Methode eingebunden werden kann.
Und genau das ist "DEPENDENCY INJECTION". Das Object UrlFetcher sucht sich die Information über den User, der für die Autorisierung genutzt nicht selbst. NEIN. Diese Information WIRD VON AUSSEN INJEZIERT. Dependency Injection als Design Pattern hat sich in verschiedenen Objekt Orientierten Sprachen auch über Frameworks wie Spring oder Guice in den letzten Jahren sehr stark verbreitet. Hier haben wir einen Fall von nicht-frameworkgestützter programmatischer Dependency Injection, aber es implementiert das Pattern.
Hier ist nun der einfachste Code mit jakarta.commons HTTP Client. Das macht einfach nur ein HTTP-GET gegen google. Wird noch deutlich weiter ausgearbeitet.
apache.Commons HTTPClient besitzt eine Menge von sinnvoller bis mission critical Zusatzfeatures:
- allgemein konfortabler
- Authentifizierung gegen Proxy
- SSL-Unterstützung
- Unterstützung der HTTP Methoden: GET, HEAD, POST, PUT und DELETE
- einfacher lesender und schreibender Zugriff auf HTTP Bodys
- unterstützt Cookies.
import lotus.domino.*;
import java.io.IOException;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
public class JavaAgent extends AgentBase {
public void NotesMain() {
try {
Session session = getSession();
AgentContext agentContext = session.getAgentContext();
} catch(Exception e) {
e.printStackTrace();
}
HttpClient client = new HttpClient();
String url = "http://www.google.com";
HttpMethod method = new GetMethod(url);
try {
client.executeMethod(method);
if (method.getStatusCode() == HttpStatus.SC_OK) {
String response = method.getResponseBodyAsString();
System.out.println("response=--------------------\n" + response);
}
} catch (HttpException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
method.releaseConnection();
}
}
}
Etwas komplizierter ist die Einbindung der von jakarta.commons.HTTPClient benötigten Bibliotheken. Man kann die einfach in das lib/ext Verzeichnis der JVM des Notes Clients/Notes Servers packen.
Oder in JavaUserClasses in der Notes-ini. Man kann die auch an den Agenten binden.
Zunächst müssen die benötigten jars sowieso runtergeladen werden. Ich habe festgestellt, dass die 3.01 Version Probleme mit Java 1.4 macht. Offenbar haben die von apache.jakarta beim Kompilieren nicht aufgepasst. ;D
Deshalb sollte man sich die binary Version der 3.1 rc1 Version herunterladen:
http://jakarta.apache.org/site/downloads/downloads_commons-httpclient.cgi
Die entsprechende jar befindet sich im Root des downloads.
oder EINFACHER aber OHNE DOKU bei maven: http://www.ibiblio.org/maven/commons-httpclient/jars/ (Datei commons-httpclient-3.1-rc1.jar)
Gleichzeitig werden die folgenden beiden jars benötigt:
- commons-codec-1.3.jar (gibts bei maven: http://www.ibiblio.org/maven/commons-codec/jars/)
- commons-logging-1.3.jar (gibts bei maven: http://www.ibiblio.org/maven/commons-logging/jars/)
Alle diese jars müssen jetzt in ein und dasselbe Verzeichnis kopiert werden.
Dann kann über die Schaltfläche Projekt bearbeiten in der Notes Designersicht des Agenten eine Dialogbox geöffnet werden, mit der diese jars hinzugefügt werden können (s. Screenshot).
Dies ist der Schlüssel, Domino als Webservice Client zu verwenden. Ok. Die WS-* Specs kriegt man so auch nicht unterstützt, aber die sind eh noch in Arbeit. Ansonsten kann man damit aber von SOAP-Zugriffen auf SAP bis rebellischen REST Webservices in Domino alles als Client verarbeiten. Ich werde das noch weiter ausführen.
Gruß Axel