Archive for general

SEO-friendly URLs con Seam II

Vamos a ampliar la información del post anterior con conceptos más avanzados.
Partimos de la base de que tenemos configurado y funcionando el módulo UrlRewrite.

El primer problema con el que me topé es que mi aplicación usaba identificadores numéricos, tipo .../product.seam?idProduct=123. Por lo tanto necesitaba traducir el identificador numerico a una palabra descriptiva del producto, como .../product/silla o .../product/mesa.

Afortunadamente con Seam eso no presenta mucha dificultad y puede emplearse una EL Expression tipo #{rewriter.fromNameToId('silla')}.
Por desgracia, en el archivo urlrewrite.xml no pueden incluirse EL Expressions.

Para solucionarlo, creé una 'página virtual' en el pages.xml que recogía los parámetros como String de la url y mediante una llamada a un componente de Seam los traducía en el correspondiente identificador numérico, aprovechando que en el archivo pages.xml sí que se pueden usar EL Expressions.

  • Fichero urlrewrite.xml
    1. <urlrewrite>
    2.    <rule>
    3.      <from>^/pruduct/([A-Za-z]*)$</from>
    4.      <to last="true">/virtual_product.seam?name=$1</to>
    5.    </rule>
    6.  </urlrewrite>

  • Fichero pages.xml
    1. .....
    2. <page view-id="/virtual_product.xhtml" action="#{product.init}">
    3.     <param name="name"/>
    4.     <navigation>
    5.         <redirect view-id="/product.seam">   
    6.             <param name="idProduct" value="#{urlRewriter.idProductFromName(name)}"/>
    7.         </redirect>
    8.     </navigation>
    9. </page>
    10. .....

  • Componente rewrite
    1. public int idProductFromName(String name) {
    2.     // Consulta en la bbdd para halar
    3.     // el id del producto a partir del nombre
    4. }

Leave a Comment

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)

Problemas con Google Maps

Como siempre que se desarrolla una aplicación, aparecen errores que nos dan multitud de quebraderos de cabeza.
Ahí van unos cuantos con los que me he enfrentado trabajando con Google Maps.

  • El fondo del mapa se ve totalmente en gris
    Casi seguro es porque alguna propiedad css está interfiriendo con el mapa. Un caso en el que aparece es con la propiedada overflow:hidden.
  • El fondo del mapa se ve parcialmente gris, y el centro se sitúa en la esquina superior izquierda
    Esto ocurre porque cuando la api de google maps carga el mapa, el div contenedor que ha sido pasado como argumento al constructor GMap2 tiene tamaño cero. El trozo de mapa que se ve es realmente el trozos que queda fuera de los limites de visión pero es cargado para mejorar el rendimiento al desplazar el mapa.
    Que el div contendor tenga tamaño cero, suele ocurrir cuando está dentro de una tabla o tiene el tamaño especificado con porcentajes. Hasta que el DOM no se carga completamente, el div no adquiere un tamaño definido, y en el momento en el que se carga el mapa aun no tiene su tamaño final.
    Se puede solucionar especificando el ancho (width) y alto (height) del contendor sin porcentajes o especificando directamente el tamaño del mapa con un argumento en el constructor GMap2:

    new GMap2(container, {size:new GSize(width,  height)})
  • Object cannot be created in this context" code: "9" ... (NS_ERROR_DOM_NOT_SUPPORTED_ERR)
    Este error suele aparecer en firefox cuando el mapa se inserta en una pagina JSF con Facelets.Es debido a que el objeto 'document' de javascript creado por firefox cuando el mime type es 'application/xhtml+xml', no tiene el metodo write().
    Para solucionarlo tienes que establecer el content type de la pagina como text/html. Con JSF se puede hacer añadiendo el tag

    		<f contenttype="text/html" />

    en la página que contenga el mapa (No afecta a facelets ni al funcionamiento de la pagina).

Leave a Comment