FAM 4.0 - Creació d'un manteniment simple
1. Prerequisits
- Instal·lada la darrera versió de l'entorn de desenvolupament de la Diputació.
- Un projecte basat en l'arquetip del FAM Backoffice
2. Creació del manteniment
En aquest article s'explica com generar un CRUD (Create Update Read Delete) agafant com a base una entitat de les més simples del projecte FAM Backoffice: FamZona.
Nota: Als noms de les entitats es respecta el prefix FAM de les taules, però a la resta de classes no per facilitar la lectura i la cerca.
2.1. Generació de l'entitat
Per generar l'entitat a partir de la taula cal seguir l'article Generació model de dades JPA
2.2. Generació de les classes base: servei, BB, data model, cerca i vistes. Scaffolding
Aquí s'expliquen els passos bàsics, per veure en detalla com funciona llegir l'article scaffolding.
Els canvis es fan a l'arxiu /scaffolding.json
i cal modificar la clau entities
amb el nom de l'entitat ["FamZona"]
i la clau base_package
amb el nom del paquet del projecte.
L'arixu quedaria així:
{
"output_directory": ".",
"template_location": "scaffolding",
"base_package": "cat.diba.jee.fam",
"read": false,
"only_with_entity_directives": true,
"entities": ["Zona"]
}
Nota: com es vol obtenir ZonaService, el nom de l'entitat s'informa sense el prefix Fam. A les plantilles, per solucionar possibles problems de si porta o no prefix, ja està contemplat en cadascun dels casos com es veurà als apartats de revisió del codi generat.
Executar la classe tools.Scaffolding
que està definida al menú per obtenir les classes generades a partir de les plantilles.
En aquest punt ja estan creades les classes i les vistes mínimes per implementar el CRUD de l'entitat FamZona. Al següent apartat s'expliquen els canvis concrets de cadascuna de les classes.
3. Modificacions a realitzar
3.1. Servei: ZonaService
Les classes de servei són les que inclouen el negoci de l'aplicació i no tenen accés a la vista. Com que les aplicacions fan servir JPA i un contenidor de transaccions del servidor d'aplicacions, no cal una capa de DAO.
El servei generat en base a la plantilla és el següent:
package cat.diba.jee.fam.service; import javax.ejb.TransactionManagement; import javax.ejb.TransactionManagementType; import com.querydsl.core.types.EntityPath; import cat.diba.jee.core.service.DibaService; import cat.diba.jee.fam.entity.FamZona; import cat.diba.jee.fam.entity.QFamZona; @TransactionManagement(TransactionManagementType.BEAN) public class ZonaService extends DibaService<FamZona> { /** * Constructor per defecte * */ public ZonaService() { super(FamZona.class); } @Override public EntityPath<FamZona> entityPath() { return QFamZona.famZona; } }
Es pot observa l'ús de la classe generada per QueryDSL: QFamZona. En aquest cas no cal modificar ni afegir res al servei.
3.2. Cerca: ZonaSearchBean
Aquesta classse s'utilitza a la vista ja que proporciona els camps per filtrar o cercar resultats i alhora utilitza el servei per poder aplicar les condicions de cerca als resultats.
La classe generada en base a la plantilla és la següent:
package cat.diba.jee.fam.view.search; import javax.faces.bean.ManagedBean; import javax.faces.bean.SessionScoped; import org.primefaces.model.SortOrder; import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; import cat.diba.jee.core.view.search.DibaSearchBean; import cat.diba.jee.fam.entity.QFamZona; @ManagedBean @SessionScoped public class ZonaSearchBean extends DibaSearchBean { /** * */ private static final long serialVersionUID = 1L; /* * (non-Javadoc) * * @see cat.diba.jee.core.view.search.DibaSearchBean#orderBy() */ @Override public OrderSpecifier<?> orderBy() { // FIXME // return QFamZona.zona..asc(); return null; } /* * (non-Javadoc) * * @see cat.diba.jee.core.view.search.DibaSearchBean#order(java.lang.String, * org.primefaces.model.SortOrder) */ @Override public OrderSpecifier<?> orderBy(String field, SortOrder sortOrder) { // FIXME return orderBy(); } /* * (non-Javadoc) * * @see cat.diba.jee.core.view.search.DibaSearchBean#reset() */ public void reset() { // FIXME } /* * (non-Javadoc) * * @see cat.diba.jee.core.view.search.DibaSearchBean#where() */ public Predicate where() { QFamZona zona = QFamZona.famZona; BooleanBuilder where = new BooleanBuilder(); // FIXME return where; } }
A tenir en compte que l'abast del bean és @SessionScoped
per poder mantenir els criteris de cerca i l'índex de la pàgina al navegar per l'aplicació. Si això no és necessari, llavors por canviar-se per @ViewScoped
que és més eficient des del punt de vista de memòria al servidor.
La cerca de zona és només pel nom, llavors caldrà afegir a la classe un atribut que permeti introduir aquest valor a la vista:
private String name; public String getName() { return name; } @Override public void reset() { this.name = null; } public void setName(String name) { this.name = name; }
I adaptar les condicions de cerca amb l'ajuda de la classse QFamZona generada per QueryDSL:
public Predicate where() { QFamZona famZona = QFamZona.famZona; BooleanBuilder where = new BooleanBuilder(); if (null != name) { where = where.and(famZona.famZonaNom.containsIgnoreCase(name)); } return where; }
I per últim, es modifican les condicions d'ordenació. Més endavant s'explica com es lliga amb la vista i la taula:
/** * Ordenació per defecte */ @Override public OrderSpecifier<?> orderBy() { return QFamZona.famZona.famZonaNom.asc(); } /** * Ordenació segons les columnes de la taula de la vista */ @Override public OrderSpecifier<?> orderBy(String sortField, SortOrder sortOrder) { OrderSpecifier<?> orderBy = orderBy(); if (sortField != null && sortOrder != null) { if (sortField.endsWith(QFamZona.famZona.famZonaNom.getMetadata().getName())) { orderBy = SortOrder.DESCENDING.equals(sortOrder) ? QFamZona.famZona.famZonaNom.desc() : QFamZona.famZona.famZonaNom.asc(); } } return orderBy; }
3.3. Model de dades: ZonaDataModel
Aquesta classe està relacionada amb el model de dades necessari per a les taules de PrimeFaces amb càrrega lazy dels elements.
El model de dades generat en base a la plantilla és el següent:
package cat.diba.jee.fam.view.datamodel; import javax.inject.Inject; import cat.diba.jee.core.service.DibaService; import cat.diba.jee.core.view.datamodel.DibaDataModel; import cat.diba.jee.fam.entity.FamZona; import cat.diba.jee.fam.service.ZonaService; public class ZonaDataModel extends DibaDataModel<FamZona> { @Inject private ZonaService zonaService; @Override public DibaService<FamZona> service() { return zonaService; } }
No cal fer cap modificació.
3.4. Backing Bean o Managed Bean: ZonaBB
Aquesta és la classe que gestiona totes les crides de la vista i retorna els valors a mostrar.
La classe generada en base a la plantilla és la següent:
package cat.diba.jee.fam.view.bb; import javax.faces.bean.ManagedBean; import javax.faces.bean.ManagedProperty; import javax.faces.bean.ViewScoped; import javax.inject.Inject; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import cat.diba.jee.core.service.DibaService; import cat.diba.jee.core.view.bb.DibaBB; import cat.diba.jee.core.view.search.DibaSearchBean; import cat.diba.jee.fam.entity.FamZona; import cat.diba.jee.fam.service.ZonaService; import cat.diba.jee.fam.view.datamodel.ZonaDataModel; import cat.diba.jee.fam.view.search.ZonaSearchBean; @ManagedBean @ViewScoped public class ZonaBB extends DibaBB<FamZona> { private static final long serialVersionUID = 1L; private static final String CLASS_ID = ZonaBB.class.getName(); private static final Log LOG = LogFactory.getLog(CLASS_ID); @Inject private ZonaService zonaService; @Inject private ZonaDataModel dataModel; @ManagedProperty(value = "#{zonaSearchBean}") private ZonaSearchBean zonaSearchBean; @Override public ZonaDataModel getDataModel() { return dataModel; } @Override public String index() { LOG.trace(CLASS_ID + "::index()"); return "pretty:zonas"; } @Override public DibaService<FamZona> service() { return zonaService; } @Override protected FamZona newEntity() { LOG.trace(CLASS_ID + "::newEntity()"); return new FamZona(); } /** * Instància el bean de cerca, que és de sessió, per poder mantenir els * valors dels camps i la darrera pàgina de la taula al tornar d'editar una * entitat. * * @see DibaSearchBean#setLastFirst(int) * @see DibaSearchBean#getLastFirst() * */ public void setZonaSearchBean(ZonaSearchBean searchBean) { this.zonaSearchBean = searchBean; this.dataModel.setSearchBean(searchBean); } }
Aspectes a considerar:
- El bean de cerca s'injecta com a una propietat de forma que pot compartir-se entre diversos beans:
@ManagedProperty(value = "#{zonaSearchBean}")
- L'abast és
@ViewScoped
per una millor gestió de la memòria al servidor - Quan s'injecta el bean de cerca, automàticament s'instància al model de dades. Veure:
public void setZonaSearchBean(ZonaSearchBean searchBean)
- La vista de 'índex no és el plural correcte de Zona i es modificarà per coherència a
"pretty:zones"
Segons les necessitats del negoci, cal ampliar ZonaBB per poder tornar totes les zones ordenades pel nom i mostrar-les en un camp de selecció desplegable. A la vista veurem com es tracta la llista d'entitats sense necessitat de transformar-la.
/** * Zones amb l'ordenació per defecte (el nom) * * @return */ public List<FamZona> getZones() { List<FamZona> zones = new ArrayList<FamZona>(); try { zones = zonaService.findAll(null, zonaSearchBean.orderBy()); } catch (IllegalStateException | SecurityException | SystemException | RollbackException | HeuristicRollbackException e) { MessagesUtils.errorMessage(new DibaException(e)); } return zones; }
I aquestes són les modificacions a fer en les classes generades. A continuació es veure com definir la navegació amb PrettyFaces y les vistes.
4. Navegació: PrettyFaces
Les regles de navegació que cal agefir a l'arxiu pretty-aplicacio.xml
són
<!-- Índex o llista: des del menu o des de l'edició --> <url-mapping id="zones"> <pattern value="/zones" /> <view-id value="/views/zona/zonaLlista.xhtml" /> </url-mapping> <!-- Edició de la zona des de la fila de la taula --> <url-mapping id="viewZona"> <pattern value="/zones/#{id}/edita" /> <view-id value="/views/zona/zonaDetall.xhtml" /> <!-- Evita executar l'acció amb submit o Ajax, només amb GET --> <action onPostback="false">#{zonaBB.edit}</action> </url-mapping> <!-- Nova zona des de l'índex o llistat --> <url-mapping id="newZona"> <pattern value="/zones/crea" /> <view-id value="/views/zona/zonaDetall.xhtml" /> <!-- Evita executar l'acció amb submit o Ajax, només amb GET --> <action onPostback="false">#{zonaBB.create}</action> </url-mapping>
A l'article navegació es pot trobar una explicació detallada.
5. Vistes
Ara les vistes tenen una plantilla base, /WebRoot/WEB-INF/templates/layout.xhtml
, i els arxius xhtml
concrets de les entitats només descriuen la vista concreta.
Com a pas previ, cal afegir a l'arxiu on es defineix el menú, /WebRoot/WEB-INF/templates/menuAplicacio.xhtml, l'opció de la llista de zones:
<li><h:link outcome="pretty:zones">#{literalsAplicacio['Menu.Zona']}</h:link></li>
El scaffolding també genera dos plantilles per les vistes més habituals de l'aplicació dins d'un directori amb el nom de l'entitat:
/WebRoot/views/zona/zonaDetall.xhtml
/WebRoot/views/zona/zonaLlista.xhtml
Cal tenir en compte que el layout està basat en Bootstrap 3.
5.1. Llista
El codi generat per la vista és el següent:
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" xmlns:o="http://omnifaces.org/ui" xmlns:of="http://omnifaces.org/functions" xmlns:diba="http://java.sun.com/jsf/composite/diba/components" template="/WEB-INF/templates/layout.xhtml"> <ui:define name="content"> <div class="page-header c-diba"> <h2>#{literalsAplicacio['Zona.IndexTitle']} <small>#{literalsCore['Helper.Index']}</small> </h1> </div> <h:form id="zona_llistat_form" styleClass="form-horizontal"> <!-- http://stackoverflow.com/questions/4573190/jsf-primefaces-update-attribu... --> <h:panelGroup id="zona_search_fields" layout="block" styleClass="well well-sm"> <!-- FIXME: camps de cerca --> <div class="form-group"> <!-- Exemple <o:outputLabel for="descripcio" styleClass="col-sm-1 control-label" value="#{literalsAplicacio['Zona.Descripcio']}" /> <div class="col-sm-3"> <h:inputText styleClass="form-control" id="descripcio" value="#{zonaSearchBean.descripcio}" /> </div> --> </div> <diba:llistaCercaButtons search="#{zonaBB.dataModel.search}" reset="#{zonaBB.dataModel.reset}" /> </h:panelGroup> <div class="table-responsive well well-sm"> <p:dataTable id="zona_taula" paginatorPosition="top" var="element" tableStyleClass="table table-striped table-hover" tableStyle="width: 100%;" value="#{zonaBB.dataModel.lazyDataModel}" lazy="true" paginator="true" rows="20" paginatorTemplate="{RowsPerPageDropdown} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {CurrentPageReport} {New}" currentPageReportTemplate="#{literalsCore['Helper.Table.PageTemplate']}" emptyMessage="#{literalsCore['Helper.Table.Empty']}" first="#{zonaBB.dataModel.first}"> <f:facet name="{New}"> <span class="pull-right"> <h:link styleClass="btn btn-default" style="text-align: right;" role="button" outcome="pretty:newZona" > <i class="fa fa-plus" aria-hidden="true"></i> #{literalsCore['Helper.New']} </h:link> </span> </f:facet> <!-- FIXME: columnes de la taula --> <!-- Exemple <p:column headerText="#{literalsAplicacio['Zona.Descripcio']}"> <h:link outcome="pretty:viewZona" value="#{element.id}"> <f:param name="id" value="#{element.id}"></f:param> </h:link> </p:column> --> </p:dataTable> </div> </h:form> </ui:define> </ui:composition>
Aspectes a destacar:
- Els botons de la cerca fan servir un component propi de la Diputació que rep com a paràmetres els dos mètodes a cridar: cerca i neteja.
<diba:llistaCercaButtons search="#{zonaBB.dataModel.search}" reset="#{zonaBB.dataModel.reset}" />
- El model de dades de la taula es declara així:
value="#{zonaBB.dataModel.lazyDataModel}"
- El botó de nou element es declara com a un facet de la taula:
<f:facet name="{New}"> <span class="pull-right"> <h:link styleClass="btn btn-default" style="text-align: right;" role="button" outcome="pretty:newZona" > <i class="fa fa-plus" aria-hidden="true"></i> #{literalsCore['Helper.New']} </h:link> </span> </f:facet>
Les modificacions a fer són:
- Afegir un camp de cerca al primer FIXME:
<div class="form-group"> <o:outputLabel for="nom" styleClass="col-sm-1 control-label" value="#{literalsAplicacio['Zona.Nom']}" /> <div class="col-sm-3"> <h:inputText styleClass="form-control" id="nom" value="#{zonaSearchBean.name}" /> </div> </div>
- Afegir una columna a la taula amb ordenació i enllaç al detall de l'entitat:
Indicar a la taula la columna per defecte afegint la propietat següent a p:dataTable: sortBy="#{element.famZonaNom}" <p:column headerText="#{literalsAplicacio['Zona.Nom']}" sortBy="#{element.famZonaNom}"> <h:link outcome="pretty:viewZona" value="#{element.famZonaNom}"> <f:param name="id" value="#{element.id}"></f:param> </h:link> </p:column>
I aquest entitat no necessita més canvis a la llista.
5.2. Detall
El codi generat per la vista és el següent:
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui" xmlns:o="http://omnifaces.org/ui" xmlns:of="http://omnifaces.org/functions" xmlns:diba="http://java.sun.com/jsf/composite/diba/components" template="/WEB-INF/templates/layout.xhtml"> <ui:define name="content"> <o:importConstants type="cat.diba.jee.fam.view.bb.ZonaBB" /> <div class="page-header c-diba"> <h2>#{literalsAplicacio['Zona.EditTitle']} <small>#{literalsCore['Helper.Edit']}</small> </h2> </div> <h:form styleClass="form-horizontal" id="zona_detail_form"> <div class="form-edit"> <!-- FIXME: camps del formulari --> <div class="form-group"> </div> </div> <diba:detailButtons deleteAction="#{zonaBB.delete}" saveOrUpdateAction="#{zonaBB.saveOrUpdate}" cancelLink="pretty:zones" isNew="#{zonaBB.detailEntity.id eq null}" /> </h:form> <o:highlight styleClass="has-error" focus="true" /> </ui:define> </ui:composition>
Aspectes a destacar:
- Els botons del detall estan definits com a un component de la Diputació. Només cal proporcionar els paràmetres corresponents:
<diba:detailButtons deleteAction="#{zonaBB.delete}" saveOrUpdateAction="#{zonaBB.saveOrUpdate}" cancelLink="pretty:zones" isNew="#{zonaBB.detailEntity.id eq null}" />
- Per facilitar la validació (veure l'article de validació per més informació ), cal afegir fora del form:
<o:highlight styleClass="has-error" focus="true" />
La plantilla generada no incorpora el contingut del formulari amb els camps, per tant si afegim els camps al formulari el resultat és:
<div class="form-edit"> <div class="form-group"> <o:outputLabel for="zonaNom" value="#{literalsAplicacio['Zona.Nom']}" styleClass="control-label col-sm-1" /> <div class="col-sm-3"> <h:inputText value="#{zonaBB.detailEntity.famZonaNom}" id="zonaNom" styleClass="form-control"> <f:validateRequired /> </h:inputText> </div> </div> </div>
El punt més important és que l'atribut for de l'outputLabel coincideix amb l'id de l'InputText i llavors queda lligat el missatge d'error que és obligatori. La documentació original d'OmniFaces és molt clara i amb exemples: <o:outputLabel> <o:highlight>