mardi 30 août 2011

Ajax, Struts2 et JSP





Notre page de paramètres est prête, sauf que le look'n feel n'est pas au rendez-vous. L'idée étant d'avoir un tableau (joli) qui permet de filtrer, ajouter, trier des enregistrements de manière simple.


En cherchant un peu, voici sur quoi je suis tombé : 


http://code.google.com/p/struts2-jquery/wiki/EditGrid#Simple_Edit

Plugin JQuery (Ajax) compatible Struts2, et plus particulièrement cette EditGrid qui fait tout toute seule (enfin presque).


Mise en place : 


 1) Tout d'abord, il faut tirer avec Maven la dépendance sur ce Plugin, direction fichier pom :


<dependency>
<groupId>com.jgeppert.struts2.jquery</groupId>
<artifactId>struts2-jquery-grid-plugin</artifactId>
<version>3.1.1</version>
<type>jar</type>
<scope>compile</scope>
</dependency>


 2) Import dans le Head des JSP des tags nécessaires au chargement de JQuery et de ce plugin.


<sj:head locale="fr" jqueryui="true" defaultIndicator="myDefaultIndicator" jquerytheme="ui-darkness"/>



3) Ajout du composant Grid dans la JSP de base appelée parameter.jsp soumise sur parameterAction


   <s:url id="remoteurl" action="parameterJQueryAction"/>
    <s:url id="editurl" action="parameterEditAction"/>
    <sjg:grid
    id="gridedittable"
   
    dataType="json"
    href="%{remoteurl}"
    pager="true"
    navigator="true"
   
    navigatorSearchOptions="{sopt:['eq','ne','lt','gt']}"
    navigatorAddOptions="{height:200,width:400,reloadAfterSubmit:true}"
    navigatorEditOptions="{height:200,width:400,reloadAfterSubmit:false}"
    navigatorEdit="false"
    navigatorView="false"
    navigatorDelete="true"
    navigatorDeleteOptions="{height:200,width:400,reloadAfterSubmit:true}"
    navigatorExtraButtons="{
    seperator: { 
    title : 'seperator'  
    }, 
    hide : { 
    title : 'Show/Hide', 
    icon: 'ui-icon-wrench', 
    topic: 'showcolumns'
    },
    alert : { 
    title : 'Alert', 
    onclick: function(){ alert('Grid Button clicked!') }
    }
    }"
    gridModel="gridModel"
    rowList="10,15,20"
    rowNum="10"
    editurl="%{editurl}"
    editinline="true"
    onSelectRowTopics="rowselect"
    onEditInlineSuccessTopics="oneditsuccess"
    viewrecords="true"
    >
    <sjg:gridColumn name="id" index="id" title="ID" width="30" formatter="integer" editable="false" sortable="false" search="true" searchoptions="{sopt:['eq','ne','lt','gt']}"/>
    <sjg:gridColumn name="cle" index="cle" title="Cle" width="250" editable="true" edittype="text" sortable="true" search="false"/>
    <sjg:gridColumn name="valeur" index="valeur" width="250" editable="true" edittype="text" title="Valeur" sortable="false" hidden="false"/>
    <sjg:gridColumn name="commentaire" index="commentaire" editable="true" edittype="text" width="250" title="Commentaire" sortable="false" hidden="false"/>
    </sjg:grid>


En bleu, l'action qui permet de peupler la table et renvoyer sur parameter.jsp,
En vert, l'action qui permet le CRUD sur les objets (Create, Retrieve, Update, Delete)

4) Config struts, on retrouve nos 3 actions : 

      A) La première consiste à afficher la page des paramètres (depuis le menu)



                <action name="parameterAction" class="parameterAction">
<result name="success">WEB-INF/jsp/panel/parameter.jsp</result>
</action>


      B) La seconde permet de peupler notre grille, elle est peuplée via une requête AJAX.


<action name="parameterJQueryAction" class="parameterAction">
<result name="success" type="json" />
</action>


     C) La dernière permet de réaliser le CRUD


<action name="parameterEditAction" class="parameterEditAction">
<result name="success">WEB-INF/jsp/panel/parameter.jsp</result>
</action>

5) Détail de l'action parameterAction


package com.homeautomate.ihm.actions;


import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;


import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts2.interceptor.SessionAware;


import com.homeautomate.bean.parameter.Parameter;


public class ParameterAction extends AbstractAction implements SessionAware {


private static final long serialVersionUID = 5078264277068533593L;
private static final Log log = LogFactory.getLog(JQueryAction.class);


private List<Parameter> gridModel;
private List<Parameter> myParameters;
private Integer rows = 0;
private Integer page = 0;
private Integer total = 0;
private Integer record = 0;
private String sord;
private String sidx;
private String searchField;
private String searchString;
private String searchOper;
private boolean loadonce = false;
private Map<String, Object> session;


@Override
public String execute() {
log.debug("Page " + getPage() + " Rows " + getRows()
+ " Sorting Order " + getSord() + " Index Row :" + getSidx());
log.debug("Search :" + searchField + " " + searchOper + " "
+ searchString);


Object list = session.get("mylist");
if (list != null) {
myParameters = (List<Parameter>) list;
} else {
log.debug("Build new List");
myParameters = getParameterManager().getDao().list(Parameter.class);
}


if (getSord() != null && getSord().equalsIgnoreCase("asc")) {
Collections.sort(myParameters);
}
if (getSord() != null && getSord().equalsIgnoreCase("desc")) {
Collections.sort(myParameters);
Collections.reverse(myParameters);
}


setRecord(myParameters.size());


int to = (getRows() * getPage());
int from = to - getRows();


if (to > getRecord())
to = getRecord();


if (loadonce) {
setGridModel(myParameters);
} else {
System.out.println(searchString);
if (searchString != null && searchOper != null) {
try {
int id = Integer.parseInt(searchString);
if (searchOper.equalsIgnoreCase("eq")) {
log.debug("search id equals " + id);
List<Parameter> cList = new ArrayList<Parameter>();
cList.add(getParameterManager().getDao().findById(
Parameter.class, id));
setGridModel(cList);
} else if (searchOper.equalsIgnoreCase("ne")) {
log.debug("search id not " + id);
setGridModel(getParameterManager().getDao().findByQbe(
null));
} else if (searchOper.equalsIgnoreCase("lt")) {
log.debug("search id lesser then " + id);
setGridModel(getParameterManager().getDao().findByQbe(
null));
} else if (searchOper.equalsIgnoreCase("gt")) {
log.debug("search id greater then " + id);
setGridModel(getParameterManager().getDao().findByQbe(
null));
}
} catch (NumberFormatException e) {
searchString = null;
}


} else {
setGridModel(getParameterManager().getDao().list(
Parameter.class));
}
}


setTotal((int) Math.ceil((double) getRecord() / (double) getRows()));


session.put("mylist", myParameters);


return SUCCESS;
}


public String getJSON() {
return execute();
}


/**
* @return how many rows we want to have into the grid
*/
public Integer getRows() {
return rows;
}


/**
* @param rows
*            how many rows we want to have into the grid
*/
public void setRows(Integer rows) {
this.rows = rows;
}


/**
* @return current page of the query
*/
public Integer getPage() {
return page;
}


/**
* @param page
*            current page of the query
*/
public void setPage(Integer page) {
this.page = page;
}


/**
* @return total pages for the query
*/
public Integer getTotal() {
return total;
}


/**
* @param total
*            total pages for the query
*/
public void setTotal(Integer total) {
this.total = total;
}


/**
* @return total number of records for the query. e.g. select count(*) from
*         table
*/
public Integer getRecord() {
return record;
}


/**
* @param record
*            total number of records for the query. e.g. select count(*)
*            from table
*/
public void setRecord(Integer record) {


this.record = record;


if (this.record > 0 && this.rows > 0) {
this.total = (int) Math.ceil((double) this.record
/ (double) this.rows);
} else {
this.total = 0;
}
}


/**
* @return an collection that contains the actual data
*/
public List<Parameter> getGridModel() {
return gridModel;
}


/**
* @param gridModel
*            an collection that contains the actual data
*/
public void setGridModel(List<Parameter> gridModel) {
this.gridModel = gridModel;
}


/**
* @return sorting order
*/
public String getSord() {
return sord;
}


/**
* @param sord
*            sorting order
*/
public void setSord(String sord) {
this.sord = sord;
}


/**
* @return get index row - i.e. user click to sort.
*/
public String getSidx() {
return sidx;
}


/**
* @param sidx
*            get index row - i.e. user click to sort.
*/
public void setSidx(String sidx) {
this.sidx = sidx;
}


public void setSearchField(String searchField) {
this.searchField = searchField;
}


public void setSearchString(String searchString) {
this.searchString = searchString;
}


public void setSearchOper(String searchOper) {
this.searchOper = searchOper;
}


public void setLoadonce(boolean loadonce) {
this.loadonce = loadonce;
}


public void setSession(Map<String, Object> session) {
this.session = session;
}


}

5) Détail de l'action parameterEditAction

package com.homeautomate.ihm.actions;

import com.homeautomate.bean.parameter.Parameter;
import com.homeautomate.dao.IParameterDao;

public class ParameterEditAction extends AbstractAction {

/**
*/
private static final long serialVersionUID = -7217313678052619367L;

private String oper;
private String id;
private String cle;
private String valeur;
private String commentaire;
private IParameterDao parameterDao;

@Override
public String execute() throws Exception {
Parameter parameter;
if (oper.equalsIgnoreCase("add")) {
parameter = new Parameter();
parameter.setCle(cle);
parameter.setCommentaire(commentaire);
parameter.setValeur(valeur);
parameterDao.save(parameter);
} else if (oper.equalsIgnoreCase("edit")) {
parameter = parameterDao.findById(Parameter.class,
Integer.parseInt(id));
parameter.setCle(cle);
parameter.setCommentaire(commentaire);
parameter.setValeur(valeur);
parameterDao.update(parameter);
} else if (oper.equalsIgnoreCase("del")) {
parameter = parameterDao.findById(Parameter.class,
Integer.parseInt(id));
parameterDao.delete(parameter);
}
return SUCCESS;
}

public IParameterDao getParameterDao() {
return parameterDao;
}

public void setParameterDao(IParameterDao parameterDao) {
this.parameterDao = parameterDao;
}

public String getOper() {
return oper;
}

public void setOper(String oper) {
this.oper = oper;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getCle() {
return cle;
}

public void setCle(String cle) {
this.cle = cle;
}

public String getValeur() {
return valeur;
}

public void setValeur(String valeur) {
this.valeur = valeur;
}

public String getCommentaire() {
return commentaire;
}

public void setCommentaire(String commentaire) {
this.commentaire = commentaire;
}
}


On va donc créer 2 classes Actions pour tous les objets persistés de notre application (Au moins pour ceux à modifier et afficher sous format tableau)

Au passage, d'autres thèmes sont dispos, il suffit de les modifier dans le tag du Head.

Aperçus : 





vendredi 26 août 2011

Lien Actions Struts & Spring

Dans la partie Actions Struts, on va injecter des managers standards (JdbManager, I18NManager, ParameterManager, VocalManager & SystemManager).

Pour cela il faut indiquer à Struts que l'on utilise Spring, facile ! Dans le fichier struts.xml, il faut ajouter cette variable : 

<constant name="struts.objectFactory" value="org.apache.struts2.spring.StrutsSpringObjectFactory" />

indiquant à Struts d'aller chercher les Actions dans la conf Spring.

Et dans la configuration Spring, on peut alors déclarer nos actions de cette manière :

Ici je déclare une action "ParameterAction" pointant sur la classe concrète de l'action, je n'oublie pas d'indiquer l'action parente qui contiendra tous mes managers standards, ainsi toutes les actions les posséderont par héritage.

<bean id="parameterAction" class="com.homeautomate.ihm.actions.ParameterAction"  parent="abstractAction">
</bean>


<bean id="abstractAction" class="com.homeautomate.ihm.actions.AbstractAction" abstract="true" >
<property name="jdbManager" ref="jdbManager" />
<property name="parameterManager" ref="parameterManager" />
<property name="vocalManager" ref="vocalManager" />
<property name="i18nManager" ref="i18NManager" />
<property name="systemManager" ref="systemManager" />
</bean>



Petit tour dans la configuration Struts : 

<action name="parameterAction" class="parameterAction">
<result name="success">WEB-INF/jsp/panel/parameter.jsp</result>
</action>



Que va-t-il se passer lorsque j'accéderai à l'action ParamAction ?
              
1) Struts va aller chercher le bean parameterAction dans la conf Spring,
2) Spring va fournir le singleton et injecter automatiquement à la classe parente les managers standard.
3) Struts execute le corps de l'action
4) Struts trappe le retour de l'action (ex: Success) et recherche la jsp à afficher.

Concrètement le contenu de ParamAction (qui rappel, ne fait que remplir une liste de tous les paramètres contenus dans une table depuis la méthode POJO execute() )

package com.homeautomate.ihm.actions;

import java.util.List;

import com.homeautomate.bean.parameter.Parameter;
import com.opensymphony.xwork2.Action;

public class ParameterAction extends AbstractAction {

List<Parameter> parameters;
/**
*/
private static final long serialVersionUID = -8464535536069893165L;

@Override
public String execute() throws Exception {
parameters = getParameterManager().getDao().list(Parameter.class);
return Action.SUCCESS;
}

public List<Parameter> getParameters() {
return parameters;
}

public void setParameters(List<Parameter> parameters) {
this.parameters = parameters;
}
}

                           
On remarque ici, l'utilisation de getParameterManager().getDao().list(Clazz) qui permet de récupérer tous les enregs de la base.

Côté JSP : c'est tout simple : 

Pour afficher les données récupérée, Struts utilise OGNL (Parcoureur de Graph), pour résumer, Struts place le contenu de l'action dans une pile (Stack), l'objet parameters (notre liste de parametres) y est placée.

Si j'ai besoins d'un attribut d'un paramètre, il suffit d'indiquer le nom de l'attribut dans un tag struts, ex : 

// Tag itérator indiquant que l'on va itérer sur les élements de l'objet parameters présent dans la pile

<s:iterator value="parameters">
<tbody>
<tr>
// Ici on indique que l'on souhaite afficher l'id de l'élement courant.
<td><s:property value="id"/></td>
<td><s:property value="cle"/></td>
<td> <s:property value="valeur"/></td>
<td> <s:property value="commentaire"/></td>
</tr>
</tbody>
</s:iterator>

Struts inspecte donc chaque objet présent dans la pile.

Résultat 



Il ne restera donc plus qu'à ajouter les boutons modifier/delete/ajouter.

Pour démarrer le HomeAutomate depuis la GUI, il est nécessaire d'ajouter le démarrage du StartupServlet dans le fichier web.xml et c'est tout.

<servlet>
<servlet-name>StartupServlet</servlet-name>
<servlet-class>com.homeautomate.startup.StartupServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>


lundi 15 août 2011

Premiers pas vers IHM (Full JAVA) Struts2, JSP & AJAX

Après une longue absence, un petit post pour présenter le projet IHM, les bases du projet Core étant bien avancées.
Et oui l'hiver arrive, et pas question de faire un IHM à l'arrache sous la neige ;)

Bon, plusieurs solutions : 

  1. Un nouveau projet GUI avec communication sur CORE via Services WEB
  2. Un nouveau projet GUI avec dépendance CORE.
  3. Intégration dans le CORE.
Finalement, pour gagner du temps, j'opte pour la solution n°2.
Explications : 
 
    Solution 1 : 

Optimale en terme de dépendance projet, il est tout à fait possible de faire une IHM avec le langage préféré (PHP, .NET, VB, C++, ...), il suffisait simplement de définir des WebServices de dialogue avec CORE équipés de leurs signatures XML lisibles par tout le monde. Une autre piste aurait été de réaliser un autre projet JAVA dédié GUI qui dialoguerait avec ce que l'on appelle des DTO (Data Transfer Object), le projet CORE <<exhibant>> les objets (sous forme d'un projet), ainsi la structure interne du Core peut changer mais pas ce qu'il expose (il assure le contrat de service).
Un peu lourd pour moi, je n'ai pas forcément le temps de tout développer aux petits oignons. A noter que de nombreux outils comme Dozer permettent le "peuplement" automatique d'un objet dans un autre, moyennant un peu de conf xml expliquant le mapping lien : http://dozer.sourceforge.net/



   Solution 2 :

Evidemment, cette solution est plus rapide en terme de développement, on accède directement aux objets du projet CORE sans tout redéfinir. l'inconvénient est que si un objet évolue, on devra par capillarité se taper l'évolution côté GUI. Mais bon, le projet doit être cohérent.

   Solution 3 :

L'intégration directe dans le CORE sans séparation de projet, me semble un peu trop incrustée à mon goût. 

A noter, une version client (en ligne de commande est prévue), pratique lorsque l'IHM rebute un peu. car finalement on peut tout faire en ligne de commande.

Vous l'aurez compris, la solution 2 est engagée.

On commence donc par créer un projet JAVA WEB sous Eclipse : 


On y retrouvera nos sources dans le packages src, les jsp, css, images et autres sous WebContent.
On déclarera les dépendances à l'aide d'un fichier pom.xml. 

Alors pour la partie IHM, on utilisera un Pattern MVC (Model Vue Controller) qui permet de dissocier les écrans, du code, et du modèle. Ce pattern évite que l'on retrouve dans les beans (model) et les traitements (Controller) des morceaux de code liés aux IHM. Pour cela j'utiliserai Struts 2 http://struts.apache.org/2.x/

Contenu du WebContent : 



Nos Jsp seront protégées sous WEB-INF.

Struts utilise un fichier de configuration xml qui détermine les actions (url) possibles sur votre site. Et l'action est redirigée vers une classe java qui retournera SUCCESS généralement, dans le fichier de configuration si success est renvoyé, Struts ira charger la JSP indiquée exemple  :


Ici http://monsite/JdbAction sera redirigé vers JdbAction (Classe POJO), le result (SUCCESS par défaut afichera la jsp jjdb.jsp.

Contenu de src (ne pas tenir compte de beans, devices, ancienne version) :



On y retrouve comme prévu struts.xml à la racine. et sous ihm nous regrouperons nos classes actions (vu au dessus), les interceptors (qui permettent comme leurs noms l'indique d'intercepter n'importe quelle action) utilisation pour la sécurité, on y reviendra plus tard avec security. Servlets en cas de servlets à définir, et enfin tags si des tags sont à prévoir.

Voici donc à qui ressemble l'IHM (pour l'instant hyper sobre): 

  1. Menu type dock Mac en haut 
    1. Home.
    2. Alarme.
    3. Caméra.
    4. Luminaires.
    5. Fermetures.
    6. Températures
    7. ChatAction.
    8. Paramètres.
    9. Déconnexion.
  2. Corps de la fonction utilisée.
  3. Footer contenant les x dernières lignes du JDB (raffraichissement automatique avec AJAX)









Voila le squelette, reste à remplir maintenant, je commencerai par la partie paramètres.

Une petite vidéo de la navigation, assez rapide je trouve :




Une autre vidéo sur la partie JDB dynamique se rechargeant toutes les 10 secondes :