Les assembleurs Java.
Pour continuer mes périgrinations dans le monde de LiveCycle Data Services, voici un petit exposé sur les assembleurs Java.
Résumé du système Hibernate
Dans mon premier ticket sur le sujet des assembleurs, j'avais expliqué en long et en large la création d'un assembleur hibernate permettant de récupérer des données parent-enfants dans Flex, et autoriser la mise à jour automatique des modifications en utilisant la simple commande "commit()" côté Flex, Hibernate s'occupant du reste.
Le système est très intéressant, mais nous pouvons vite nous rendre compte de ses limites. Pour peu que le programmeur décide de vouloir formatter les données, de permettre des requêtes SQL complexe pendant la récupération des données ou encore qu'on veuille exécuter plusieurs actions en une seule requête serveur, les assembleurs hibernate ne sont de loin pas la solution idéale.
Quelques exemples auquel j'ai été confronté :
- Récupérer des données hiérarchisées en format XML pour alimenter un composant Tree.
- Lors de la phase d'authentification d'un client, tester son identifiant ainsi que son mot de passe, et, si besoin est, mettre à jour son langage selon une sélection préalable qu'il aura exécutée côté client
Il est souvent probable que les assembleurs hibernates ne seront pas adéquat pour certains cas complexes. Nous avons deux autres possibilités, que j'avais cité dans ma première approche. Il existe deux autres types d'assembleurs qui sont dédiés à hibernate, je cite :
- Les assembleurs SQL
- Les assembleurs JAVA
Les assembleurs SQL fournissent une couche d'accès direct au serveur de base de données par le biais de requêtes SQL. Ils peuvent être intéressants dans certains cas, et surtout lorsque le programmeur ne connait rien à Java. Mais ils nécessitent beaucoup de requêtes et beaucoup d'aller-retour client/serveur pour exécuter une suite du genre "récupération de données, modifications, mises à jour, suppression d'un élément, ajout d'un autre élément". Je ne rentre même pas en matière dans les cas complexes de données liées genre parent-enfants, ou les assembleurs hibernate sont en quelque sorte "magique", une fois que la bonne configuration a été mise en place. Les assembleurs SQL, bien que permettant de pouvoir rapidement mettre en place des communications client/serveur, restent tout de même limités et gourmands en ressource.
Reste les assembleurs JAVA, qui eux, font la toute puissance du système Hibernate. Il est bien clair qu'un pré-requis minimum en JAVA est pratique pour se lancer dans ce type d'assembleur, car c'est par ce langage que tout se défini.
Entrons dans le vif du sujet :
Un assembleur JAVA est généralement composé de 3 classes, ou plus. Contrairement aux assembleurs hibernate, les assembleurs JAVA ne demandent pas de fichier de configuration *.hbm.xml, car Hibernate n'a point besoin de connaitre les liaisons entre les VO, ou Value Object, et les tables de la base de données.
Tout d'abord, nous trouve un "assembleur", qui est une classe étendant la classe AbstractAssembler située dans flex-messaging.jar.
Associé à cette classe, nous allons créer une classe DAO (Data Access Object) qui va s'occuper d'exécuter les requêtes, de récupérer les données et les formatter, et de renvoyer les bonnes données à l'assembleur.
Enfin, comme d'habitude, nous aurons à disposition un ou plusieurs VO (côté serveur), permettant de stocker les données sous forme de classe avec "accesseurs" et de faire une liaison avec les mêmes VO côté client.
Pour mes premiers exemples, je me suis inspiré des exemples fournis avec LCDS, et j'y avais trouvé 2 classes bien utiles : ConnectionHelper.java et DAOException.java. La première permet de récupérer une connexion avec la base de données, tandis que la seconde est utilisée pour renvoyer les exceptions rencontrées par les classes DAO. Voici leur code :
ConnectionHelper.java
/** * */ package ch.titouille.app; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; /** * @author titouille * */ public class ConnectionHelper { /** * url of the db server */ private String url; /** * username to connect on the db */ private String username; /** * password to connect on the db */ private String password; /** * instance of the connection helper */ private static ConnectionHelper instance; /** * @author titouille * initialize basic variables */ //private ConnectionHelper() private ConnectionHelper() { try { // initialization of basic variables Class.forName("com.mysql.jdbc.Driver"); this.url = "jdbc:mysql://localhost/test"; this.username = "root"; this.password = "test"; } catch( Exception e ) { e.printStackTrace(); } } /** * @author titouille * return the singleton ConnectionHelper instance * @return ConnectionHelper */ public static ConnectionHelper getInstance() { if( instance == null ) instance = new ConnectionHelper(); return instance; } /** * @author titouille * get the db connection to execute queries * @return Connection * @throws SQLException */ public static Connection getConnection() throws SQLException { // test if instance exists if( instance == null ) { instance = new ConnectionHelper(); } try { // get db connection return DriverManager.getConnection( instance.url, instance.username, instance.password ); } catch( SQLException e ) { throw e; } } /** * @author titouille * close the db connection * @param connection */ public static void close( Connection connection ) { try { if( connection != null ) { connection.close(); } } catch( SQLException e ) { e.printStackTrace(); } } }
DAOException.java
package ch.titouille.app; /** * * @author titouille * */ public class DAOException extends RuntimeException { /** * serial version uid */ static final long serialVersionUID = -1881205326938716446L; /** * @author titouille * throw DAO exception * @param message */ public DAOException(String message) { super(message); } /** * @author titouille * throw DAO exception * @param cause */ public DAOException(Throwable cause) { super(cause); } /** * @author titouille * throw DAO exception * @param message * @param cause */ public DAOException(String message, Throwable cause) { super(message, cause); } }
A partir de ces 2 classes, je vais pouvoir construire mon application.
Mon premier problème était de pouvoir récupérer des données hiérarchisées en format XML. Les noeuds de base sont des immeubles, les noeuds enfants sont des appartements. Un propriétaire possède 1 ou plusieurs immeubles. Je devais lister pour chaque immeuble ses appartements respectifs, et récupérer les données pour un propriétaire particulier. Après quelques recherches, j'ai fini par trouver comment parser une chaine de caractères et la transformer en format XML valide.
J'ai donc créé un assembleur BuildingAssembler.java n'ayant qu'une seule méthode nommée fill et prenant en argument l'identifiant du propriétaire. Cette méthode crée une instance de la classe BuildingDAO.java, qui s'occupe de récupérer les données, les formatter dans une chaine de caractères, la transformer en format XML, et la retourner à mon assembleur. Ce dernier instancie alors un objet de type BuildingVO qui possède une variable XmlDoc, qui va accueillir les données XML résultantes du processus.
Voici les classes, avec pour chacune une petite explication :
BuildingVO.java
/** * */ package ch.titouille.app.vo; import org.w3c.dom.Document; /** * @author titouille * */ public class BuildingVO { private int id; private Document xmlDoc; /** * @return the id */ public int getId() { return id; } /** * @param id the id to set */ public void setId(int id) { this.id = id; } /** * @return the xmlDoc */ public Document getXmlDoc() { return xmlDoc; } /** * @param xmlDoc the xmlDoc to set */ public void setXmlDoc(Document xmlDoc) { this.xmlDoc = xmlDoc; } }
Le Value Object est simplement composés d'accesseurs (getter / setter) pour chaque donnée. Côté Flex, on aura un VO du même type, qui sera associé à ce VO côté serveur.
Voici le code du VO côté client :
BuildingVO.as
package ch.titouille.app.vo { import flash.xml.XMLDocument; [Managed] [RemoteClass(alias="ch.titouille.app.vo.BuildingVO")] public class BuildingVO { private var _id:uint; private var _xmlDoc:XML; public function BuildingVO() { } public function get id():uint { return this._id; } public function set id( value:uint ):void { this._id = value; } public function get xmlDoc():XML { return this._xmlDoc; } public function set xmlDoc( value:XML ):void { this._xmlDoc = value; } public function get xmlList():XMLList { return this._xmlDoc.children(); } } }
On peut constater que côté Flex, le getter de la variable xmlDoc est du type XML. Java renvoie un type org.w3c.dom.Document et Flex récupère la donnée dans le type XML, sans nécessiter un quelconque autre formattage.
BuildingAssembler.java
/** * @author titouille */ package ch.titouille.app.assemblers; import org.w3c.dom.Document; import flex.data.assemblers.AbstractAssembler; import flex.messaging.io.ArrayCollection; import ch.titouille.app.vo.BuildingVO; import ch.titouille.app.dao.BuildingDAO; /** * @author titouille * */ public class BuildingAssembler extends AbstractAssembler { /** * @author titouille * get buildings / apparts from db switch ownerid * @param actorid * @return */ public ArrayCollection fill( Integer ownerid ) { // create new RoomDAO instance BuildingDAO bdao = new BuildingDAO(); // get document Document x = bdao.getBuildings( ownerid ); // create new returned ArrayCollection ArrayCollection a = new ArrayCollection(); // create new BuildingVO instance and add id and xml document into BuildingVO o = new BuildingVO(); o.setId(1); o.setXmlDoc( x ); // add BuildingVO instance into ArrayCollection a.add( o ); return a; } }
On peut constater que l'assembleur récupère un document XML en appelant la méthode getBuildings du DAO. Un tableau (ArrayCollection) est créé pour stocker les données (Flex nécessite ce type de retour pour être capable de récupérer et de comprendre les données). Enfin, un VO est créé, auquel on affecte un identifiant (arbitraire, la destination Hibernate dans le fichier data-management-config.xml requiert une colonne identifiant) ainsi que le document XML. Le VO est ajouté au tableau, qui est envoyé à Flex.
BuildingDAO.java
/** * */ package com.neolinkup.app.dao; import java.io.ByteArrayInputStream; import java.io.IOException; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.text.CharacterIterator; import java.text.StringCharacterIterator; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import com.neolinkup.app.ConnectionHelper; import com.neolinkup.app.DAOException; /** * @author titouille * */ public class BuildingDAO { /** * @author titouille * get buildings for a specific owner * @param actorid * @return */ public Document getBuildings( Integer ownerid ) { // initialization of basic variables Connection c = null; String s = "<buildings>"; try { // get connection c = ConnectionHelper.getConnection(); // create statement to execute query Statement s1 = c.createStatement(); // get all cinema for a specific user ResultSet rs1 = s1.executeQuery("SELECT buildingid, buildingname FROM building where ownerid=" + ownerid.toString() + " order by buildingname;" ); // if resultSet is not empty while( rs1.next() ) { // open building tag and add each building into XML structure s += "<building id='"+ rs1.getInt("buildingid") + "' label='"+ RoomDAO.forXML( rs1.getString("buildingname") ) +"'>"; // create statement to execute query Statement s2 = c.createStatement(); // get all appartments from current building ResultSet rs2 = s2.executeQuery( "Select appartid, appartname from appart where appartid=" + rs1.getInt("buildingid") + " order by appartweight;" ); // if resultSet is not empty while( rs2.next() ) { // add each appartment into current building hierarchy s += "<appart id='" + rs2.getInt( "appartid" ) + "' label='" + RoomDAO.forXML( rs2.getString( "appartname" ) ) + "'/>"; } // close building tag s += "</building>"; } } catch (SQLException e) { e.printStackTrace(); throw new DAOException(e); } finally { ConnectionHelper.close(c); } // close root tag s += "</buildings>"; return this._getXMLDocument( s ); }
désolé, la classe est coupée en deux, il y a un problème avec l'analyseur syntaxique qui fait planter l'affichage de la page si je le laisse en un seul block...
/** * @author titouille * escape special characters like <, >, ', ", & for XML * @param aText * @return String escaped string */ public static String forXML(String aText) { String result = new String(); final StringCharacterIterator iterator = new StringCharacterIterator(aText); char character = iterator.current(); while (character != CharacterIterator.DONE ) { switch( character ) { case '<' : result += "<"; break; case '>' : result += ">"; break; case '\"' : result += """; break; case '\'' : result += "'"; break; case '&' : result += "&"; break; default : result += character; break; } character = iterator.next(); } return result; } /** * @author titouille * get xml document from xml string * @param s * @return */ private Document _getXMLDocument( String s ) { try { // create a builder factory DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); // get a builder from builder factory DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); // parse string to create xml document Document d = docBuilder.parse( new InputSource( new ByteArrayInputStream( s.getBytes() ) ) ); return d; } // throw exceptions catch( ParserConfigurationException pce ) { System.out.println( "ERROR - ParserConfigurationException: getXmlDocument - " + pce.getMessage() ); return null; } catch( SAXException saxe ) { System.out.println( "ERROR - SAXException: getXmlDocument - " + saxe.getMessage() ); return null; } catch( IOException ioe ) { System.out.println( "ERROR - IOException: getXmlDocument - " + ioe.getMessage() ); return null; } } }
Rien de vraiment complexe : double itération, la première sur les immeubles (building) et la seconde sur les appartements, pour chaque immeuble trouvé. Création d'une chaine de caractères en format XML pour hiérarchiser les données, et enfin, passage par la méthode _getXMLDocument afin de transformer la chaine de caractère en véritable "Document" XML.
Voici les classes utilisées pour mon assembleur Java. Cette construction peut au premier abord rebuter un peu, mais la logique est bien mise en place et permet de séparer les rôles de chacun. Au final, côté Flex, je peux créer un dataService et appeler sa méthode "fill" qui est surchargée dans mon assembleur, et je recevrai automatiquement en retour un tableau (ArrayCollection) contenant 1 seul objet de type BuildingVO, ce dernier ayant dans sa variable xmlDoc le document XML que je désire.
Un dernier ajout est nécessaire pour que le tout soit fonctionnel. Hibernate, même si il n'a pas besoin de mapping avec la base de données puisque Java s'occupe de récupérer les données, doit tout de même définir une "destination" dans le fichier data-management-config.xml pour permettre à la partie "client" de faire des demandes de données.
Voici la destination ajoutée dans mon fichier de configuration :
<destination id="buildingvo.hibernate"> <adapter ref="java-dao" /> <properties> <source>ch.titouille.app.assemblers.BuildingAssembler</source> <scope>application</scope> <metadata> <identity property="id"/> </metadata> <network> <session-timeout>20</session-timeout> <paging enabled="false" pageSize="10" /> <throttle-inbound policy="ERROR" max-frequency="500"/> <throttle-outbound policy="REPLACE" max-frequency="500"/> </network> <server> <fill-method> <name>fill</name> <params>java.lang.Integer</params> </fill-method> </server> </properties> </destination>
Si nous analysons un peu les destinations des assembleurs hibernate et des assembleurs JAVA, ces dernières sont un peu plus simples, car ne nécessitent pas de spécifier quelle est "l'entité hibernate" à lier aux données. Ici, nous spécifions simplement l'assembleur à utiliser, la ou les méthodes disponibles ainsi que leurs paramètres respectifs. Dans cet exemple, une seule méthode est disponible, et prend en charge un unique paramètre du type java.lang.Integer qui est fourni par Flex.
Voilà. Après ces explications, vous voilà fin prêt à attaquer hibernate et en particulier les assembleurs JAVA, afin de rendre vos applications encore plus puissantes