@Entity // 1 @EntityValidator // 2 @RemoveValidator // 3 public class NombreEntidad { // 4 // Propiedades // 5 // Referencias // 6 // Colecciones // 7 // Métodos // 8 // Buscadores // 9 // Métodos de retrollamada // 10 }
@Embeddable // 1 public class NombreIncrustada { // 2 // Propiedades // 3 // Referencias // 4 // Metodos // 5 }
@Stereotype // 1 @Column(length=) @Column(precision=) @Max @Length(max=) @Digits(integer=) // 2 @Digits(integer=) @Digits(fraction=) // 3 @Required @Min @Range(min=) @Length(min=) // 4 @Id // 5 @Hidden // 6 @SearchKey // 7 @Version // 8 @Formula // 9 Nuevo en v3.1.4 @Calculation // 10 Nuevo en v5.7 @DefaultValueCalculator // 11 @PropertyValidator // 12 private tipo nombrePropiedad; // 13 public tipo getNombrePropiedad() { ... } // 13 public void setNombrePropiedad(tipo nuevoValor) { ... } // 13
<editor url="editorNombrePersona.jsp">
<para-estereotipo estereotipo="NOMBRE_PERSONA"/>
<para-anotacion anotacion="com.tuempresa.tuaplicacion.anotaciones.NombrePersona"/> <!-- Nuevo en v6.6 -->
</editor>
De esta forma indicamos que editor se ha de ejecutar para editar y
visualizar propiedades con el estereotipo NOMBRE_PERSONA. Fíjate como
desde v6.6 puedes usar una anotación en lugar de un estereotipo, o usar
ambas cosas.<para-estereotipo nombre="NOMBRE_PERSONA" longitud="40"/>
<para-anotacion clase="com.tuempresa.tuaplicacion.anotaciones.NombrePersona" longitud="40"/> <!-- Nuevo en v6.6 -->
Y así si no ponemos longitud asumirá 40 por defecto.<validador-requerido>
<clase-validador clase="org.openxava.validators.NotBlankCharacterValidator"/>
<para-estereotipo estereotipo="NOMBRE_PERSONA"/>
<para-anotacion anotacion="com.tuempresa.tuaplicacion.anotaciones.NombrePersona"/> <!-- Nuevo en v6.6 -->
</validador-requerido>
Ahora podemos definir propiedades con estereotipo NOMBRE_PERSONA:@Stereotype("NOMBRE_PERSONA")
private String nombre;
En este caso asume 40 longitud y tipo String, así como
ejecutar el validador NotBlankCharacterValidator@NombrePersona
private String nombre;
@Stereotype("GALERIA_IMAGENES") private String fotos;Además, en el mapeo tenemos que mapear la propiedad contra una columna adecuada para almacenar una cadena (String) con 32 caracteres de longitud (VARCHAR(32)).
CREATE TABLE IMAGENES ( ID VARCHAR(32) NOT NULL PRIMARY KEY, GALLERY VARCHAR(32) NOT NULL, IMAGE BLOB); CREATE INDEX IMAGENES01 ON IMAGENES (GALLERY);El tipo de la columna IMAGE puede ser un tipo más adecuado para almacenar byte [] en el caso de nuestra base de datos (por ejemplo LONGVARBINARY) .
<persistence-unit name="default"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <non-jta-data-source>java:comp/env/jdbc/OpenXavaTestDS</non-jta-data-source> <class>org.openxava.session.GalleryImage</class> <!-- AÑADE ESTA LÍNEA --> <class>org.openxava.web.editors.DiscussionComment</class> ... </persistence-unit>Fíjate que hemos añadido <class>org.openxava.session.GalleryImage</class>.
<hibernate-configuration> <session-factory> ... <mapping resource="GalleryImage.hbm.xml"/> ... </session-factory> </hibernate-configuration>Después de todo esto ya podemos usar el estereotipo GALERIA_IMAGENES en los componentes de nuestra aplicación.
@File
@Column(length=32)
private String documento;
@Stereotype("ARCHIVO")
@Column(length=32)
private String documento;
Usa @Files (nuevo en v6.6) para adjuntar múltiples
archivos:@Files
@Column(length=32)
private String documentos;
@Stereotype("ARCHIVOS")
@Column(length=32)
private String documentos;
Cuando usas la versión con anotación (@File o @Files)
puedes definir atributos como acceptFileTypes o maxFileSizeInKb
para restringir los archivos que el usuario puede subir. Por
ejemplo, con este código:
@File(acceptFileTypes="image/*", maxFileSizeInKb=90)
@Column(length=32)
private String foto;
@Files(acceptFileTypes="text/csv, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
@Column(length=32)
private String hojasCalculo;
filePersistorClass=org.openxava.web.editors.JPAFilePersistor ...
<persistence-unit name="default"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <non-jta-data-source>java:comp/env/jdbc/OpenXavaTestDS</non-jta-data-source> <class>org.openxava.session.GalleryImage</class> <class>org.openxava.web.editors.AttachedFile</class> ... </persistence-unit> ... <persistence-unit name="junit"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <class>org.openxava.web.editors.AttachedFile</class> .... </persistence-unit>Observa que hemos añadido <class>org.openxava.web.editors.AttachedFile</class> a ambas unidades de persistencia.
CREATE TABLE OXFILES ( ID VARCHAR(32) NOT NULL PRIMARY KEY, NAME VARCHAR(255), DATA LONGVARBINARY, LIBRARYID VARCHAR(32) );Debemos verificar que el tipo de la columna DATA sea el tipo más adecuado para almacenar byte[] (en nuestro caso LONGVARBINARY).
Las propiedades anotadas con @File, @Files, Stereotype("ARCHIVO") o @Stereotype("ARCHIVOS") solo almacenan un identificador de 32 caracteres, no almacenan el contenido del archivo. Para acceder desde tu propio código al contenido del archivo subido has de usar un IFilePersistor que se obtiene desde FilePersistorFactory, clases que puedes encontrar en el paquete org.openxava.web.editors. Estas clases funcionan igual no importa si los archivos se guardan en el sistema de archivos, una base de datos o cualquier otro sitio.
En el caso de @File o @Stereotype("ARCHIVO") la propiedad guarda directamente el id del archivo. Por ejemplo si tenemos una propiedad como esta:
@File @Column(length=32)
String foto;
Podemos llenarla con un archivo usando nuestro propio código así:
import java.nio.file.*;
import org.openxava.actions.*;
import org.openxava.web.editors.*;
public class CargarFoto extends ViewBaseAction {
public void execute() throws Exception {
// En este ejemplo obtenemos el archivos del sistema de archivos
// pero en tu caso puedes obtener el archivo desde cualquier otro sitio
String filePath = "/home/me/images/mifoto.png";
byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
// Un IFilePersistor para trabajar con el archivo
IFilePersistor filePersistor = FilePersistorFactory.getInstance();
// Creamos un objeto AttachedFile
AttachedFile file = new AttachedFile();
file.setName("mifoto.png");
file.setData(fileBytes);
// Esto graba el archivo
filePersistor.save(file);
// Después de grabado, el AttachedFile tiene el id generado
// por lo que lo asignamos a la propiedad de la vista
getView().setValue("foto", file.getId());
}
}
Creas un AttachedFile y lo guardas con un IFilePersistor, luego obtienes el id del archivo para usarlo como valor para la propiedad. La propiedad foto almacena el id del archivo.
El proceso contrario, es decir, obtener y manipular el archivo que ya hay en la propiedad @File, sería así:
import java.nio.file.*;
import org.openxava.actions.*;
import org.openxava.web.editors.*;
public class GrabarFoto extends ViewBaseAction {
public void execute() throws Exception {
// Un IFilePersistor para trabajar con el archivo
IFilePersistor filePersistor = FilePersistorFactory.getInstance();
// Obtenemos el id de la foto de la propiedad
String photoId = getView().getValueString("foto");
// Y buscamos el AttachedFile a partir del id usando IFilePersistor
AttachedFile file = filePersistor.find(photoId);
// Obtenemos el nombre y el contenido del AttachedFile
String fileName = file.getName();
byte[] fileBytes = file.getData();
// En este ejemplo grabamos el archivo en el sistema de archivos
// pero tú puedes hacer lo que quieras con él
String filePath = "/home/me/images/" + fileName;
Files.write(Paths.get(filePath), fileBytes);
}
}
Buscas un AttachedFile usando un IFilePersistor a partir del id de la foto que tienes en la propiedad.
Trabajar con @Files o @Stereotype("ARCHIVOS") es ligeramente diferente, porque en este caso en la propiedad se almacen el id de la librería, no del archivo. Una librería es un grupo de archivos. Cada archivo tiene su propio id, pero todos comparten un id común de librería. Por ejemplo, con una propiedad como esta:
@Files @Column(length=32)
String documentos;
Podemos llenarla con varios archivos usando nuestro propio código así:
import java.nio.file.*;
import org.openxava.actions.*;
import org.openxava.web.editors.*;
public class CargarDocumentos
extends GenerateIdForPropertyBaseAction { // Para usar el método generateIdForProperty()
public void execute() throws Exception {
// En este ejemplo vamos a subir algunos archivos desde el sistema de archivos
// pero podrías obtener los archivos o su contenido desde cualquier otro lugar
String basePath = "/home/me/documents/";
String [] fileNames = {
"limiting-data-by-user.pdf",
"quick-start.odg"
};
// Necesitamos generar un id para la librería la primera vez o usar el que ya existe
// Este trabajo lo hace generateIdForProperty() por nosotros
// El id generado se deja en la propiedad 'documentos' en la vista
String libraryId = generateIdForProperty("documentos");
// Un IFilePersistor para trabajar con los archivos
IFilePersistor filePersistor = FilePersistorFactory.getInstance();
for (String fileName: fileNames) {
// En nuestro ejemplo obtenemos el contenido del archivo desde el sistema de archivos
byte[] fileBytes = Files.readAllBytes(Paths.get(basePath + fileName));
// Creamos un AttachedFile y lo rellenamos
AttachedFile file = new AttachedFile();
file.setLibraryId(libraryId); // El mismo libraryId para todos los archivos
file.setName(fileName);
file.setData(fileBytes);
// Lo grabamos usando IFilePersistor
filePersistor.save(file);
}
}
}
El truco es que tenemos que tener un único id de librería para asignar a cada uno de los archivos que vamos a ir guardando. Este id lo generamos con generateIdForProperty() que lo genera si no existe o lo devuelve si ya existente, también si genera uno nuevo lo asigna a la propiedad en la vista, por lo que al grabar la entidad se grabará con el id de librería correcto. Lo demás es simplemente un bucle creando AttachedFile y grabándolos con IFilePersistor, asignando a cada uno el mismo id de librería.
El proceso contrario, es decir, obtener y manipular los archivos que ya hay en la propiedad @Files, sería así:
import java.nio.file.*;
import java.util.*;
import org.openxava.actions.*;
import org.openxava.web.editors.*;
public class GrabarDocumentos extends ViewBaseAction {
public void execute() throws Exception {
// Un IFilePersistor para trabajar con los archivos
IFilePersistor filePersistor = FilePersistorFactory.getInstance();
// Con @Files la propiedad almacena el id de la librería y no un id de archivo
String libraryId = getView().getValueString("documentos");
// Usamos findLibrary() de IFilePersistor para obtener la colección de archivos
Collection<AttachedFile> files =filePersistor.findLibrary(libraryId);
for (AttachedFile file: files) {
// Obtenemos el nombre y el contenido de AttachedFile
String fileName = file.getName();
byte[] fileBytes = file.getData();
String filePath = "/home/me/documents/" + fileName;
// En nuestro ejemplo lo grabamos en el sistema de archivos,
// pero en tu caso puedes hacer lo que quieras
Files.write(Paths.get(filePath), fileBytes);
}
}
}
Recuerda que para @Files en la propiedad se guarda el id de la librería, no el id del archivo. A partir de este id usamos el método findLibrary() de IFilePersistor para obtener todos los archivos asociados a esa librería, es decir a esa propiedad. Después hacemos un bucle sobre esos archivos y los procesamos al gusto.
@Discussion
@Column(length=32)
private String discusion;
O con el estereotipo DISCUSION si usas una versión anterior a la
6.6. Así:@Stereotype("DISCUSION")
@Column(length=32)
private String discusion;
@PreRemove private void borrarDiscusion() { DiscussionComment.removeForDiscussion(discusion); }Verifica que persistence.xml contiene la entidad DiscussionComment, si no añádelo:
<persistence-unit name="default"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <non-jta-data-source>java:comp/env/jdbc/OpenXavaTestDS</non-jta-data-source> <class>org.openxava.session.GalleryImage</class> <class>org.openxava.web.editors.DiscussionComment</class> <!-- AÑADE ESTA LÍNEA --> ... </persistence-unit>Fíjate que hemos añadido <class>org.openxava.web.editors.DiscussionComment</class>. Cuando se genere la base datos, la tabla OXDISCUSSIONCOMMENTS se creará:
CREATE TABLE OXDISCUSSIONCOMMENTS ( ID VARCHAR(32) NOT NULL, COMMENT CLOB(16777216), DISCUSSIONID VARCHAR(32), TIME TIMESTAMP, USERNAME VARCHAR(30), PRIMARY KEY (ID) ); CREATE INDEX OXDISCUSSIONCOMMENTS_DISCUSSIONID_INDEX ON OXDISCUSSIONCOMMENTS (DISCUSSIONID);Comprueba que el tipo para la columna COMMENT es el más adecuado para almacenar un texto grande (CLOB por defecto) en tu base de datos, si no haz un ALTER COLUMN para poner un tipo mejor.
@Coordinates @Column(length=50)
private String ubicacion;
# OpenTopoMap
mapsTileProvider=https://b.tile.opentopomap.org/{z}/{x}/{y}.png
mapsAttribution=Map data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://opentopomap.org">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)
# MapBox
# Cambia abajo YOUR_ACCESS_TOKEN por tu propio token de acceso
mapsTileProvider=https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token=YOUR_ACCESS_TOKEN
mapsAttribution=Map data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>
mapsTileSize=512
mapsZoomOffset=-1
@View(members=
"ciudad [ estado; "
+ "condicionEstado;"
+ "codigo;"
+ "nombre;"
+ "población;"
+ "codigoPostal;"
+ "condado;"
+ "pais;"
+ "fundacion;"
+ "superficie;"
+ "altitud;"
+ "tipoGobierno;"
+ "alcalde;"
+ "], "
+ "ubicacion")
@Mask("L-000000") private String pasaporte;
@Mask("0000 0000 0000 0000") private String tarjeta;
@Mask("LL 000 AA") private String patente;
@Mask("0.000/0-000") private String customMask;
@Version private Integer version;Esta propiedad es para uso del mecanismo de persistencia (Hibernate o JPA), ni nuestra aplicación ni usuarios deberían acceder directamente a ella. Si no usas evolución automática de esquema recuerda añadir la columna VERSION a la tabla.
private Distancia distancia; public enum Distancia { LOCAL, NACIONAL, INTERNACIONAL };La propiedad distancia solo puede valer LOCAL, NACIONAL o INTERNACIONAL, y como no hemos puesto @Required también permite valor vacío (null). Desde v5.3 si pones @Required, la primera opción es por defecto y ya no mostrará valor vacío. Si deseas cambiar la opción por defecto usa @DefaultValueCalculator. Desde v5.6.1 los enums anotados con @Required en una clase incrustable mostrarán valor vacío si ésta es utilizada en una colección de elementos.
public enum Prioridad implements IIconEnum {
BAJA("transfer-down"), MEDIA("square-medium"), ALTA("transfer-up");
private String icon;
private Priority(String icon) {
this.icon = icon;
}
public String getIcon() {
return icon;
}
};
private Prioridad prioridad;
Simplemente haz que tu enum implemente IIconEnum
que fuerza a que tengas un método getIcon(). Este método ha de
devolver un identificador de icono de Material
Design Icons. OpenXava puede usar estos iconos en varias partes de
la interfaz de usuario, por ejemplo en la lista:@Depends("precioUnitario") // 1 @Max(9999999999L) // 2 public BigDecimal getPrecioUnitarioEnPesetas() { if (precioUnitario == null) return null; return precioUnitario.multiply(new BigDecimal("166.386")) .setScale(0, BigDecimal.ROUND_HALF_UP); }De acuerdo con esta definición ahora podemos usar el código de esta manera:
Producto producto = ... producto.setPrecioUnitario(2); BigDecimal resultado = producto.getPrecioUnitarioEnPesetas();Y resultado contendrá 332,772.
@Max(999) public int getCantidadLineas() { // Un ejemplo de uso de JDBC Connection con = null; try { con = DataSourceConnectionProvider.getByComponent("Factura").getConnection(); // 1 String tabla = MetaModel.get("LineaFactura").getMapping().getTable(); PreparedStatement ps = con.prepareStatement("select count(*) from " + tabla + " where FACTURA_AÑO = ? and FACTURA_NUMERO = ?"); ps.setInt(1, getAño()); ps.setInt(2, getNumero()); ResultSet rs = ps.executeQuery(); rs.next(); Integer result = new Integer(rs.getInt(1)); ps.close(); return result; } catch (Exception ex) { log.error("Problemas al calcular cantidad de líneas de una Factura", ex); // Podemos lanzar cualquier RuntimeException aquí throw new SystemException(ex); } finally { try { con.close(); } catch (Exception ex) { } } }Es verdad, el código JDBC es feo y complicado, pero a veces puede ayudar a resolver problemas de rendimiento. La clase DataSourceConnectionProvider nos permite obtener la conexión asociada a la misma fuente de datos que la entidad indicada (en este caso Factura). Esta clase es para nuestra conveniencia, también podemos acceder a una conexión JDBC usando JNDI o cualquier otro medio que queramos. De hecho, en una propiedad calculada podemos escribir cualquier código que Java nos permita.
private long codigo; @Id @Column(length=10) // Anotamos el getter, public long getCodigo() { // por tanto JPA usará acceso basado en propiedades para nuestra clase return codigo; } public void setCodigo(long codigo) { this.codigo = codigo; } @Transient // Hemos de anotar como Transient nuestra propiedad calculada public String getZoneOne() { // porque usamos acceso basado en propiedades return "En ZONA 1"; }
@org.hibernate.annotations.Formula("PRECIOUNITARIO * 1.16") private BigDecimal precioUnitarioConIVA; public BigDecimal getPrecioUnitarioConIVA() { return precioUnitarioConIVA; }El uso es simple. Hemos de poner el cálculo como lo hariamos si lo tuvieramos que poner en una sentencia SQL.
@DefaultValueCalculator(CurrentYearCalculator.class) private int año;En este caso cuando el usuario intenta crear una nueva factura (por ejemplo) se encontrará con que el campo de año ya tiene valor, que él puede cambiar si quiere. La lógica para generar este valor está en la clase CurrentYearCalculator class, así:
package org.openxava.calculators; import java.util.*; /** * @author Javier Paniza */ public class CurrentYearCalculator implements ICalculator { public Object calculate() throws Exception { Calendar cal = Calendar.getInstance(); cal.setTime(new java.util.Date()); return new Integer(cal.get(Calendar.YEAR)); } }Es posible personalizar el comportamiento de un calculador poniendo el valor de sus propiedades, como sigue:
@DefaultValueCalculator( value=org.openxava.calculators.StringCalculator.class, properties={ @PropertyValue(name="string", value="BUENA") } ) private String relacionConComercial;En este caso para calcular el valor por defecto OpenXava instancia StringCalculator y entonces inyecta el valor "BUENA" en la propiedad string de StringCalculator, y finalmente llama al método calculate() para obtener el valor por defecto para relacionConComercial. Como se ve, el uso de la anotación @PropertyValue permite crear calculadores reutilizable.
@DefaultValueCalculator( value=org.openxava.test.calculadores.CalculadorObservacionesTransportista.class, properties={ @PropertyValue(name="tipoPermisoConducir", from="permisoConducir.tipo") } ) private String observaciones;En este caso antes de ejecutar el calculador OpenXava llena la propiedad permisoConducir de CalculadorObservacionesTransportista con el valor de la propiedad visualizada tipo de la referencia permisoConducir. Como se ve el atributo from soporta propiedades calificadas (referencia.propiedad). Además, cada ve que permisoConducir.tipo cambia observaciones se recalcula (nuevo en v5.1, con versiones anteriores se recalculaba solo la primera vez).
@DefaultValueCalculator(value=CalculadorPrecioDefectoProducto.class, properties= @PropertyValue(name="codigoFamilia") )En este caso OpenXava coge el valor de la propiedad visualizada codigoFamilia y lo inyecta en la propiedad codigoFamilia del calculador, es decir @PropertyValue(name="codigoFamilia") equivale a @PropertyValue(name="codigoFamilia", from="codigoFamilia").
@DefaultValueCalculator(value=CalculadorCantidadLineas.class, properties= { @PropertyValue(name="año"), @PropertyValue(name="numero"), } ) private int cantidadLineas;Y la clase del calculador:
package org.openxava.test.calculadores; import java.sql.*; import org.openxava.calculators.*; import org.openxava.util.*; /** * @author Javier Paniza */ public class CalculadorCantidadLineas implements IJDBCCalculator { // 1 private IConnectionProvider provider; private int año; private int numero; public void setConnectionProvider(IConnectionProvider provider) { // 2 this.provider = provider; } public Object calculate() throws Exception { Connection con = provider.getConnection(); try { PreparedStatement ps = con.prepareStatement( "select count(*) from XAVATEST.LINEAFACTURA “ + “where FACTURA_AÑO = ? and FACTURA_NUMERO = ?"); ps.setInt(1, getAño()); ps.setInt(2, getNumero()); ResultSet rs = ps.executeQuery(); rs.next(); Integer result = new Integer(rs.getInt(1)); ps.close(); return result; } finally { con.close(); } } public int getAño() { return año; } public int getNumero() { return numero; } public void setAño(int año) { this.año = año; } public void setNumero(int numero) { this.numero = numero; } }Para usar JDBC nuestro calculador tiene que implementar IJDBCCalculator (1) y entonces recibirá un IConnectionProvider (2) que podemos usar dentro de calculate().
@Id @Hidden @GeneratedValue(strategy=GenerationType.IDENTITY) private Integer id;Podemos usar otras técnicas de generación, por ejemplo, una sequence de base de datos puede ser definida usando el estándar JPA de esta manera:
@SequenceGenerator(name="SIZE_SEQ", sequenceName="SIZE_ID_SEQ", allocationSize=1 ) @Hidden @Id @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SIZE_SEQ") private Integer id;Si queremos generar un identificador único de tipo String y 32 caracteres, podemos usar una extensión de Hibernate de JPA:
@Id @GeneratedValue(generator="system-uuid") @Hidden @GenericGenerator(name="system-uuid", strategy = "uuid") private String oid;Ver la sección 9.1.9 de la especificación JPA 1.0 (parte de JSR-220) para aprender más sobre @GeneratedValues.
@PrePersist private void calcularContador() { contador = new Long(System.currentTimeMillis()).intValue(); }La anotación JPA @PrePersist hace que este método se ejecute antes de insertar datos por primera vez en la base de datos, en este método podemos calcular el valor para nuestra clave o incluso para propiedades no clave con nuestra propia lógica.
@PropertyValidator(value=ValidadorExcluirCadena.class, properties= @PropertyValue(name="cadena", value="MOTO") ) @PropertyValidator(value=ValidadorExcluirCadena.class, properties= @PropertyValue(name="cadena", value="COCHE"), onlyOnCreate=true ) private String descripcion;Con un OpenXava anterior a 6.1 has de usar @PropertyValidators para englobar las anotaciones, así:
@PropertyValidators ({ // Sólo necesario hasta v6.0.2 @PropertyValidator(value=ValidadorExcluirCadena.class, properties= @PropertyValue(name="cadena", value="MOTO") ), @PropertyValidator(value=ValidadorExcluirCadena.class, properties= @PropertyValue(name="cadena", value="COCHE"), onlyOnCreate=true ) }) private String descripcion;La forma de configurar el validador (con los @PropertyValue, aunque el atributo from no funciona, hay que usar value siempre) es exactamente igual como en los calculadores. Con el atributo onlyOnCreate=”true” se puede definir que esa validación solo se ejecute cuando se crea el objeto, y no cuando se modifica.
package org.openxava.test.validadores; import org.openxava.util.*; import org.openxava.validators.*; /** * @author Javier Paniza */ public class ValidadorExcluirCadena implements IPropertyValidator { // 1 private String cadena; public void validate( Messages errores, // 2 Object valor, // 3 String nombreObjecto, // 4 String nombrePropiedad) // 5 throws Exception { if (valor==null) return; if (valor.toString().indexOf(getCadena()) >= 0) { errores.add("excluir_cadena", nombrePropiedad, nombreObjeto, getCadena()); } } public String getCadena() { return cadena==null?"":cadena; } public void setCadena(String cadena) { this.cadena = cadena; } }Un validador ha de implementar IPropertyValidator (1), esto le obliga a tener un método validate() en donde se ejecuta la validación de la propiedad. Los argumentos del método validate() son:
excluir_cadena={0} no puede contener {2} en {1}Si el identificador que se envía no está en el archivo de mensajes, sale tal cual al usuario; pero lo recomendado es siempre usar identificadores del archivo de mensajes.
@PropertyValidator(value=ValidadorTituloLibro.class, message="{libro_rpg_no_permitido}") private String titulo;Si el mensaje está entre llaves se obtiene de los archivos i18n, si no se usa tal cual.
public class ValidadorTituloLibro implements IPropertyValidator, IWithMessage { private String message; public void setMessage(String message) throws Exception { this.message = message; // Este es message de @PropertyValidator } public void validate(Messages errors, Object value, String propertyName, String modelName) { if (((String)value).contains("RPG")) { errors.add(message); // Podemos añadir el mensaje directamente } } }El mensaje especificado en la anotación @PropertyValidator, libro_rpg_no_permitido, se inyecta en el validador llamando a setMessage(). Este mensaje puede ser añadido directamente como un error.
<validadores> <validador-defecto> <clase-validador clase="org.openxava.test.validadores.ValidadorNombrePersona"/> <para-estereotipo stereotipo="NOMBRE_PERSONA"/> </validador-defecto> </validadores>En este caso estamos asociando el validador ValidadorNombrePersona al estereotipo NOMBRE_PERSONA. Ahora si definimos una propiedad como la siguiente:
@Required @Stereotype("NOMBRE_PERSONA") private String nombre;Esta propiedad será validada usando ValidadorNombrePersona aunque la propiedad misma no defina ningun validador. ValidadorNombrePersona se aplica a todas las propiedades con el estereotipo NOMBRE_PERSONA.
@Calculation("((horas * trabajador.precioHora) + desplazamiento - descuento) * porcentajeIVA / 100") private BigDecimal total;Fíjate como trabajador.precioHora se usa para obtener el valor de una referencia.
Cliente cliente = ... cliente.getComercial().getNombre();para acceder al nombre del comercial de ese cliente.
@Required // 1 @Id // 2 @SearchKey // 3 Nuevo en v3.0.2 @DefaultValueCalculator // 4 @ManyToOne( // 5 optional=false // 1 ) private tipo nombreReferencia; // 5 public tipo getNombreReferencia() { ... } // 5 public void setNombreReferencia(tipo nuevoValor) { ... } // 5
@ManyToOne private Comercial comercial; // 1 public Comercial getComercial() { return comercial; } public void setComercial(Comercial comercial) { this.comercial = comercial; } @ManyToOne(fetch=FetchType.LAZY) private Comercial comercialAlternativo; // 2 public Comercial getComercialAlternativo() { return comercialAlternativo; } public void setComercialAlternativo(Comercial comercialAlternativa) { this.comercialAlternativo = comercialAlternativo; }
Cliente cliente = ... Comercial comercial = cliente.getComercial(); Comercial comercialAlternativo = cliente.getComercialAlternativo();
@ManyToOne(optional=false, fetch=FetchType.LAZY) @JoinColumn(name="FAMILY") @DefaultValueCalculator(value=IntegerCalculator.class, properties= @PropertyValue(name="value", value="2") ) private Familia familia;El método calculate() de este calculador es:
public Object calculate() throws Exception { return new Integer(value); }Como se puede ver se devuelve un entero, es decir, el valor para familia por defecto es la familia cuyo código es el 2.
@ManyToOne(fetch=FetchType.LAZY) @JoinColumns({ @JoinColumn(name="ZONA", referencedColumnName="ZONA"), @JoinColumn(name="ALMACEN", referencedColumnName="CODIGO") }) @DefaultValueCalculator(CalculadorDefectoAlmacen.class) private Almacen almacen;Y el código del calculador:
package org.openxava.test.calculadores; import org.openxava.calculators.*; /** * @author Javier Paniza */ public class CalculadorDefectoAlmacen implements ICalculator { public Object calculate() throws Exception { Almacen clave = new Almacen(); clave.setNumber(4); clave.setZoneNumber(4); return clave; } }Devuelve un objeto de tipo Almacen pero rellenando sólo las propiedades clave.
@Entity @IdClass(DetalleAdicionalKey.class) public class DetalleAdicional { // JoinColumn se especifica también en DetalleAdicionalKey por un // bug de Hibernate, ver http://opensource.atlassian.com/projects/hibernate/browse/ANN-361 @Id @ManyToOne(fetch=FetchType.LAZY) @JoinColumn(name="SERVICIO") private Servicio servicio; @Id @Hidden private int contador; ... }Además, necesitamos escribir la clase clave:
public class DetalleAdicionalKey implements java.io.Serializable { @ManyToOne(fetch=FetchType.LAZY) @JoinColumn(name="SERVICIO") private Servicio servicio; @Hidden private int contador; // equals, hashCode, toString, getters y setters ... }Necesitamos escribir la clase clave aunque la clave sea solo una referencia con una sola columna clave.
@Embedded private Direccion direccion;Y hemos de definir la clase Direccion como incrustable:
package org.openxava.test.model; import javax.persistence.*; import org.openxava.annotations.*; /** * * @author Javier Paniza */ @Embeddable public class Direccion implements IConPoblacion { @Required @Column(length=30) private String calle; @Required @Column(length=5) private int codigoPostal; @Required @Column(length=20) private String poblacion; // ManyToOne dentro de un Embeddable no está soportado en JPA 1.0 (ver en 9.1.34), // pero la implementación de Hibernate lo soporta. @ManyToOne(fetch=FetchType.LAZY, optional=false) @JoinColumn(name="STATE") private Provincia provincia; public String getPoblacion() { return poblacion; } public void setPoblacion(String poblacion) { this.poblacion = poblacion; } public String getCalle() { return calle; } public void setCalle(String calle) { this.calle = calle; } public int getCodigoPostal() { return codigoPostal; } public void setCodigoPostal(int codigoPostal) { this.codigoPostal = codigoPostal; } public Provincia getProvincia() { return provincia; } public void setProvincia(Provincia provincia) { this.provincia = provincia; } }Como se ve una clase incrustable puede implementar una interfaz (1) y contener referencias (2), entre otras cosas, pero no puede usar métodos de retrollamada de JPA.
Cliente cliente = ... Direccion direccion = cliente.getDireccion(); direccion.getCalle(); // para obtener el valorO así para establecer una nueva dirección
// para establecer una nueva dirección Direccion direccion = new Direccion(); direccion.setCalle(“Mi calle”); direccion.setCodigoPostal(46001); direccion.setMunicipio(“Valencia”); direccion.setProvincia(provincia); cliente.setDireccion(direccion);En este caso que tenemos una referencia simple, el código generado es un simple JavaBean, cuyo ciclo de vida esta asociado a su objeto contenedor, es decir, la Direccion se borrará y creará junto al Cliente, jamas tendrá vida propia ni podrá ser compartida por otro Cliente.
@Size // 1 @Condition // 2 @OrderBy // 3 @XOrderBy // 4 @OrderColumn // 5 @OneToMany/@ManyToMany // 6 private Collection<TuEntidad> nombreColeccion; // 5 public Collection<TuEntidad> getNombreColeccion() { ... } // 5 public void setNombreColeccion(Collection<TuEntidad> nuevoValor) { ... } // 5
@OneToMany (mappedBy="factura") private Collection<Albaran> albaranes; public Collection<Albaran> getAlbaranes() { return albaranes; } public void setAlbaranes(Collection<Albaran> albaranes) { this.albaranes = albaranes; }Si ponemos esto dentro de una Factura, estamos definiendo una colección de los albaranes asociados a esa Factura. La forma de relacionarlo se hace en la parte del mapeo objeto-relacional. Usamos mappedBy="factura" para indicar que la referencia factura de Albaran se usa para mapear esta colección.
Factura factura = ... for (Albaran albaran: factura.getAlbaranes()) { albaran.hacerAlgo(); }Para hacer algo con todos los albaranes asociados a una factura.
@OneToMany (mappedBy="factura", cascade=CascadeType.REMOVE) // 1 @OrderBy("tipoServicio desc") // 2 @org.hibernate.validator.Size(min=1) // 3 private Collection<LineaFactural> facturas;
@Condition( "${almacen.codigoZona} = ${this.almacen.codigoZona} AND " + "${almacen.codigo} = ${this.almacen.codigo} AND " + "NOT (${codigo} = ${this.codigo})" ) public Collection<Transportista> getCompañeros() { return null; }Si ponemos esta colección dentro de Transportista, podemos obtener todos los transportista del mismo almacén menos él mismo, es decir, la lista de sus compañeros. Es de notar como podemos usar this en la condición para referenciar al valor de una propiedad del objeto actual. @Condition solo aplica a la interfaz de usuario generada por OpenXava, si llamamos directamente a getFellowCarriers() retornará null.
public Collection<Transportista> getCompañeros() { Query query = XPersistence.getManager().createQuery("from Transportista t where " + "t.almacen.codigoZona = :zona AND " + "t.almacen.codigo = :codigoAlmacen AND " + "NOT (t.codigo = :codigo) "); query.setParameter("zona", getAlmacen().getCodigoZona()); query.setParameter("codigoAlmacen", getAlmacen().getCodigo()); query.setParameter("codigo", getCodigo()); return query.getResultList(); }Como se ve es un método getter. Obviamente ha de devolver una java.util.Collection cuyos elementos sean de tipo Transportista.
@OneToMany(mappedBy="comercial") private Collection<Cliente> clientes;Para indicar que es la referencia comercial y no comercialAlternativo la que vamos a usar para esta colección.
@Entity public class Cliente { ... @ManyToMany private Collection<Provincia> provincias; ... }En este caso un cliente tiene una colección de provincias, pero una misma provincia puede estar presente en varios clientes.
@OneToMany (mappedBy="factura", cascade=CascadeType.REMOVE) private Collection<LineaFactura> lineas;Es de notar que usamos CascadeType.REMOVE y LineaFactura es una entidad y no una clase incrustable:
package org.openxava.test.model; import java.math.*; import javax.persistence.*; import org.hibernate.annotations.Columns; import org.hibernate.annotations.Type; import org.hibernate.annotations.Parameter; import org.hibernate.annotations.GenericGenerator; import org.openxava.annotations.*; import org.openxava.calculators.*; import org.openxava.test.validators.*; /** * * @author Javier Paniza */ @Entity @EntityValidator(value=ValidadorLineaFactura.class, properties= { @PropertyValue(name="factura"), @PropertyValue(name="oid"), @PropertyValue(name="producto"), @PropertyValue(name="precioUnitario") } ) public class LineaFactura { @ManyToOne // 'Lazy fetching' produce un falla al borrar una linea desde la factura private Factura factura; @Id @GeneratedValue(generator="system-uuid") @Hidden @GenericGenerator(name="system-uuid", strategy = "uuid") private String oid; private TipoServicio tipoServicio; public enum TipoServicio { ESPECIAL, URGENTE } @Column(length=4) @Required private int cantidad; @Stereotype("DINERO") @Required private BigDecimal precioUnitario; @ManyToOne(fetch=FetchType.LAZY, optional=false) private Producto producto; @DefaultValueCalculator(CurrentDateCalculator.class) private java.util.Date fechaEntrega; @ManyToOne(fetch=FetchType.LAZY) private Comercial vendidoPor; @Stereotype("MEMO") private String observaciones; @Stereotype("DINERO") @Depends("precioUnitario, cantidad") public BigDecimal getImporte() { return getPrecioUnitario().multiply(new BigDecimal(getCantidad())); } public boolean isGratis() { return getImporte().compareTo(new BigDecimal("0")) <= 0; } @PostRemove private void postRemove() { factura.setComentario(factura.getComentario() + "DETALLE BORRADO"); } public String getOid() { return oid; } public void setOid(String oid) { this.oid = oid; } public TipoServicio getTipoServicio() { return tipoServicio; } public void setTipoServicio(TipoServicio tipoServicio) { this.tipoServicio = tipoServicio; } public int getCantidad() { return cantidad; } public void setCantidad(int cantidad) { this.cantidad = cantidad; } public BigDecimal getPrecioUnitario() { return precioUnitario==null?BigDecimal.ZERO:precioUnitario; } public void setPrecioUnitario(BigDecimal precioUnitario) { this.precioUnitario = precioUnitario; } public Product getProducto() { return producto; } public void setProducto(Producto producto) { this.producto = producto; } public java.util.Date getFechaEntrega() { return fechaEntrega; } public void setFechaEntrega(java.util.Date fechaEntrega) { this.fechaEntrega = fechaEntrega; } public Comercial getVendidoPor() { return vendidoPor; } public void setVendidoPor(Comercial vendidoPor) { this.vendidoPor = vendidoPor; } public String getObservaciones() { return observaciones; } public void setObservaciones(String observaciones) { this.observaciones = observaciones; } public Invoice getFactura() { return factura; } public void setFactura(Factura factura) { this.factura = factura; } }Como se ve esto es una entidad compleja, con calculadores, validadores, referencias y así por el estilo. También hemos de definir una referencia a su clase contenedora (factura). En este caso cuando una factura se borre todas sus líneas se borrarán también. Además hay diferencias a nivel de interface gráfica (podemos aprender más en el capítulo de la vista).
@Size // 1 @OrderBy // 2 @OrderColumn // 3 Nuevo en v5.3 @ElementCollection // 4 private Collection<TuClaseIncrustable> nombreColeccion; // 3 public Collection<TuClaseIncrustable> getNombreColeccion() { ... } // 3 public void setNombreColeccion<TuClaseIncrustable> nuevoValor) { ... } // 3
@Entity public class Presupuesto extends Identifiable { ... @ElementCollection private Collection<LineaPresupuesto> lineas; public Collection<LineaPresupuesto> getLineas() { return lineas; } public void setLineas(Collection<LineaPresupuesto> lineas) { this.lineas = lineas; } ... }Ahora definimos nuestra clase incrustada:
@Embeddable public class LineaPresupuesto { @ManyToOne(fetch=FetchType.LAZY, optional=false) // 1 private Producto producto; @Required // 2 private BigDecimal precioUnitario; @Required private int cantidad; private Date fechaDisponibilidad; @Column(length=30) private String comentarios; @Column(precision=10, scale=2) @Depends("precioUnitario, cantidad") public BigDecimal getImporte() { // 3 return getPrecioUnitario().multiply(new BigDecimal(getCantidad())); } ... }Como se puede ver, una clase incrustable usada en una colección de elementos puede contener referencias(1), validaciones(2) y propiedades calculadas(3) entre otras cosas.
@OneToMany(mappedBy="proyecto", cascade=CascadeType.ALL) @OrderColumn private List<TareaProyecto> tareas;La interfaz de usuario permitirá al usuario cambiar el orden de los elementos y este orden se almacenará en la base de datos. Además, si se cambia el orden de los elementos por código este orden también se persistirá en la base de datos.
ALTER TABLE TAREAPROYECTO ADD TAREAS_ORDER INTEGEREn la implementación actual el usuario cambia el orden arrastrando y soltando, con colecciones @OneToMany el orden se almacena justo después de soltar, mientras que en las colecciones @ElementCollection el orden se almacen al grabar la entidad contenedora.
public void incrementarPrecio() { setPrecioUnitario(getPrecioUnitario().multiply(new BigDecimal("1.02")).setScale(2)); }Los métodos son la salsa de los objetos, sin ellos solo serían caparazones tontos alrededor de los datos. Cuando sea posible es mejor poner la lógica de negocio en los métodos (capa del modelo) que en las acciones (capa del controlador).
public static Cliente findByCodigo(int codigo) throws NoResultException { Query query = XPersistence.getManager().createQuery( "from Cliente as o where o.codigo = :codigo"); query.setParameter("codigo", codigo); return (Cliente) query.getSingleResult(); } public static Collection findTodos() { Query query = XPersistence.getManager().createQuery("from Cliente as o"); return query.getResultList(); } public static Collection findByNombreLike(String nombre) { Query query = XPersistence.getManager().createQuery( "from Cliente as o where o.nombre like :nombre order by o.nombre desc"); query.setParameter("nombre", nombre); return query.getResultList(); }Estos métodos se pueden usar de esta manera:
Cliente cliente = Cliente.findByCodigo(8); Collection javieres = Cliente.findByNombreLike(“%JAVI%”);Como se ve, usar método buscadores produce un código más legible que usando la verbosa API de JPA. Pero esto es solo una recomendación de estilo, podemos escoger no escribir métodos buscadores y usar directamente consultas de JPA.
@EntityValidator( value=clase, // 1 onlyOnCreate=(true|false), // 2 properties={ @PropertyValue ... } // 3 )
@EntityValidator(value=org.openxava.test.validadores.ValidadorProductoBarato.class, properties= { @PropertyValue(name="limite", value="100"), @PropertyValue(name="descripcion"), @PropertyValue(name="precioUnitario") }) public class Producto {Y el código del validador:
package org.openxava.test.validadores; import java.math.*; /** * @author Javier Paniza */ public class ValidadorProductoBarato implements IValidator { // 1 private int limite; private BigDecimal precioUnitario; private String descripcion; public void validate(Messages errores) { // 2 if (getDescripcion().indexOf("CHEAP") >= 0 || getDescripcion().indexOf("BARATO") >= 0 || getDescripcion().indexOf("BARATA") >= 0) { if (getLimiteBd().compareTo(getPrecioUnitario()) < 0) { errors.add("producto_barato", getLimiteBd()); // 3 } } } public BigDecimal getPrecioUnitario() { return precioUnitario; } public void setPrecioUnitario(BigDecimal decimal) { precioUnitario = decimal; } public String getDescripcion() { return descripcion==null?"":descripcion; } public void setDescripcion(String string) { descripcion = string; } public int getLimite() { return limite; } public void setLimite(int i) { limite = i; } private BigDecimal getLimiteBd() { return new BigDecimal(Integer.toString(limite)); } }Este validador ha de implementar IValidator (1), lo que le obliga a tener un método validate(Messages messages) (2). En este método solo hay que añadir identificadores de mensajes de error (3) (cuyos textos estarán en los archivos i18n), si en el proceso de validación (es decir en la ejecución de todos los validadores) hubiese al menos un mensaje de error, OpenXava no graba la información y visualiza los mensajes al usuario.
@EntityValidator(value=org.openxava.test.validadores.ValidadorProductoBarato.class, properties= { @PropertyValue(name="limite", value="100"), @PropertyValue(name="descripcion"), @PropertyValue(name="precioUnitario") }) @EntityValidator(value=org.openxava.test.validadores.ValidadorProductoCaro.class, properties= { @PropertyValue(name="limite", value="1000"), @PropertyValue(name="descripcion"), @PropertyValue(name="precioUnitario") }) @EntityValidator(value=org.openxava.test.validadores.ValidadorPrecioProhibido.class, properties= { @PropertyValue(name="precioProhibido", value="555"), @PropertyValue(name="precioUnitario") }, onlyOnCreate=true ) public class Product {Con un OpenXava anterior a 6.1 has de usar @EntityValidators para poder aplicar varios validadores:
@EntityValidators({ // Sólo necesario hasta v6.0.2 @EntityValidator(value=org.openxava.test.validadores.ValidadorProductoBarato.class, properties= { @PropertyValue(name="limite", value="100"), @PropertyValue(name="descripcion"), @PropertyValue(name="precioUnitario") }), @EntityValidator(value=org.openxava.test.validadores.ValidadorProductoCaro.class, properties= { @PropertyValue(name="limite", value="1000"), @PropertyValue(name="descripcion"), @PropertyValue(name="precioUnitario") }), @EntityValidator(value=org.openxava.test.validadores.ValidadorPrecioProhibido.class, properties= { @PropertyValue(name="precioProhibido", value="555"), @PropertyValue(name="precioUnitario") }, onlyOnCreate=true ) }) public class Product {@EntityValidator está definida como una restriccion de Bean Validation a partir de v5.3 y como una restricción de Hibernate Validator hasta v5.2.x.
@RemoveValidator( value=clase, // 1 properties={ @PropertyValue ... } // 2 )
@RemoveValidator(value=ValidadorBorrarTipoAlbaran.class, properties=@PropertyValue(name="codigo") ) public class TipoAlbaran {Y el validador:
package org.openxava.test.validadores; import org.openxava.test.model.*; import org.openxava.util.*; import org.openxava.validators.*; /** * @author Javier Paniza */ public class ValidadorBorrarTipoAlbaran implements IRemoveValidator { // 1 private TipoAlbaran tipoAlbaran; private int codigo; // Usamos esto (en vez de obtenerlo de tipoAlbaran) // para probar @PropertyValue con propiedades simples public void setEntity(Object entidad) throws Exception { // 2 this.tipoAlbaran = (TipoAlbaran) entidad; } public void validate(Messages errores) throws Exception { if (!tipoAlbaran.getAlbaranes().isEmpty()) { errores.add("no_borrar_tipo_albaran_si_albaranes", new Integer(getCodigo())); // 3 } } public int getCodigo() { return codigo; } public void setCodigo(int codigo) { this.codigo = codigo; } }Como se ve tiene que implementar IRemoveValidator (1) lo que le obliga a tener un método setEntity() (2) con el recibirá el objeto que va a ser borrado. Si hay algún error de validación se añade al objeto de tipo Messages enviado a validate() (3). Si después de ejecutar todas las validaciones OpenXava detecta al menos 1 error de validación no realizará el borrado del objeto y enviará la lista de mensajes al usuario.
@PrePersist private void antesDeCrear() { setDescripcion(getDescripcion() + " CREADO"); }En este caso cada vez que se graba por primera vez un TipoAlbaran se añade un sufijo a su descripción.
@PreUpdate private void antesDeModificar() { setDescripcion(getDescripcion() + " MODIFICADO"); }En este caso cada vez que se modifica un TipoAlbaran se añade un sufijo a su descripción.
public onPreCreate { // Crea automaticamente un cliente if (getCliente() == null) { Cliente clte = new Cliente(); clte.setNombre(getNombre()); clte.setDireccion(getDireccion()); clte = XPersistence.getManager().merge(clte); setCliente(clte); } }En este ejemplo, la operación del manejador de persistencia, no afectará el comportamiento de este y las demás retrollamadas. Además de @PreCreate están disponible @PostCreate y @PreDelete. Los métodos que son decorados con estas anotaciones forman parte de la misma transacción donde se ejecutaran las retrollamadas de JPA. Cuando estas retrollamadas son combinadas con las de JPA el orden de ejecución es de acuerdo a lo siguiente:
package org.openxava.test.model; import javax.persistence.*; import org.hibernate.annotations.*; import org.openxava.annotations.*; /** * Clase base para definir entidades con un oid UUID. <p> * * @author Javier Paniza */ @MappedSuperclass public class Identificable { @Id @GeneratedValue(generator="system-uuid") @Hidden @GenericGenerator(name="system-uuid", strategy = "uuid") private String oid; public String getOid() { return oid; } public void setOid(String oid) { this.oid = oid; } }Podemos definir otra @MappedSuperclass que extienda de esta, por ejemplo:
package org.openxava.test.model; import javax.persistence.*; import org.openxava.annotations.*; /** * Clase base para entidades con una propiedad 'nombre'. <p> * * @author Javier Paniza */ @MappedSuperclass public class ConNombre extends Identifiable { @Column(length=50) @Required private String nombre; public String getNombre() { return nombre; } public void setNombre(String nombre) { this.nombre = nombre; } }Ahora podemos usar Identificable y ConNombre para definir nuestra entidades, como sigue:
package org.openxava.test.model; import javax.persistence.*; /** * * @author Javier Paniza */ @Entity @DiscriminatorColumn(name="TYPE") @DiscriminatorValue("HUM") @Table(name="PERSONA") @AttributeOverrides( @AttributeOverride(name="name", column=@Column(name="PNOMBRE")) ) public class Humano extends ConNombre { @Enumerated(EnumType.STRING) private Sexo sexo; public enum Sexo { MASCULINO, FEMENINO }; public Sexo getSexo() { return sexo; } public void setSexo(Sexo sexo) { this.sexo = sexo; } }Y ahora, la auténtica herencia de entidades, una entidad que extiende de otra entidad:
package org.openxava.test.model; import javax.persistence.*; /** * * @author Javier Paniza */ @Entity @DiscriminatorValue("PRO") public class Programador extends Humano { @Column(length=20) private String lenguajePrincipal; public String getLenguajePrincipal() { return lenguajePrincipal; } public void setLenguajePrincipal(String lenguajePrincipal) { this.lenguajePrincipal = lenguajePrincipal; } }Podemo crear un módulo OpenXava para Humano y Programador (no para Identificable ni ConNombre directamente). En el módulo de Programador el usuario puede acceder solo a programadores, por otra parte usando el módulo de Humano el usuario puede acceder a objetos de tipo Humano y Programador. Además cuando el usuario trata de visualizar el detalle de un Programador desde el módulo de Humano se mostrará la vista de Programador. Polimorfismo puro.
package org.openxava.test.model; import javax.persistence.*; import org.openxava.annotations.*; import org.openxava.jpa.*; /** * * @author Javier Paniza */ @Entity @IdClass(AlmacenKey.class) public class Almacen { @Id // Column también se especifica en AlmacenKey por un bug en Hibernate, ver // http://opensource.atlassian.com/projects/hibernate/browse/ANN-361 @Column(length=3, name="ZONA") private int codigoZona; @Id @Column(length=3) private int codigo; @Column(length=40) @Required private String nombre; public String getNombre() { return nombre; } public void setNombre(String nombre) { this.nombre = nombre; } public int getCodigo() { return codigo; } public void setCodigo(int codigo) { this.codigo = codigo; } public int getCodigoZona() { return codigoZona; } public void setCodigoZona(int codigoZona) { this.codigoZona = codigoZona; } }También necesitamos declarar una clase id, una clase serializable normal y corriente con todas las propiedades clave de la entidad:
package org.openxava.test.model; import java.io.*; import javax.persistence.*; /** * * @author Javier Paniza */ public class AlmacenKey implements Serializable { @Column(name="ZONE") private int codigoZona; private int codigo; @Override public boolean equals(Object obj) { if (obj == null) return false; return obj.toString().equals(this.toString()); } @Override public int hashCode() { return toString().hashCode(); } @Override public String toString() { return "AlmacenKey::" + codigoZona + ":" + codigo; } public int getCodigo() { return codigo; } public void setCodigo(int codigo) { this.codigo = codigo; } public int getCodigoZona() { return codigoZona; } public void setCodigoZona(int codigoZona) { this.codigoZona = codigoZona; } }
package org.openxava.test.model; import javax.persistence.*; import org.openxava.annotations.*; /** * * @author Javier Paniza */ @Entity public class Almacen { @EmbeddedId private AlmacenKey clave; @Column(length=40) @Required private String nombre; public AlmacenKey getClave() { return clave; } public void setClave(AlmacenKey clave) { this.clave = clave; } public String getNombre() { return nombre; } public void setNombre(String nombre) { this.nombre = nombre; } }Y nuestra clave es una clase incrustable que contiene las propiedades clave:
package org.openxava.test.model; import javax.persistence.*; /** * * @author Javier Paniza */ @Embeddable public class AlmacenKey implements java.io.Serializable { @Column(length=3, name="ZONA") private int codigoZona; @Column(length=3) private int codigo; public int getCodigo() { return codigo; } public void setCodigo(int codigo) { this.codigo = codigo; } public int getCodigoZona() { return codigoZona; } public void setCodigoZona(int codigoZona) { this.codigoZona = codigoZona; } }
import javax.persistence.*; import org.openxava.annotations.*; import org.openxava.model.* import javax.validation.constraints.*; @Entity public class Conductor extends Identifiable{ @Required @Column(length = 40) private String nombre; @AssertTrue(message = "{no_puede_conducir}") private boolean puedeConducir; //getters y setters... }{no_puede_conducir} es el identificador de mensaje que se encuentra declarado en el archivo i18n así:
no_puede_conducir=Conductor {nombre} no puede ser registrado: debe aprobar el examen de conducirSi intentamos crear una entidad con nombre=MIGUEL GRAU y puedeConducir=false se mostrará el mensaje de error:
import javax.persistence.*; import org.openxava.annotations.*; import org.openxava.model.*; import javax.validation.constraints.*; @Entity public class Vehiculo extends Identifiable{ @Required @Column(length = 15) private String tipo; @Required @Column(length = 7) private String placa; private boolean puedeCircular; @ManyToOne private Conductor conductor; @AssertTrue(message="{no_puede_circular}") private boolean isAptoParaCircular(){ return driver == null || roadworthy; } //getters y setters... }{no_puede_circular} es el identificador de mensaje que se encuentra declarado en el archivo i18n así:
no_puede_circular={tipo} de placa {placa} no es apto para circular. No se puede asignar al conductor {conductor.nombre}Si tenemos la entidad con: tipo=AUTO, placa=A1-0001 y puedeCircular=false; e intentamos asignar conductor (nombre=MIGUEL GRAU), el método de validación fallará y se mostrará el mensaje de error: