Añadir logging a un cliente SOAP de web services

Cuando trabajamos con web services en java, a la hora de depurar nos sería muy útil el poder ver el xml (protocolo SOAP) que se envían cliente y servidor.
Sin embargo no existe una forma 'sencilla' de hacerlo.

La idea consiste en hacer una clase que implemente el interface javax.xml.rpc.handler.Handler, capaz de interceptar los mensajes SOAP antes de su envío del cliente al servidor y antes de la recepción por parte del cliente de los mensajes enviados por el servidor.
Pueden añadirse varios handlers en serie para procesar las llamadas (handler chains). Este comportamiento es idéntico al de un ServletFilter en una cadena de filtros.
Desde esta clase tenemos acceso al contenido del mensaje SOAP, por lo que somos capaces de leerlo y escribirlo en un fichero de texto o similar.

En el caso de un cliente hecho con apache axis, podemos obtener la lista de Handlers asociados a un nombre de puerto (javax.xml.namespace.QName) y a un punto de destino a través de la clase org.apache.axis.client.Service. Una vez obtenida la lista, solo nos resta añadir nuestro Handler.

Vamos a ello. En primer lugar implementamos el interfaz javax.xml.rpc.handler.Handler para interceptar la llamada soap:

  1. package com.madeinxpain.seamcity.ws;
  2.  
  3. import java.io.File;
  4. import java.io.FileNotFoundException;
  5. import java.io.FileOutputStream;
  6. import java.io.OutputStream;
  7. import java.util.ArrayList;
  8. import java.util.List;
  9.  
  10. import javax.xml.namespace.QName;
  11. import javax.xml.rpc.handler.Handler;
  12. import javax.xml.rpc.handler.HandlerInfo;
  13. import javax.xml.rpc.handler.MessageContext;
  14. import javax.xml.rpc.handler.soap.SOAPMessageContext;
  15.  
  16. public class SoapLoggingHandler implements Handler{
  17.  
  18. private HandlerInfo handlerInfo;
  19. private List streams;
  20.  
  21. public SoapLoggingHandler() {
  22. streams = new ArrayList();
  23. try {
  24. this.addOutputStreamLog(System.out);
  25. this.addFileLog("soap.xml");
  26. } catch (Exception e) {
  27. e.printStackTrace();
  28. }
  29. }
  30.  
  31. public void addOutputStreamLog(OutputStream os) {
  32. this.streams.add(os);
  33. }
  34.  
  35. public void addFileLog(String rute) throws FileNotFoundException {
  36. FileOutputStream fos = new FileOutputStream(new File(rute));
  37. this.addOutputStreamLog(fos);
  38. }
  39.  
  40. public boolean handleRequest(MessageContext arg0) {
  41. SOAPMessageContext messageContext = (SOAPMessageContext) arg0;
  42. try {
  43. for (OutputStream os : streams) {
  44. os.write(new String("\n\n<!-- REQUEST -->\n").getBytes());
  45. messageContext.getMessage().writeTo(os);
  46. }
  47. } catch (Exception e) {
  48. e.printStackTrace();
  49. }
  50. return true;
  51. }
  52.  
  53. public boolean handleResponse(MessageContext arg0) {
  54. SOAPMessageContext messageContext = (SOAPMessageContext) arg0;
  55. try {
  56. for (OutputStream os : streams) {
  57. os.write(new String("\n\n<!-- RESPONSE -->\n").getBytes());
  58. messageContext.getMessage().writeTo(os);
  59. }
  60. } catch (Exception e) {
  61. }
  62. return true;
  63. }
  64.  
  65. public boolean handleFault(MessageContext arg0) {
  66. SOAPMessageContext messageContext = (SOAPMessageContext) arg0;
  67. try {
  68. for (OutputStream os : streams) {
  69. os.write(new String("\n\n<!-- FAULT -->\n").getBytes());
  70. messageContext.getMessage().writeTo(os);
  71. }
  72. } catch (Exception e) {
  73. }
  74. return true;
  75. }
  76.  
  77. public void init(HandlerInfo arg0) {
  78. this.handlerInfo = arg0;
  79. }
  80.  
  81. public void destroy() {
  82. }
  83.  
  84. public QName[] getHeaders() {
  85. return handlerInfo.getHeaders();
  86. }
  87. }

Esta clase implementa una serie de métodos extra aparte de los obligatorios definidos por el interface. Estos métodos permiten definir una lista de Streams por los que escribir el mensaje.
Estos métodos de logging deben ser invocado desde el constructor, ya que al añadir el Handler a la cadena (como veremos más adelante) no se añade una instancia de un objeto, si no una clase, y es el framework axis el encargado de crear la instancia mediante el método Class.new Instance(), por lo que no es posible definir las formas de logging si no es en el constructor por defecto del handler.

Un a vez codificada la clase, vamos a proceder a añadirla a la cadena. Cada handler debe estar asociado
Tomaremos como ejemplo un cliente WebService para Microsoft Exchange 2007, creado en un post anterior
En primer lugar buscamos la clase que extiende de org.apache.axis.client.Stub, en nuestro caso llamada ExchangeServiceBindingStub, y le añadimos dos campos para almacenar el port por el que se va a realizar la petición soap y el punto de destino.
Estos campos se van a utilizar en el metodo createCall() a la hora de crear la petición soap.
Por simplicidad crearemos los campos como públicos.

  1. public class ExchangeServiceBindingStub extends org.apache.axis.client.Stub implements com.microsoft.schemas.exchange.services._2006.messages.ExchangeServicePortType {
  2.  
  3. public javax.xml.namespace.QName portName;
  4. public String endPoint;
  5.  
  6. // resto del código original de la clase
  7.  
  8. // metodo a modificar
  9. protected org.apache.axis.client.Call createCall() throws java.rmi.RemoteException {
  10. // codigo anterior
  11. if (super.cachedPortName != null) {
  12. _call.setPortName(super.cachedPortName);
  13. }
  14.  
  15. // Tenemos que añadir estas dos lineas
  16. _call.setPortName(this.portName);
  17. _call.setTargetEndpointAddress(this.endPoint);
  18.  
  19. // codigo posterior
  20. java.util.Enumeration keys = super.cachedProperties.keys();
  21.  
  22. }

Cuando instanciamos el objeto org.apache.axis.client.Stub le añadimos el handler:

  1. public class ExchangeWebServiceClient {
  2. private ExchangeServiceBindingStub esb;
  3. private ExchangeServicesLocator locator;
  4.  
  5. public ExchangeWebServiceClient() {
  6. this.locator = new ExchangeServicesLocator();
  7. this.esb = (ExchangeServiceBindingStub) locator.getExchangeServicePort(new URL(locator.getExchangeServicePortAddress()));
  8.  
  9. // Añadimos el handler
  10. QName portName = new QName("http://www.neodoo.es/", locator.getExchangeServicePortWSDDServiceName());
  11. List list = locator.getHandlerRegistry().getHandlerChain(portName);
  12. HandlerInfo handlerInfo = new HandlerInfo();
  13. handlerInfo.setHandlerClass(com.madeinxpain.seamcity.ws.SoapLoggingHandler.class);
  14. list.add(handlerInfo);
  15. this.esb.portName = portName;
  16. this.esb.endPoint = locator.getExchangeServicePortAddress();
  17. }
  18. }

Y con esto y un bizcocho, ya tenemos una forma de depurar las llamadas soap a un webservice desde un cliente hecho con axis.
La optimización del código queda a cargo del desarrollador final ;)

Leave a Comment

Crear un cliente java de Web Services para Exchange 2007

Para crear el cliente he usado el IDE eclipse-Red Hat Developer Studio, pero el procedimiento será similar para cualquier IDE.
Supondremos que la dirección del servidor es "my.exchange.com".

El primer paso es bajarse del servidor exchange estos 3 archivos y guardarlos en local:
* https://my.exchange.com/ews/Services.wsdl
* https://my.exchange.com/ews/messages.xsd
* https://my.exchange.com/ews/types.xsd
Después hay que modificar el archivo "Services.wsdl" para añadir el servicio y su url.

Debes añadir al final del archivo (antes de cerrar la etiqueta <wsdl:definitions>) las siguientes lineas:

  1. <wsdl :service name="ExchangeServices">
  2.      </wsdl><wsdl :port name="ExchangeServicePort" binding="tns:ExchangeServiceBinding">
  3.        <soap :address location="https://my.exchange.com/EWS/Exchange.asmx"/>
  4.      </wsdl>

Ahora tendrás que modificar los archivos "types.xsd" y "messages.xsd" para que maneje correctamente los arrays, mapeandolos a colecciones.
Para cada tipo llamado "ArrayOf..." o "NonEmpyArrayOf..." tendrás que añadirle el atributo maxOccurs="unbounded"
Ejemplo:

  1. <xs :complexType name="ArrayOfFoldersType">
  2.     </xs><xs :choice minOccurs="0" maxOccurs="unbounded">
  3.       <xs :element name="Folder" type="t:FolderType" maxOccurs="unbounded"/>
  4.       <xs :element name="CalendarFolder" type="t:CalendarFolderType" maxOccurs="unbounded"/>
  5.       <xs :element name="ContactsFolder" type="t:ContactsFolderType" maxOccurs="unbounded"/>
  6.       <xs :element name="SearchFolder" type="t:SearchFolderType" maxOccurs="unbounded"/>
  7.       <xs :element name="TasksFolder" type="t:TasksFolderType" maxOccurs="unbounded"/>
  8.     </xs>

Si ya has generado el cliente y sin haber cambiado esto, las clases "ArrayOf..." y "NonEmpyArrayOf..." y generadas serán incapaces de manejar arrays.
Pero aún tiene solución. Deber añadir a cada clase un campo de tipo List (cualquier Collection vale), y en cada método setXXX añadir el objeto pasado como parámetro al array.
Por ejemplo, en la clase ArrayOfRealItemsType:

  1. // declaras la lista:
  2. private List&lt;ItemType&gt; items = new ArrayList&lt;ItemType&gt;();
  3.  
  4. //sobreescribes los setters para añadir los parametros a la lista:
  5. public void setPostItem(com.microsoft.schemas.exchange.services._2006.types.PostItemType postItem) {
  6.     this.items.add(postItem);
  7.     this.postItem = postItem;
  8. }
  9.  
  10. public void setMessage(com.microsoft.schemas.exchange.services._2006.types.MessageType message) {
  11.     this.items.add(message);
  12.     this.message = message;
  13. }
  14.  
  15. // etc..

Una vez que has generado el cliente verás que te da algunos fallos de compilación.
Esto es porque por algún motivo los constructores no se generan bien.
Tras arreglar los fallos podemos continuar.

La clase principal para realizar las llamadas al Web Service es ExchangeServiceBindingStub.
Creamos el objeto y le ponemos los parametros de usuario, password y servidor.

  1. ExchangeServiceBindingStub esb = (ExchangeServiceBindingStub) new ExchangeServicesLocator().getExchangeServicePort(new  URL(locator.getExchangeServicePortAddress()));
  2. esb.setUsername("usuario");
  3. esb.setPassword("password");
  4. esb._setProperty(javax.xml.rpc.Stub.ENDPOINT_ADDRESS_PROPERTY, "https://my.exchange.com/EWS/Exchange.asmx");

Antes de continuar debes asegurarte que el servidor tiene activada la autentificación básica, si no te saldrá un error HTTP 402.2 "acceso denegado debido a la configuración del servidor"

Ahora solo queda invocar al método del Web Service que quieras y rellenar los parámetros requeridos.

Comments (1)