Visualizzazione post con etichetta SOAP. Mostra tutti i post
Visualizzazione post con etichetta SOAP. Mostra tutti i post

martedì 14 novembre 2017

Inviare allegati a un web service con MTOM. (parte due... con IBMi)

Nella prima parte abbiamo visto come scrivere un servizio web che accetti file inviati con MTOM, un sistema standard per la trasmissione di allegati non codificati in Base64, ora vediamo come trasferire il tutto su IBMi.

Dividiamo i passaggi sempre tra producer e consumer, prima quindi vediamo come installare il servizio sul server delle applicazioni integrato sulla macchina, poi come creare un client scritto in RPG che interroghi il servizio.

IL PRODUCER.


Di cosa abbiamo bisogno:

  • Una macchina IBMi con IAS versione 8.1 (io ho usato la versione del sistema operativo V7R1).

Ricordate il war prodotto da Maven nel progetto Eclipse? Nella cartella target... MTOMService.war, proprio quello. Bene, prendiamolo e trasferiamolo sull'IFS del nostro sistema, in qualunque modo e in qualunque cartella vogliate.

Ad esempio io uso FileZilla e ho caricato il file su /tmp

Ora dovremmo collegarci al server amministrativo della nostra macchina, prima però verifichiamo che sia operativo, altrimenti avviamolo con:
STRTCPSVR SERVER(*HTTP) HTTPSVR(*ADMIN)

Quindi apriamo il browser e indirizziamolo verso:

http://<host_nostra_macchina_IBMi>:2001/HTTPAdmin

e accediamo con un utente amministrativo.

Ci viene mostrata la lista dei server presenti nel sistema, dobbiamo creare un server delle applicazioni (come Tomcat) che ospiti la nostra servlet, per cui clicchiamo su "Crea server delle applicazioni":

Questo link è molto più semplice, rispetto a quello del DCM... valli a capire...

Scegliamo di usare la versione 8.1 del server:


e andiamo avanti accettando i default che ci vengono proposti, se non avevamo creato altri server delle applicazioni dovremmo avere il server INTAPPSVR in ascolto sulla porta 10000 della macchina.

Fermiamolo. Così com'è ora non riuscirebbe a instanziare la nostra servlet CXF, questo perché CXF non è compatibile con l'implementazione JAX-WS presente sul server, vanno usate le librerie che forniamo noi, per cui:

Stop. Dopo lo riavviamo con il tasto play.

Spostiamoci su terminale, dobbiamo modificare una configurazione prima di farlo ripartire, modifichiamo questo file:
EDTF STMF('/www/INTAPPSVR/lwi/conf/overrides/i5javaopts.javaopt')
Inserendo questa riga:
-Dcom.ibm.websphere.webservices.DisableIBMJAXWSEngine=true
In questo modo istruiamo la JVM a non far partire il server con il supporto nativo JAX-WS.

Ora possiamo ritornare alla gestione del server e farlo ripartire (vedi sopra).

Clicchiamo sul link "Gestisci applicazioni installate", vediamo che per ora è presente una fantomatica quanto solinga "Guida di Eclipse", premiamo sul tasto Installa per aggiungere la nostra applicazione e chiediamo di importare il nostro war caricato prima:


Accettiamo tutti i default, infine vedremo la nostra applicazione fare compagnia alla "Guida di Eclipse" (?).

Facciamo click sul nome della nostra applicazione per interrogarla, si apre una nuova finestra del browser e... errore 404! Vi ricordate che va aggiusto services alla fine del link, vero?

La lista dei servizi messi a disposizione!

Possiamo copiare il link al nostro WSDL per creare un ambiente di test nel solito SoapUI, ma penso che sia arrivato il momento di fare da soli e creare un client in RPG.

IL CONSUMER.


Cosa ci occorre:
  • HTTPAPI di S. Klement (versione usata 1.37)
  • WSDL2RPG di T. Raddatz (la versione 1.16.5, che ha introdotto il supporto a MTOM, seppur sperimentale)

 

Preparativi.


Installiamo le due utility, magari con il supporto a HTTPS, e iniziamo con il creare un libreria e un file di sorgenti:
CRTLIB LIB(ZENEXMPL) TEXT('Example programs')

CRTSRCPF FILE(ZENUTILS/QWSDL) RCDLEN(112) TEXT('WSDL 2 RPG Stubs')
e mettiamo tutto in lista librerie:
ADDLIBLE LIB(LIBHTTP)

ADDLIBLE LIB(WSDL2RPG)

ADDLIBLE LIB(ZENEXMPL)
WSDL2RPG è un utility che crea le procedure necessarie ad assemblare la SOAP Envelope ed effettua la chiamata tramite HTTPAPI. I moduli creati vanno compilati ed assemblati in un service program che andrà collegato ai nostri programmi.
Per creare i sorgenti necessari usiamo il comando:
WSDL2RPG URL('http://localhost:10000/MTOMService/services/archiveServer?wsdl') SRCFILE(ZENEXMPL/QWSDL) SRCMBR(MTOM01) TYPE(*STUB) STRLEN(256) ATTACHMENT(*YES) PARMSTRUCT(*STMF) STRUCTSTMF('/tmp/MTOM01.log' *YES)
e selezioniamo con 1 tutti i metodi esposti dal servizio web: stiamo dicendo all'utility di creare il necessario per l'interrogazione di entrambi i metodi, con il supporto per l'invio di allegati.
Vengono creati i seguenti sorgenti nel file creato prima:
MTOM01      RPGLE       Web Service: ArchiveServerPort
MTOM01001   RPGLE       Web Service: archiveFile()
MTOM01002   RPGLE       Web Service: getFile()
Da questi sorgenti vanno creati i moduli, ma prima sono necessarie delle modifiche per attivare la ricezione MTOM, dato che - come dicevo prima - è sperimentale. Apriamo il sorgente MTOM01001:
STRSEU SRCFILE(ZENEXMPL/QWSDL) SRCMBR(MTOM01001)
(ma potete anche usare RDi.)

Dobbiamo cercare l'istruzione:
g_hMsgCtx = MessageContext_new();
e modificarla così:
g_hMsgCtx = MessageContext_new(cTrue);
(tutto qui.)

La stessa cosa va fatta anche per il sorgente MTOM01002.

Ora possiamo compilare:
CRTRPGMOD MODULE(ZENEXMPL/MTOM01) SRCFILE(ZENEXMPL/QWSDL) SRCMBR(MTOM01)

CRTRPGMOD MODULE(ZENEXMPL/MTOM01001) SRCFILE(ZENEXMPL/QWSDL) SRCMBR(MTOM01001)

CRTRPGMOD MODULE(ZENEXMPL/MTOM01002) SRCFILE(ZENEXMPL/QWSDL) SRCMBR(MTOM01002)
e creare il service program.
CRTSRVPGM SRVPGM(ZENEXMPL/MTOM01) MODULE(ZENEXMPL/MTOM01 ZENEXMPL/MTOM01001 ZENEXMPL/MTOM01002) EXPORT(*ALL) TEXT('MTOM Service Example') BNDSRVPGM((*LIBL/WSDL2RPGRT) (*LIBL/MIME) (*LIBL/HTTPMIME) (*LIBL/BASICS1)) BNDDIR(QC2LE)

 

Implementiamo.


L'utility ha creato le procedure necessarie per interrogare il nostro web service, e noi abbiamo compilato il programma di servizio per utilizzarle, in particolare la procedura:

 ArchiveServerPort_archiveFile 

effettua la chiamata al servizio archiveFile della nostra applicazione e se andiamo a indagare la sua definizione vediamo che richiede in ingresso:
  • una struttura dati i_tns_archiveFile di tipo tns_archiveFileRnmd_t : sono i parametri da passare al web service;
  • una struttura dati o_msg di tipo wsdl_errText_t : contiene eventuali messaggi di errore;

in uscita invece popola una struttura dati di tipo tns_archiveFileResponse_t che contiene la risposta del web service.

NB: le strutture dati tns_archiveFileRnmd_t e tns_archiveFileResponse_t sono modellate dall'utility secondo quanto è stato dichiarato nel WSDL, rispecchiano quindi la stessa struttura dello schema XML!

NB2: l'utility rinomina le procedure e le strutture dati secondo i nomi del nostro web service e i suoi metodi, normalmente quindi le procedure descritte sopra cambiano sempre nome per ogni web service di cui facciamo lo stub, ad esempio il template per i nomi visti prima sono:
  • procedura principale: <Nome_Web_Service>_<Nome_Metodo> ;
  • struttura dati ingresso: tns_<Nome_Metodo>Rnmd_t ;
  • struttura dati uscita: tns_<Nome_Metodo>Response_t .

Se non abbiamo idea di come utilizzare questa procedura e non vogliamo partire da zero, niente paura! WSDL2RPG ci permette di costruire in automatico un programma di esempio con il comando:
WSDL2RPG URL('http://localhost:10000/MTOMService/services/archiveServer?wsdl') SRCFILE(ZENEXMPL/QWSDL) SRCMBR(MTOM01001T) TYPE(*PGM) STUB(MTOM01001)
e selezionando il metodo archiveFile e ancora:
WSDL2RPG URL('http://localhost:10000/MTOMService/services/archiveServer?wsdl') SRCFILE(ZENEXMPL/QWSDL) SRCMBR(MTOM01002T) TYPE(*PGM) STUB(MTOM01002)
e selezionando il metodo getFile.

Ora abbiamo due sorgenti di due programmi che testano le due procedure, dobbiamo solo modificare qualche riga per poter fargli fare quello che vogliamo. Iniziamo dal programma per fare l'upload: MTOM01001T.
STRSEU SRCFILE(ZENEXMPL/QWSDL) SRCMBR(MTOM01001T)
(come prima, potete usare RDi)

Prima di tutto inseriamo dei parametri di ingresso, in modo che il programma possa essere utilizzato in maniera variabile (al momento i parametri sono fissi) tramite un comando, cerchiamo:
D MTOM01001T...
D                 PR
e
D MTOM01001T...
D                 PI
e sotto inseriamo:
D description                         like(
D                                     tns_archiveRequest_t.description)
D fileLoad                            like(
D                                     tns_archiveRequest_t.fileLoad)
D fileName                            like(
D                                     tns_archiveRequest_t.fileName)
D fileType                            like(
D                                     tns_archiveRequest_t.fileType)
D key                                 like(
D                                     tns_archiveRequest_t.key)
Quindi spostiamo i parametri in ingresso nella struttura dati che contiene i parametri della Request, che diventa:
parameters.request.fileName = fileName;
parameters.request.fileType = 'application/pdf';
parameters.request.key = key;
parameters.request.description = description;
alla fine aggiungiamo il nostro allegato:
parameters.request.fileLoad = 'cid:'+
     ArchiveServerPort_archiveFile_Attachments_addFile(
        filePath: parameters.request.fileType);
NB: la procedura ArchiveServerPort_archiveFile_Attachments_addFile serve per aggiungere un allegato, richiede il percorso del file sull'IFS e il suo MIME-Type e ritorna l'identificativo della sezione http dove verrà inserito, come al solito il nome può cambiare e il template è:
<Nome_Web_Service>_<Nome_Metodo>_Attachments_addFile.

Ora tocca al programma per fare il download: MTOM01002T.
STRSEU SRCFILE(ZENEXMPL/QWSDL) SRCMBR(MTOM01002T)
In maniera simile a prima aggiungiamo i parametri, che è uno solo: la chiave del documento registrata in upload. Per cui cerchiamo:
D MTOM01002T...
D                 PR
e
D MTOM01002T...
D                 PI
e sotto inseriamo:
D key                                 like(
D                                     tns_getFile_t.key)
E poi facciamo sempre in modo che il nostro input finisca nei parametri della Request:
parameters.key = key;

Ora possiamo compilare:
CRTRPGMOD MODULE(ZENEXMPL/MTOM01001T) SRCFILE(*LIBL/QWSDL) SRCMBR(MTOM01001T) DBGVIEW(*LIST) TRUNCNBR(*NO)

CRTPGM PGM(ZENEXMPL/MTOM01001T) MODULE(ZENEXMPL/MTOM01001T) TEXT('MTOM Service Example - Test Upload') BNDSRVPGM((ZENEXMPL/MTOM01)) BNDDIR(QC2LE) ACTGRP(*NEW)

CRTRPGMOD MODULE(ZENEXMPL/MTOM01002T) SRCFILE(*LIBL/QWSDL) SRCMBR(MTOM01002T) DBGVIEW(*LIST) TRUNCNBR(*NO)

CRTPGM PGM(ZENEXMPL/MTOM01002T) MODULE(ZENEXMPL/MTOM01002T) TEXT('MTOM Service Example - Test Download') BNDSRVPGM((ZENEXMPL/MTOM01)) BNDDIR(QC2LE) ACTGRP(*NEW)

Abbiamo i nostri due programmi di test, ma potrebbe essere utile fare due comandi per poterli utilizzare, iniziamo creando il comando per inviare:
STRSEU SRCFILE(ZENEXMPL/QWSDL) SRCMBR(SNDMTOM) TYPE(CMD) TEXT('Web Service: archiveFile() - Upload Comand')
e modifichiamo il sorgente SNDMTOM così:
CMD        PROMPT('MTOM Service Upload')

PARM       KWD(DESC)              +
           MIN(1)                 +
           TYPE(*CHAR)            +
           VARY(*YES *INT2)       +
           LEN(128)               +
           CASE(*MIXED)           +
           PROMPT('File description' 1)

PARM       KWD(PATH)              +
           MIN(1)                 +
           TYPE(*CHAR)            +
           VARY(*YES *INT2)       +
           LEN(128)               +
           CASE(*MIXED)           +
           PROMPT('File path' 2)

PARM       KWD(NAME)              +
           MIN(1)                 +
           TYPE(*CHAR)            +
           VARY(*YES *INT2)       +  
           LEN(128)               +
           CASE(*MIXED)           +
           PROMPT('File name' 3)

PARM       KWD(TYPE)              +
           MIN(1)                 +
           TYPE(*CHAR)            +
           VARY(*YES *INT2)       +
           LEN(128)               +
           CASE(*MIXED)           +
           PROMPT('File type' 4)

PARM       KWD(KEY)              +
           MIN(1)                 +
           TYPE(*CHAR)            +
           VARY(*YES *INT2)       +
           LEN(20)                +
           CASE(*MIXED)           +
           PROMPT('Key' 5)
Stessa cosa per il comando per ricevere: RCVMTOM.
STRSEU SRCFILE(ZENEXMPL/QWSDL) SRCMBR(RCVMTOM) TYPE(CMD) TEXT('Web Service: archiveFile() - Download Comand')
e quindi:
CMD        PROMPT('MTOM Service Download')

PARM       KWD(KEY)              +
           MIN(1)                 +
           TYPE(*CHAR)            +
           VARY(*YES *INT2)       +
           LEN(20)                +
           CASE(*MIXED)           +
           PROMPT('Key' 1)
Infine non ci resta che creare anche questi oggetti:
CRTCMD CMD(ZENEXMPL/SNDMTOM) PGM(ZENEXMPL/MTOM01001T) SRCFILE(ZENEXMPL/QWSDL) SRCMBR(SNDMTOM)

CRTCMD CMD(ZENEXMPL/RCVMTOM) PGM(ZENEXMPL/MTOM01002T) SRCFILE(ZENEXMPL/QWSDL) SRCMBR(RCVMTOM)

Ora possiamo testare i nostri programmi! Assicuriamoci di avere un file PDF da utilizzare per i nostri scopi salvato su IFS, per comodità io mi riferirò a un ipotetico file testfile.pdf presente in /tmp, iniziamo inviando il file:
ZENEXMPL/SNDMTOM DESC('Test file.') PATH('/tmp/testfile.pdf') NAME(renamed.pdf) TYPE('application/pdf') KEY(TEST1)
(lo salviamo sotto altro nome, giusto per provare.)

Il messaggio *** Success *** in fondo al terminale ci indica che la trasmissione è avvenuta, ma dove è stato salvato il file? Ricordate che l'applicazione stampa in output il path di archiviazione? Però questo output non è interattivo, è salvato in un file apposito nel percorso dell'istanza del server:
DSPF STMF('/www/INTAPPSVR/lwi/logs/lwistdout.txt')
dall'output notiamo che i file temporanei sono gestiti per singola istanza, per cui troviamo il file salvato qui:
DSPF STMF('/www/INTAPPSVR/lwi/temp/uploaded/renamed.pdf')
Ora quindi proviamo a recuperare il file appena archiviato:
ZENEXMPL/RCVMTOM KEY(TEST1)
Ancora, il messaggio *** Success *** ci indica che tutto è ok, questa volta però spetta alla nostra procedura salvare il file, che di default salva tutti gli allegati in /tmp/attachments:
WRKLNK OBJ('/tmp/attachments')

Considerazioni.


La funzione MTOM dell'utility è un'aggiunta interessante ed è stato divertente approcciarsi a questa metodologia e farla funzionare su IBMi, ricordo però che è ancora in fase sperimentale, e non è quindi esente da mancanze e bug che ne potrebbero limitare l'usabilità, nel caso vi invito a contattare l'autore per segnalare eventuali problemi che potrebbero essere corretti in una futura release!

Di seguito alcuni link utili:
  • il sito di Raddatz ha una serie di manuali che spiegano più ampiamente l'utility e le sue procedure;
  • la mailing list di Klement: è per avere aiuto per HTTPAPI, però viene data risposta anche a domande su WSDL2RPG;
  • Il repository dove ho rilasciato i sorgenti di questo esempio: i sorgenti vanno importati poi a mano nella libreria ZENEXMPL, oppure ho preparato un SAVF che può essere ripristinato sulla macchina (salvato come V7R1M0);
  • Configurazioni particolari per fa funzionare CXF su WebSphere;
  • Manuale dello IAS.

mercoledì 25 ottobre 2017

Inviare allegati a un web service con MTOM. (parte uno... per ora senza IBMi)

Se pensate a come allegare un grosso file in una SOAP envelope immaginerete di creare un campo BLOB codificato in base64. La codifica è utile per rispettare le specifiche (senza infilare nell'XML caratteri speciali), ma ha la brutta abitudine di diventare enorme rispetto al contenuto effettivo del file. MTOM è un meccanismo semplice e soprattutto standard per migliorare questa trasmissione e sfrutta un altro standard, XOP: in pratica, il file viene serializzato come parte della chiamata http, come se fosse un normale form html.

AGGIORNAMENTO 14/11/2017: e stata pubblicata la seconda parte qui.

(Vi apettavate una barra di sapone? Spiacente, uso quello liquido)

Di seguito vediamo come implementare un semplice sistema di archiviazione che sfrutta questa serializzazione. Il producer riceverà i file e li salverà associandoli a una determinata chiave, e sarà in grado di inviarli alla richiesta della stessa chiave, il consumer invierà e richiederà i file al servizio.

In questa prima parte il producer verrà fatto girare in locale su Tomcat, mentre il ruolo del consumer viene giocato da SoapUI che costruirà da solo il necessario per fare la chiamata e testare il tutto, nella seconda parte vedremo come implementare un consumer con RPG e di far girare tutto, compreso il producer, su IBMi.

IL PRODUCER.


Prepariamo il necessario.


Di cosa abbiamo bisogno:

  • Java JDK 6 (bisogna registrarsi);
  • Eclipse Oxygen for JEE Developer (ho utilizzato la versione 4.7.1a);
  • Maven (ho utilizzato la versione 3.2.5);
  • Tomcat (ho utilizzato la versione 8.5), per provare il progetto in locale;

NB: le versioni delle librerie utilizzate sono compatibili con Java 6, questo per poter poi installare il tutto sulla versione 8.1 del server delle applicazioni integrato dell' IBMi, come vedremo in seguito.

Installate tutto quanto, in particolare Maven e Tomcat andranno estratti ognuno in una sua cartella alle quali, per comodità, mi riferirò come $MAVEN_HOME e $TOMCAT_HOME, mentre la cartella di installazione di Java sarà $JAVA_HOME.
Per prima cosa impostiamo Eclipse:

  • per Java: dal menu Windows > Java > Installed JREs > Add selezioniamo "Standard VM" e andiamo avanti, poi "Directory..." e andiamo a cercare $JAVA_HOME, alla fine impostiamo come JRE di default;
  • per Maven: dal menu Windows > Preferences > Maven > Installations > Add selezioniamo "External" e poi "Directory..." e andiamo a cercare $MAVEN_HOME;
  • per Tomcat: dal menu Windows > Preferences > Server > Runtime Environments > Add  selezioniamo "Apache Tomcat 8.5" e andiamo avanti, quindi "Browse..." e andiamo a cercare $TOMCAT_HOME, viene proposto di lavorare con la JRE di default definita prima.

Ok, procediamo. Creiamo un nuovo progetto Maven, dal menu File > New > Maven Project, chiediamo di non utilizzare un archetipo e andiamo avanti in questo modo:


Diamo almeno un nome gruppo e un nome artefatto al nostro progetto, il packaging da utilizzare invece è war:


e finalizziamo.

Come prima cosa Eclipse ci avverte che nel nuovo progetto manca il descrittore web.xml, per cui creiamone uno cliccando con il tasto destro sul progetto e quindi Java EE Tools > Generate Deployment Descriptor Stub.

Come seconda cosa dobbiamo inserire un po' di dipendenze e plugin nel progetto, fortuna che Maven si occupa di reperire tutte le librerie necessarie! A patto ovviamente di farglielo sapere indicandolo nel suo descrittore, apriamo quindi il file pom.xml e inseriamo questo dentro al tag <project>:
<build>
 <pluginManagement>
  <plugins>
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.6.1</version>
    <configuration>
     <source>1.6</source>
     <target>1.6</target>
    </configuration>
   </plugin>
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-war-plugin</artifactId>
    <version>3.0.0</version>
    <configuration>
     <warSourceDirectory>src/main/webapp</warSourceDirectory>
     <webXml>src/main/webapp/WEB-INF/web.xml</webXml>
     <warName>MTOMService</warName>
    </configuration>
   </plugin>
  </plugins>
 </pluginManagement>
</build>
In questo modo aggiungiamo qualche plugin necessario a compilare il progetto, istruito per creare un file di nome MTOMService.war .

Sempre nello stesso tag <project> aggiungiamo anche qualche dipendenza:
<dependencies>
 <dependency>
  <groupId>org.apache.cxf</groupId>
  <artifactId>cxf-rt-frontend-jaxws</artifactId>
  <version>2.5.2</version>
 </dependency>
 <dependency>
  <groupId>org.apache.cxf</groupId>
  <artifactId>cxf-rt-transports-http</artifactId>
  <version>2.5.2</version>
 </dependency>
 <dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-core</artifactId>
  <version>3.0.6.RELEASE</version>
 </dependency>
 <dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-web</artifactId>
  <version>3.0.6.RELEASE</version>
 </dependency>
 <dependency>
  <groupId>org.apache.servicemix.bundles</groupId>
  <artifactId>org.apache.servicemix.bundles.saaj-impl</artifactId>
  <version>1.3.18_1</version>
 </dependency>
</dependencies>
Useremo Spring Framework per instanziare una servlet CXF, inoltre introduciamo una dipendenza a SAAJ implementato da Apache, in quanto l'implementazione di IBM integrata con il server delle applicazioni non è completamente compatibile con CXF.

NB: le versioni utilizzate sopra sono sempre quelle compatibili con Java 6, il motivo è sempre lo stesso.

Ultime configurazioni e poi implementiamo, giuro, per ora apriamo il Deployment Descriptor:


e sostituiamolo completamente in questo modo:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
  <display-name>MTOMService</display-name>
  <servlet>
    <servlet-name>cxfservlet</servlet-name>
    <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>cxfservlet</servlet-name>
    <url-pattern>/services/*</url-pattern>
  </servlet-mapping>
</web-app>

Implementiamo il sistema.


Mettiamo le mani in pasta, finalmente, essendo questo un esempio molto semplice, abbiamo bisogno di scrivere solo tre classi e per comodità mettiamole quindi sotto un unico package che io ho chiamato it.zenovalle.examples.

Per prima creiamo una semplice classe di proprietà (POJO) che descrive il documento che verrà scambiato, oltre al contenuto del documento stesso.

ArchiveRequest.java
package it.zenovalle.examples;

import javax.activation.DataHandler;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlMimeType;
import javax.xml.bind.annotation.XmlType;

@XmlType
@XmlAccessorType(XmlAccessType.FIELD)
public class ArchiveRequest {
 
 @XmlMimeType("application/octet-stream")
 protected DataHandler fileLoad;
 protected String fileName;
 protected String fileType;
 protected String key;
 protected String description;
 
 public DataHandler getFileLoad() {
  return fileLoad;
 }
 
 public void setFileLoad(DataHandler fileLoad) {
  this.fileLoad = fileLoad;
 }
 
 public String getFileName() {
  return fileName;
 }
 
 public void setFileName(String fileName) {
  this.fileName = fileName;
 }
 
 public String getFileType() {
  return fileType;
 }
 
 public void setFileType(String fileType) {
  this.fileType = fileType;
 }
 
 public String getKey() {
  return key;
 }
 
 public void setKey(String key) {
  this.key = key;
 }
 
 public String getDescription() {
  return description;
 }
 
 public void setDescription(String description) {
  this.description = description;
 }
 
}
Vi faccio solo notare che:
  • l'intera classe deve essere annotata come XmlType e con tipo di accesso FIELD;
  • la proprietà che contiene i dati del file è di tipo DataHandler invece di byte[], questo ci permette di trattare meglio i dati, mantenendo il meccanismo di conversione automatico;
  • la conversione automatica viene effettuata marchiando la proprietà con un'annotazione del tipo @XmlMimeType("application/octet-stream") che spiega in che formato attendersi i dati (binario, per l'appunto).
Ora scriviamo l'interfaccia con cui si presenta il Web Service.

ArchiveServer.java
package it.zenovalle.examples;

import javax.activation.DataHandler;
import javax.jws.WebParam;
import javax.jws.WebService;

@WebService
public interface ArchiveServer {

 public String archiveFile(@WebParam(name="request") ArchiveRequest request);
 
 public DataHandler getFile(@WebParam(name="key") String key);

}
Anche in questo caso utilizziamo le annotazioni per far capire al sistema che l'interfaccia è un @WebService e anche per rinominare i parametri dei vari metodi con @WebParam(name="***").

Infine implementiamo l'interfaccia con una classe vera e propria.

ArchiveServerImpl.java
package it.zenovalle.examples;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.jws.WebService;
import javax.xml.bind.annotation.XmlMimeType;
import javax.xml.ws.WebServiceException;

@WebService(endpointInterface = "it.zenovalle.examples.ArchiveServer",
            serviceName = "ArchiveServer")
public class ArchiveServerImpl implements ArchiveServer{

 Map<String, File> files = new LinkedHashMap<String, File>();
 private File tmpdir = new File(System.getProperty("java.io.tmpdir"),"uploaded");

 @Override
 public String archiveFile(ArchiveRequest request) {

  tmpdir.mkdir();

  if(request==null){
   throw new WebServiceException("Upload Failed");
  }

  File file = new File(tmpdir, request.getFileName());

  try {
   InputStream is = request.getFileLoad().getInputStream();
   OutputStream os = new FileOutputStream(file);

   file.createNewFile();

   byte[] b = new byte[100000];
   int bytesRead = 0;
   while ((bytesRead = is.read(b)) != -1) {
    os.write(b, 0, bytesRead);
   }
   
   is.close();
   os.flush();
   os.close();

   files.put(request.getKey(), file);
   
   System.out.println("File "+ request.getFileType() +" archived in " + file.getPath() + " - " +request.getKey() +" "+request.getDescription());

  } catch (IOException e) {
   e.printStackTrace();
   return "Upload Failed";
  }

  return "Upload Complete";
 }

 @Override
 public
 @XmlMimeType("application/octet-stream") DataHandler getFile(String key) {

  File file = files.get(key);
  DataSource dataSource = new FileDataSource(file);
  System.out.println("Returning " + key + " : "+ file.getPath());
  return new DataHandler(dataSource);
 }
}
Ancora l'annotazione @WebService, questa volta però con alcune informazioni in più su quale interfaccia viene implementata (che viene utilizzata come endpoint, definisce quindi il contratto WSDL) e su come si chiama il servizio. Viene anche annotato il tipo di ritorno DataHandler del metodo getFile in maniera che possa essere serializzato anch'esso con MTOM.

L'implementazione riceve il DataHandler e ne ricava l'InputStream da cui leggere i dati che sono poi scritti in un File creato nella sotto-cartella uploaded della cartella temporanea di sistema. La chiave viene associata alla posizione tramite una Map che risiede in memoria, se viene richiesto il download il web service interroga la Map per sapere che File aprire.

Ok, manca solo di chiedere a Spring di farci il favore di instanziare la servlet, creiamo il file cxf-servlet.xml sotto src > main > webapp > WEB-INF descritto in questo modo:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jaxws="http://cxf.apache.org/jaxws"
 xmlns:cxf="http://cxf.apache.org/core" xmlns:soap="http://cxf.apache.org/bindings/soap"
 xsi:schemaLocation="http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://cxf.apache.org/bindings/soap http://cxf.apache.org/schemas/configuration/soap.xsd http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">

 <jaxws:server id="archiveServer"
  serviceClass="it.zenovalle.examples.ArchiveServer" address="/archiveServer">
  <jaxws:serviceBean>
   <bean class="it.zenovalle.examples.ArchiveServerImpl" />
  </jaxws:serviceBean>
  <jaxws:properties>
   <entry key="mtom-enabled" value="true" />
   <entry key="attachment-directory" value="/tmp/"/>
   <entry key="attachment-memory-threshold" value="4000000"/>
  </jaxws:properties>
 </jaxws:server>

</beans>
La proprietà più importante è mtom-enabled che deve essere messa a true per far funzionare tutto il meccanismo, il resto delle proprietà è per indicare di trattare in memoria gli allegati fino a 4 MB, altrimenti li salva prima nella cartella temporanea.

Ora abbiamo veramente tutto, quindi...

Compiliamo e avviamo


Con il tasto destro del mouse sul nostro progetto cerchiamo Run As... e poi Maven install, se tutto ok Maven assemblerà il file MTOMService.war nella cartella target.


Bene, ora facciamo partire tutto in locale, sempre tasto destro sul progetto, sempre Run As... , questa volta scegliamo Run on Server e scegliamo il runtime Tomcat configurato prima. Comparirà il browser interno a Eclipse che cercherà di interrogare la servlet, inizialmente con scarso successo:


E sufficiente aggiungere il suffisso services all'URL per ottenere un risultato migliore:

La lista dei servizi offerti dalla servlet.

In particolare il link che la pagina ci invita a cliccare (cliccate!) non è nient'altro che il WSDL messo a disposizione dal Web Service: copiate questo link negli appunti, ci sarà utile a breve.

IL CONSUMER.


Di cosa abbiamo bisogno:
  • SoapUI (ho usato la versione 5.3.0).

SoapUI è un benchmark molto ben fatto ed OpenSource che ci permette di testare i nostri Web Service, può essere usato sia per SOAP che per REST, e permette anche di inviare allegati con MTOM (con qualche accorgimento, come vedremo). Può ricoprire benissimo il ruolo del nostro consumer in modo da avere finalmente la soddisfazione di vedere funzionare il tutto.

Una volta installato apriamolo e chiediamo subito di creare un nuovo progetto SOAP, con il tastone posizionato in alto, diamo un nome al progetto e incolliamo il link al WSDL copiato prima:


E diamo l'ok, SoapUI penserà ad analizzare il WSDL e creerà l'ambiente necessario per testare i vari metodi messi a disposizione del web service.

Espandiamo il nodo corrispondente al metodo archiveFile, il programma ha già creato una Request di default che possiamo rimaneggiare per fare un test. Normalmente la schermata è separata in due, in una parte possiamo modificare la Request, nell'altra vedremo la risposta data dal web service, per prima cosa abilitiamo MTOM nelle proprietà della Request:

Enable MTOM = true

Poi inseriamo dei valori di prova nei vari tag della SOAP Envelope, tranne per ora fileLoad. Per inserire un allegato facciamo click sul tab Attachments sotto la Request e poi sul tasto + :

(Nell'esempio ipotizzo che venga scelto un file pdf)

NB: il programma ci chiede di inserire in cache il documento, vi direi di rispondere di sì, in questo modo viene inglobato nel progetto SoapUI che poi potrete salvare.

Ora modifichiamo il tag fileLoad inserendo il nome del file scelto, ma preservando il prefisso cid: (nel mio esempio: cid:testfile.pdf ), torniamo in Attachments, scegliamo Part e vediamo che il nostro cid viene elencato tra quelli assegnabili all'allegato:

Il Type viene modificato automaticamente in XOP, proprio quello che ci serve per MTOM.

Ora è tutto pronto, clicchiamo sulla freccettina verde sopra alla Request per far partire la richiesta e, se ok, nella Response dovrebbe comparire "Upload Complete":

Sento scatenarsi le endorfine...

Spostiamoci sulla console di Eclipse, sul quale attendeva silenzioso Tomcat, e dovremmo trovare un output generato dal nostro servizio:

File application/pdf archived in C:\Users\<user>\AppData\Local\Temp\uploaded\testfile.pdf - 1234567890 This is a test file

Che sono proprio i dati che sono stati passati! Come controprova, interroghiamo la posizione e cerchiamo il file, ed eccolo qui:


Rimane solo da verificare il download! Come prima espandiamo getFile, abilitiamo MTOM e modifichiamo il tag key con la stessa chiave inserita prima (nell'esempio: 1234567890) e avviamo:


Troviamo sempre l'allegato in Attachments, ma questa volta sotto la Response, con doppio click possiamo esaminarlo: è proprio lui e come maggior conferma che sia quello caricato prima, vediamo la console di Eclipse:

Returning 1234567890 : C:\Users\<user>\AppData\Local\Temp\uploaded\testfile.pdf

Questo è tutto, per semplicità ho inserito un file di salvataggio del progetto SoapUI (MTOMService-soapui-project.xml) nel repository apposito che contiene tutti i sorgenti mostrati nel post, nel caso potete importare tutto come progetto Eclipse usando EGit.

Nella prossima parte vedremo come prendere tutto e portarlo su IBMi.

PS: dopo tutta questa spiegazione, non vi è rimasta la curiosità di capire com'è fatta in fin dei conti una trasmissione MTOM? Potete vederlo facilmente da SoapUI, nella Request di archiveFile, dopo aver fatto partire la richiesta e cliccando sul tag Raw poco sotto:

No BLOB, No Base64, baby.


Aggiungo qualche link per approfondire l'argomento:

Aggiornamento 25/10/2017 h 22:35 : Aggiunta bibliografia.