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.

Nessun commento:

Posta un commento