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
@ViewScopedper 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>
