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.
Per generar l'entitat a partir de la taula cal seguir l'article Generació model de dades JPA [1]
Aquí s'expliquen els passos bàsics, per veure en detalla com funciona llegir l'article scaffolding [2].
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.
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.
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; }
Aquesta classe està relacionada amb el model de dades necessari per a les taules de PrimeFaces [3] 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ó.
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:
@ManagedProperty(value = "#{zonaSearchBean}")
@ViewScoped
per una millor gestió de la memòria al servidorpublic void setZonaSearchBean(ZonaSearchBean searchBean)
"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.
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ó [4]es pot trobar una explicació detallada.
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].
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... [6] --> <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:
<diba:llistaCercaButtons search="#{zonaBB.dataModel.search}" reset="#{zonaBB.dataModel.reset}" />
value="#{zonaBB.dataModel.lazyDataModel}"
<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:
<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>
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.
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:
<diba:detailButtons deleteAction="#{zonaBB.delete}" saveOrUpdateAction="#{zonaBB.saveOrUpdate}" cancelLink="pretty:zones" isNew="#{zonaBB.detailEntity.id eq null}" />
<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 [8] és molt clara i amb exemples: <o:outputLabel> [9] <o:highlight> [10]
Enllaços:
[1] https://comunitatdstsc.diba.cat/wiki/fam-generacio-del-model-de-dades
[2] https://comunitatdstsc.diba.cat/wiki/fam-30-scaffolding
[3] http://www.primefaces.org/showcase/ui/data/datatable/basic.xhtml
[4] https://comunitatdstsc.diba.cat/wiki/fam-30-navegacio
[5] http://getbootstrap.com/
[6] http://stackoverflow.com/questions/4573190/jsf-primefaces-update-attribute-does-not-update-component#4574266
[7] https://comunitatdstsc.diba.cat/wiki/fam-30-validacio
[8] http://showcase.omnifaces.org/
[9] http://showcase.omnifaces.org/components/outputLabel
[10] http://showcase.omnifaces.org/components/highlight