@Entity // 1
@EntityValidator // 2
@RemoveValidator // 3
public class EntityName { // 4
// Properties // 5
// References // 6
// Collections // 7
// Methods // 8
// Finders // 9
// Callback methods // 10
}
@Embeddable // 1
public class EmbeddableName { // 2
// Properties // 3
// References // 4
// Methods // 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 New in v3.1.4
@Calculation // 10 New in v5.7
@DefaultValueCalculator // 11
@PropertyValidator // 12
private type propertyName; // 13
public type getPropertyName() { ... } // 13
public void setPropertyName(type newValue) { ... } // 13
<editor url="personNameEditor.jsp">
<for-stereotype stereotype="PERSON_NAME"/>
<for-annotation annotation="com.yourcompany.yourapp.annotations.PersonName"/> <!-- New in v6.6 -->
</editor>
This way you define the editor to render for editing and
displaying properties of stereotype PERSON_NAME. Note as since v6.6 you
can define an annotation instead of an stereotype, you can also use both.<for-stereotype name="PERSON_NAME" size="40"/>
<for-annotation class="com.yourcompany.yourapp.annotations.PersonName" size="40"/> <!-- New in v6.6 -->
Thus, if you do not put the size in a property of type PERSON_NAME
(or @PersonName) a value of 40 is assumed.<required-validator>
<validator-class class="org.openxava.validators.NotBlankCharacterValidator"/>
<for-stereotype stereotype="PERSON_NAME"/>
<for-annotation annotation="com.yourcompany.yourapp.annotations.PersonName"/> <!-- New in v6.6 -->
</required-validator>
Now everything is ready to define properties of stereotype
PERSON_NAME:@Stereotype("PERSON_NAME")
private String name;
In this case a value of 40 is assumed as size, String as type and
the NotBlankCharacterValidator validator is executed to verify
if it is required.@PersonName
private String name;
@Stereotype("IMAGES_GALLERY")
private String photos;
Furthermore, in the mapping part you have to map your property to a table
column suitable to store a String with a length of 32 characters
(VARCHAR(32)).CREATE TABLE IMAGES (
ID VARCHAR(32) NOT NULL PRIMARY KEY,
GALLERY VARCHAR(32) NOT NULL,
IMAGE BLOB);
CREATE INDEX IMAGES01
ON IMAGES (GALLERY);
The type of IMAGE column can be a more suitable one for your database to
store byte [] (for example 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> <!-- ADD THIS LINE -->
<class>org.openxava.web.editors.DiscussionComment</class>
...
</persistence-unit>
Note that we added <class>org.openxava.session.GalleryImage</class>.<hibernate-configuration>
<session-factory>
...
<mapping resource="GalleryImage.hbm.xml"/>
...
</session-factory>
</hibernate-configuration>
After this you can use the IMAGES_GALLERY stereotype in all components of
your application.@File
@Column(length=32)
private String document;
@Stereotype("FILE")
@Column(length=32)
private String document;
Use @Files (new in v6.6) for attaching multiple
files:@Files
@Column(length=32)
private String documents;
@Stereotype("FILES")
@Column(length=32)
private String documents;
When you use the annotation version (@File or @Files)
you can define attributes like acceptFileTypes or maxFileSizeInKb
to restrict the files the user can upload. For example, with this code:@File(acceptFileTypes="image/*", maxFileSizeInKb=90)
@Column(length=32)
private String photo;
@Files(acceptFileTypes="text/csv, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
@Column(length=32)
private String spreadsheets;
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>
Note that you added <class>org.openxava.web.editors.AttachedFile</class>
both persistence units.CREATE TABLE OXFILES (
ID VARCHAR(32) NOT NULL PRIMARY KEY,
NAME VARCHAR(255),
DATA LONGVARBINARY,
LIBRARYID VARCHAR(32)
);
You should check that the type of DATA column is the most suitable type
for storing byte[] (in our case LONGVARBINARY).Properties annotated with @File, @Files, @HandwrittenSignature, Stereotype("FILE"), @Stereotype("FILES") or @Stereotype("HANDWRITTEN_SIGNATURE") only store a 32-character identifier, not the file content. To access the content of the uploaded file from your own code, you need to use an IFilePersistor obtained from FilePersistorFactory, classes that you can find in the org.openxava.web.editors package. These classes work the same regardless of whether the files are stored in the file system, a database, or any other location.
In the case of @File, @HandwrittenSignature, @Stereotype("FILE") or @Stereotype("HANDWRITTEN_SIGNATURE"), the property directly stores the file ID. For example, if we have a property like this:
@File @Column(length=32)
String photo;
We can fill it with a file using our own code like this:
import java.nio.file.*;
import org.openxava.actions.*;
import org.openxava.web.editors.*;
public class LoadPhotoAction extends ViewBaseAction {
public void execute() throws Exception {
// In this example we get the file from filesystem
// but in your case you could get the file from any other place
String filePath = "/home/me/images/myphoto.png";
byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
// An IFilePersistor to work with the file
IFilePersistor filePersistor = FilePersistorFactory.getInstance();
// We create an AttachedFile object
AttachedFile file = new AttachedFile();
file.setName("myphoto.png");
file.setData(fileBytes);
// This save the file
filePersistor.save(file);
// After saved, the AttachedFile has its id populated
// so we set it to the property in the view
getView().setValue("photo", file.getId());
}
}
You create an AttachedFile and save it with an IFilePersistor, then obtain the file ID to use it as the value for the property. The photo property stores the file ID.
The reverse process, that is, obtaining and manipulating the file already in the @File property, would be like this:
import java.nio.file.*;
import org.openxava.actions.*;
import org.openxava.web.editors.*;
public class SavePhotoAction extends ViewBaseAction {
public void execute() throws Exception {
// An IFilePersistor to work with the file
IFilePersistor filePersistor = FilePersistorFactory.getInstance();
// We get the photo id from the property
String photoId = getView().getValueString("photo");
// And find the AttachedFile with that id using the IFilePersistor
AttachedFile file = filePersistor.find(photoId);
// We get the name and the content from the AttachedFile
String fileName = file.getName();
byte[] fileBytes = file.getData();
// In this example we save the file in the filesystem
// but you could do anything you want with the file
String filePath = "/home/me/images/" + fileName;
Files.write(Paths.get(filePath), fileBytes);
}
}
You search for an AttachedFile using an IFilePersistor based on the photo ID you have in the property.
Working with @Files or @Stereotype(“FILES”) is slightly different because, in this case, the property stores the library ID, not the file ID. A library is a group of files. Each file has its own ID, but they all share a common library ID. For example, with a property like this:
@Files @Column(length=32)
String documents;
We can populate it with multiple files using our own code like this:
import java.nio.file.*;
import org.openxava.actions.*;
import org.openxava.web.editors.*;
public class LoadDocumentsAction
extends GenerateIdForPropertyBaseAction { // To use the generateIdForProperty() method
public void execute() throws Exception {
// In this example we're going to upload some files from the filesystem
// but you could get the files or files content from any other place
String basePath = "/home/me/documents/";
String [] fileNames = {
"limiting-data-by-user.pdf",
"quick-start.odg"
};
// We need generate an id for the library first time or use the one
// that already exists. This work is done for you by generateIdForProperty()
// The generated id is left in the 'documents' property in the view
String libraryId = generateIdForProperty("documents");
// An IFilePersistor to work with the files
IFilePersistor filePersistor = FilePersistorFactory.getInstance();
for (String fileName: fileNames) {
// For our example we get the file content from filesystem
byte[] fileBytes = Files.readAllBytes(Paths.get(basePath + fileName));
// We create an AttachedFile and fill it
AttachedFile file = new AttachedFile();
file.setLibraryId(libraryId); // The same libraryId for all files
file.setName(fileName);
file.setData(fileBytes);
// Then save it using the IFilePersistor
filePersistor.save(file);
}
}
}
The trick is that we need to have a single library ID to assign to each of the files we are going to save. This ID is generated with generateIdForProperty(), which creates it if it doesn’t exist or returns it if it already does. If a new one is generated, it is assigned to the property in the view, so when the entity is saved, it will be saved with the correct library ID. The rest is simply a loop creating AttachedFile and saving them with IFilePersistor, assigning each one the same library ID.
The opposite process, that is, retrieving and manipulating the files that are already in the @Files property, would be like this:
import java.nio.file.*;
import java.util.*;
import org.openxava.actions.*;
import org.openxava.web.editors.*;
public class SaveDocumentsAction extends ViewBaseAction {
public void execute() throws Exception {
// An IFilePersistor to work with the files
IFilePersistor filePersistor = FilePersistorFactory.getInstance();
// For @Files the property stores the library id, not any file id
String libraryId = getView().getValueString("documents");
// We use findLibrary() of IFilePersistor to get a collection of files
Collection<AttachedFile> files =filePersistor.findLibrary(libraryId);
for (AttachedFile file: files) {
// We get the name and content from the AttachedFile
String fileName = file.getName();
byte[] fileBytes = file.getData();
String filePath = "/home/me/documents/" + fileName;
// In our example we save in the filesystem,
// but in your case you can do whatever you want
Files.write(Paths.get(filePath), fileBytes);
}
}
}
Remember that for @Files in the property, the library ID is stored, not the file ID. From this ID, we use the findLibrary() method of IFilePersistor to get all the files associated with that library, i.e., that property. Then we loop through those files and process them as desired.
@Discussion
@Column(length=32)
private String discussion;
@Stereotype("DISCUSSION")
@Column(length=32)
private String discussion;
@PreRemove
private void removeDiscussion() {
DiscussionComment.removeForDiscussion(discussion);
}
Verify that persistence.xml contains the DiscussionComment
entity, if not add it:<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> <!-- ADD THIS LINE -->
...
</persistence-unit>
Note that we added <class>org.openxava.web.editors.DiscussionComment</class>.
When the database is generated, the OXDISCUSSIONCOMMENTS table is created: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);
Check that the type for COMMENT column is the most suitable type for
storing a large text (by default CLOB) in your database, if not just do an
ALTER COLUMN to put a better type.@Coordinates @Column(length=50)
private String location;
# 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
# Change below YOUR_ACCESS_TOKEN by your own access token
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=
"city [ state; "
+ "stateCondition;"
+ "code;"
+ "name;"
+ "population;"
+ "zipCode;"
+ "county;"
+ "country;"
+ "settled;"
+ "area;"
+ "elevation;"
+ "governmentType;"
+ "mayor;"
+ "], "
+ "location")

To allow the user to sign by hand and save their signature in a property, you have to annotate the property with @HandwrittenSignature or @Stereotype("HANDWRITTEN_SIGNATURE"):
import com.openxava.annotations.*; // Not org.openxava.annotations.*
...
@HandwrittenSignature
@Column(length=32)
private String customerSignature;
Note how the annotation is in the package com.openxava.annotations (from XavaPro) and not in org.openxava.annotations.
The property would be displayed like this:
The user will be able to sign with their finger or a stylus for touch screens, especially designed for mobile phones and tablets, although it is also possible to sign using the mouse.
The property type is a String of 32 characters where an id is stored, not the signature itself. Signatures can be stored in the file system or in the database, using the same mechanism (the same filePersistorClass) as @File and @Files. It is also possible to manipulate signatures programmatically with FilePersistorFactory as with @File and @Files.
@Mask("L-000000")
private String passport;
@Mask("0000 0000 0000 0000")
private String creditCard;
@Mask("LL 000 AA")
private String carPlate;
@Mask("0.000/0-000")
private String customMask;
@Version
private Integer version;
This property is persistence engine use, your application or your user
must not use this property directly. If you don't use automatic schema
evolution remember to add the column VERSION to the table.private Distance distance;
public enum Distance { LOCAL, NATIONAL, INTERNATIONAL };
The distance property only can have the following values: LOCAL,
NATIONAL or INTERNATIONAL, and as @Required is not specified, no
value (null) is allowed too. Since v5.3 if @Required is
specified, the first option is the default value and an empty value is not
available. If a different default value is required, use @DefaultValueCalculator.
Since v5.6.1 the enums annotated with @Required in an embeddable class will
display empty value if it is used in a collection of elements.public enum Priority implements IIconEnum {
LOW("transfer-down"), MEDIUM("square-medium"), HIGH("transfer-up");
private String icon;
private Priority(String icon) {
this.icon = icon;
}
public String getIcon() {
return icon;
}
};
private Priority priority;
Just make your enum to implement IIconEnum that forces you
to have a getIcon() method. This method has to return an icon id
from Material Design Icons.
OpenXava can use these icons in several parts of the UI, for example in
list:
@Depends("unitPrice") // 1
@Max(9999999999L) // 2
public BigDecimal getUnitPriceInPesetas() {
if (unitPrice == null) return null;
return unitPrice.multiply(new BigDecimal("166.386")).setScale(0, BigDecimal.ROUND_HALF_UP);
}
According to the above definition now you can use the code in this way:Product product = ...
product.setUnitPrice(2);
BigDecimal result = product.getUnitPriceInPesetas();
And result will hold 332.772.@Max(999)
public int getDetailsCount() {
// An example of using JDBC
Connection con = null;
try {
con = DataSourceConnectionProvider.getByComponent("Invoice").getConnection(); // 1
String table = MetaModel.get("InvoiceDetail").getMapping().getTable();
PreparedStatement ps = con.prepareStatement("select count(*) from " + table +
" where INVOICE_YEAR = ? and INVOICE_NUMBER = ?");
ps.setInt(1, getYear());
ps.setInt(2, getNumber());
ResultSet rs = ps.executeQuery();
rs.next();
Integer result = new Integer(rs.getInt(1));
ps.close();
return result;
}
catch (Exception ex) {
log.error("Problem calculating details count of an Invoice", ex);
// You can throw any runtime exception here
throw new SystemException(ex);
}
finally {
try {
con.close();
}
catch (Exception ex) {
}
}
}
Yes, the JDBC code is ugly and awkward, but sometimes it can help to solve
performance problems. The DataSourceConnectionProvider class
allows you to obtain a connection associated to the same data source that
the indicated entity (Invoice in this case). This class is for
your convenience, but you can access to a JDBC connection using JNDI or
any other way you want. In fact, in a calculated property you can write
any code that Java allows you.private long number;
@Id @Column(length=10) // You annotated the getter,
public long getNumber() { // so JPA will use property-base access for your class
return number;
}
public void setNumber(long number) {
this.number = number;
}
@Transient // You have to annotate as Transient your calculated property
public String getZoneOne() { // because you are using property-based access
return "In ZONE 1";
}
@org.hibernate.annotations.Formula("UNITPRICE * 1.16")
private BigDecimal unitPriceWithTax;
public BigDecimal getUnitPriceWithTax() {
return unitPriceWithTax;
}
The use is simple. Put your calculation exactly in the same way that you
would put it in a SQL statement.@DefaultValueCalculator(CurrentYearCalculator.class)
private int year;
In this case when the user tries to create a new Invoice (for example) he
will find that the year field already has a value, that he can change it
if he wants to. The logic for generating this value is in the CurrentYearCalculator
class, that it's: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));
}
}
It's possible to customize the behaviour of a calculator setting the value
of its properties, as following:@DefaultValueCalculator(
value=org.openxava.calculators.StringCalculator.class,
properties={ @PropertyValue(name="string", value="GOOD") }
)
private String relationWithSeller;
In this case for calculating the default value OpenXava instances StringCalculator
and then injects the value "GOOD" in the property string of StringCalculator,
and finally it calls to the calculate() method in order to
obtain the default value for relationWithSeller. As you see, the
use of @PropertyValue annotation allows you to create
reusable calculators.@DefaultValueCalculator(
value=org.openxava.test.calculators.CarrierRemarksCalculator.class,
properties={
@PropertyValue(name="drivingLicenceType", from="drivingLicence.type")
}
)
private String remarks;
In this case before to execute the calculator OpenXava fills the drivingLicenceType
property of CarrierRemarksCalculator with the value of the
displayed property type from the reference drivingLicence.
As you see the from attribute supports qualified properties
(reference.property). Moreover, each time that drivingLicence.type
changes remarks is recalculated (new in v5.1, with
previous versions it was recalculated only the first time).@DefaultValueCalculator(value=DefaultProductPriceCalculator.class, properties=
@PropertyValue(name="familyNumber")
)
In this case OpenXava takes the value of the displayed property familyNumber
and inject it in the property familyNumber of the calculator;
that is @PropertyValue(name="familiyNumber") is equivalent to @PropertyValue(name="familiyNumber",
from="familyNumber").@DefaultValueCalculator(value=DetailsCountCalculator.class,
properties= {
@PropertyValue(name="year"),
@PropertyValue(name="number"),
}
)
private int detailsCount;
And the calculator class:package org.openxava.test.calculators;
import java.sql.*;
import org.openxava.calculators.*;
import org.openxava.util.*;
/**
* @author Javier Paniza
*/
public class DetailsCountCalculator implements IJDBCCalculator { // 1
private IConnectionProvider provider;
private int year;
private int number;
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.INVOICEDETAIL " +
"where INVOICE_YEAR = ? and INVOICE_NUMBER = ?");
ps.setInt(1, getYear());
ps.setInt(2, getNumber());
ResultSet rs = ps.executeQuery();
rs.next();
Integer result = new Integer(rs.getInt(1));
ps.close();
return result;
}
finally {
con.close();
}
}
public int getYear() {
return year;
}
public int getNumber() {
return number;
}
public void setYear(int year) {
this.year = year;
}
public void setNumber(int number) {
this.number = number;
}
}
To use JDBC your calculator must implement IJDBCCalculator (1)
and then it will receive an IConnectionProvider (2) that you can
use within the calculate() method.@Id @Hidden
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
You can use other generation techniques, for example, a database sequence
can be defined in this JPA standard way:@SequenceGenerator(name="SIZE_SEQ", sequenceName="SIZE_ID_SEQ", allocationSize=1 )
@Hidden @Id @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SIZE_SEQ")
private Integer id;
If you want to generate a unique identifier of type String and 32
characters, you can use a Hibernate extesion of JPA:@Id @GeneratedValue(generator="system-uuid") @Hidden
@GenericGenerator(name="system-uuid", strategy = "uuid")
private String oid;
Look at section 9.1.9 of JPA 1.0 specification (part of JSR-220) for
learning more about @GeneratedValues.@PrePersist
private void calculateCounter() {
counter = new Long(System.currentTimeMillis()).intValue();
}
The JPA @PrePersist annotation does that this method will be
executed before inserting the data the first time in database, in this
method you can calculate the value for your key or non-key properties with
your own logic.@PropertyValidator(value=ExcludeStringValidator.class, properties=
@PropertyValue(name="string", value="MOTO")
)
@PropertyValidator(value=ExcludeStringValidator.class, properties=
@PropertyValue(name="string", value="COCHE")
)
private String remarks;
With an OpenXava older than 6.1 you have to wrap the @PropertyValidator
annotations with @PropertyValidators:@PropertyValidators ({ // Only needed until v6.0.2
@PropertyValidator(value=ExcludeStringValidator.class, properties=
@PropertyValue(name="string", value="MOTO")
),
@PropertyValidator(value=ExcludeStringValidator.class, properties=
@PropertyValue(name="string", value="COCHE")
)
})
private String remarks;
The technique to configure the validator (with @PropertyValue, though from
attribute does not work, you must use value always) is exactly
the same than in calculators.
With the attribute onlyOnCreate=”true” you can define that the
validation will be executed only when the object is created, and not when
it is modified.package org.openxava.test.validators;
import org.openxava.util.*;
import org.openxava.validators.*;
/**
* @author Javier Paniza
*/
public class ExcludeStringValidator implements IPropertyValidator { // 1
private String string;
public void validate(
Messages errors, // 2
Object value, // 3
String objectName, // 4
String propertyName) // 5
throws Exception {
if (value==null) return;
if (value.toString().indexOf(getString()) >= 0) {
errors.add("exclude_string", propertyName, objectName, getString());
}
}
public String getString() {
return string==null?"":string;
}
public void setString(String string) {
this.string = string;
}
}
A validator has to implement IPropertyValidator (1), this
obliges the calculator to have a validate() method where the
validation of property is executed. The arguments of validate()
method are:exclude_string={0} cannot contain {2} in {1}
If the identifier sent is not found in the resource file, this identifier
is shown as is; but the recommended way is always to use identifiers of
resource files.@PropertyValidator(value=BookTitleValidator.class, message="{rpg_book_not_allowed}")
private String title;
If the message is between braces is get from i18n files, if not is used as
is.public class BookTitleValidator implements IPropertyValidator, IWithMessage {
private String message;
public void setMessage(String message) throws Exception {
this.message = message; // This message is from @PropertyValidator
}
public void validate(Messages errors, Object value, String propertyName, String modelName) {
if (((String)value).contains("RPG")) {
errors.add(message); // You can add it directly
}
}
}
The message specified in the @PropertyValidator annotation, rpg_book_not_allowed,
is injected in the validator calling setMessage(). This message
can be added directly as an error.<validators>
<default-validator>
<validator-class
class="org.openxava.test.validators.PersonNameValidator"/>
<for-stereotype stereotype="PERSON_NAME"/>
</default-validator>
</validators>
In this case you are associating the validator PersonNameValidator
to the stereotype PERSON_NAME. Now if you define a property as the next
one:@Required @Stereotype("PERSON_NAME")
private String name;
This property will be validated using PersonNameValidator
although the property itself does not define any validator. PersonNameValidator
is applied to all properties with PERSON_NAME stereotype.@Calculation("((hours * worker.hourPrice) + tripCost - discount) * vatPercentage / 100")
private BigDecimal total;
Note as worker.hourPrice is used to get the value from the
reference.Customer customer = ...
customer.getSeller().getName();
to access to the name of the seller of that customer.@Required // 1
@Id // 2
@SearchKey // 3 New in v3.0.2
@DefaultValueCalculator // 4
@ManyToOne( // 5
optional=false // 1
)
private type referenceName; // 5
public type getReferenceName() { ... } // 5
public void setReferenceName(type newValue) { ... } // 5
@ManyToOne
private Seller seller; // 1
public Seller getSeller() {
return seller;
}
public void setSeller(Seller seller) {
this.seller = seller;
}
@ManyToOne(fetch=FetchType.LAZY)
private Seller alternateSeller; // 2
public Seller getAlternateSeller() {
return alternateSeller;
}
public void setAlternateSeller(Seller alternateSeller) {
this.alternateSeller = alternateSeller;
}
Customer customer = ...
Seller seller = customer.getSeller();
Seller alternateSeller = customer.getAlternateSeller();
@ManyToOne(optional=false, fetch=FetchType.LAZY) @JoinColumn(name="FAMILY")
@DefaultValueCalculator(value=IntegerCalculator.class, properties=
@PropertyValue(name="value", value="2")
)
private Family family;
The calculate() method is:public Object calculate() throws Exception {
return new Integer(value);
}
As you can see an integer is returned, that is, the default value for
family is 2.@ManyToOne(fetch=FetchType.LAZY)
@JoinColumns({
@JoinColumn(name="ZONE", referencedColumnName="ZONE"),
@JoinColumn(name="WAREHOUSE", referencedColumnName="NUMBER")
})
@DefaultValueCalculator(DefaultWarehouseCalculator.class)
private Warehouse warehouse;
And the calculator code:package org.openxava.test.calculators;
import org.openxava.calculators.*;
/**
* @author Javier Paniza
*/
public class DefaultWarehouseCalculator implements ICalculator {
public Object calculate() throws Exception {
Warehouse key = new Warehouse();
key.setNumber(4);
key.setZoneNumber(4);
return key;
}
}
Returns an object of type Warehouse but filling only the key
properties.@Entity
@IdClass(AdditionalDetailKey.class)
public class AdditionalDetail {
// JoinColumn is also specified in AditionalDetailKey because
// a bug in Hibernate, see http://opensource.atlassian.com/projects/hibernate/browse/ANN-361
@Id @ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="SERVICE")
private Service service;
@Id @Hidden
private int counter;
...
}
Also, you need to write your key class:public class AdditionalDetailKey implements java.io.Serializable {
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="SERVICE")
private Service service;
@Hidden
private int counter;
// equals, hashCode, toString, getters and setters
...
}
You need to write the key class although the key would be only a reference
with only a join column.@Embedded
private Address address;
And you have to define the Address class as embeddable:package org.openxava.test.model;
import javax.persistence.*;
import org.openxava.annotations.*;
/**
*
* @author Javier Paniza
*/
@Embeddable
public class Address implements IWithCity { // 1
@Required @Column(length=30)
private String street;
@Required @Column(length=5)
private int zipCode;
@Required @Column(length=20)
private String city;
// ManyToOne inside an Embeddable is not supported by JPA 1.0 (see at 9.1.34),
// but Hibernate implementation supports it.
@ManyToOne(fetch=FetchType.LAZY, optional=false) @JoinColumn(name="STATE")
private State state; // 2
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public int getZipCode() {
return zipCode;
}
public void setZipCode(int zipCode) {
this.zipCode = zipCode;
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
}
As you see an embeddable class can implement an interface (1) and contain
references (2), among other things, but it can't use JPA callbacks
methods.Customer customer = ...
Address address = customer.getAddress();
address.getStreet(); // to obtain the value
Or in this other way to set a new address:// to set a new address
Address address = new Address();
address.setStreet("My street");
address.setZipCode(46001);
address.setCity("Valencia");
address.setState(state);
customer.setAddress(address);
In this case you have a simple reference (not collection), and the
generated code is a simple JavaBean, which life cycle is associated to its
container object, that is, the Address is removed and created
through the Customer. An Address never will have its
own life and cannot be shared by other Customer.@Size // 1
@Condition // 2
@OrderBy // 3
@XOrderBy // 4
@OrderColumn // 5 New in v5.3
@OneToMany/@ManyToMany // 6
private Collection<YourEntity> collectionName; // 5
public Collection<YourEntity> getCollectionName() { ... } // 5
public void setCollectionName(Collection<YourEntity> newValue) { ... } // 5
@OneToMany (mappedBy="invoice")
private Collection<Delivery> deliveries;
public Collection<Delivery> getDeliveries() {
return deliveries;
}
public void setDeliveries(Collection<Delivery> deliveries) {
this.deliveries = deliveries;
}
If you have this within an Invoice, then you are defining a deliveries
collection associated to that Invoice. The details to make the
relationship are defined in the object/relational
mapping.You use mappedBy="invoice" to indicate that the
reference invoice of Delivery is used to mapping this
collection.Invoice invoice = ...
for (Delivery delivery: invoice.getDeliveries()) {
delivery.doSomething();
}
To do something with all deliveries associated to an invoice.@OneToMany(mappedBy="seller")
private Collection<Customer> customers;
To indicate that the reference seller and not alternateSeller
will be used in this collection.@OneToMany (mappedBy="invoice", cascade=CascadeType.REMOVE) // 1
@OrderBy("serviceType desc") // 2
@org.hibernate.validator.Size(min=1) // 3
private Collection<InvoiceDetail> details;
@Condition(
"${warehouse.zoneNumber} = ${this.warehouse.zoneNumber} AND " +
"${warehouse.number} = ${this.warehouse.number} AND " +
"NOT (${number} = ${this.number})"
)
public Collection<Carrier> getFellowCarriers() {
return null;
}
If you have this collection within Carrier, you can obtain with
this collection all the carriers of the same warehouse but not himself,
that is the list of his fellow workers. As you see you can use this
in the condition in order to refer to the value of a property of the
current object. @Condition only applied to the user interface
generated by OpenXava, if you call directly to getFellowCarriers()
it will return null. The condition is absolute, meaning that if you set @Condition("1
= 1"), it would display all the carriers in the database.public Collection<Carrier> getFellowCarriers() {
Query query = XPersistence.getManager().createQuery("from Carrier c where " +
"c.warehouse.zoneNumber = :zone AND " +
"c.warehouse.number = :warehouseNumber AND " +
"NOT (c.number = :number) ");
query.setParameter("zone", getWarehouse().getZoneNumber());
query.setParameter("warehouseNumber", getWarehouse().getNumber());
query.setParameter("number", getNumber());
return query.getResultList();
}
As you see this is a conventional getter method. Obviously it must return
a java.util.Collection whose elements are of type Carrier.
You don't need to define the field, the setter, or use @OneToMany
or @ManyToMany. Only the getter is required.@Entity
public class Customer {
...
@ManyToMany
private Collection<State> states;
...
}
In this case a customer has a collection of states, but a state can be
present in several customers.@OneToMany (mappedBy="invoice", cascade=CascadeType.REMOVE)
private Collection details;
Note that you use CascadeType.REMOVE, and InvoiceDetail
is an entity, not an embeddable class: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=InvoiceDetailValidator.class,
properties= {
@PropertyValue(name="invoice"),
@PropertyValue(name="oid"),
@PropertyValue(name="product"),
@PropertyValue(name="unitPrice")
}
)
public class InvoiceDetail {
@ManyToOne // Lazy fetching produces a fails on removing a detail from invoice
private Invoice invoice;
@Id @GeneratedValue(generator="system-uuid") @Hidden
@GenericGenerator(name="system-uuid", strategy = "uuid")
private String oid;
private ServiceType serviceType;
public enum ServiceType { SPECIAL, URGENT }
@Column(length=4) @Required
private int quantity;
@Stereotype("MONEY") @Required
private BigDecimal unitPrice;
@ManyToOne(fetch=FetchType.LAZY, optional=false)
private Product product;
@DefaultValueCalculator(CurrentDateCalculator.class)
private java.util.Date deliveryDate;
@ManyToOne(fetch=FetchType.LAZY)
private Seller soldBy;
@Stereotype("MEMO")
private String remarks;
@Stereotype("MONEY") @Depends("unitPrice, quantity")
public BigDecimal getAmount() {
return getUnitPrice().multiply(new BigDecimal(getQuantity()));
}
public boolean isFree() {
return getAmount().compareTo(new BigDecimal("0")) <= 0;
}
@PostRemove
private void postRemove() {
invoice.setComment(invoice.getComment() + "DETAIL DELETED");
}
public String getOid() {
return oid;
}
public void setOid(String oid) {
this.oid = oid;
}
public ServiceType getServiceType() {
return serviceType;
}
public void setServiceType(ServiceType serviceType) {
this.serviceType = serviceType;
}
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public BigDecimal getUnitPrice() {
return unitPrice==null?BigDecimal.ZERO:unitPrice;
}
public void setUnitPrice(BigDecimal unitPrice) {
this.unitPrice = unitPrice;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
public java.util.Date getDeliveryDate() {
return deliveryDate;
}
public void setDeliveryDate(java.util.Date deliveryDate) {
this.deliveryDate = deliveryDate;
}
public Seller getSoldBy() {
return soldBy;
}
public void setSoldBy(Seller soldBy) {
this.soldBy = soldBy;
}
public String getRemarks() {
return remarks;
}
public void setRemarks(String remarks) {
this.remarks = remarks;
}
public Invoice getInvoice() {
return invoice;
}
public void setInvoice(Invoice invoice) {
this.invoice = invoice;
}
}
As you see this is a complex entity, with calculators, validators,
references and so on. Also you have to define a reference to the container
class (invoice). In this case when an Invoice is
removed all its details are removed too. Moreover there are differences at
user interface level (you can learn more on the view
chapter).@Size // 1
@OrderBy // 2
@OrderColumn // 3 New in v5.3
@ElementCollection // 4
private Collection<YourEmbeddableClass> collectionName; // 3
public Collection<YourEmbeddableClass> getCollectionName() { ... } // 3
public void setCollectionName(Collection<YourEmbeddableClass> newValue) { ... } // 3
@Entity
public class Quote extends Identifiable {
...
@ElementCollection
private Collection<QuoteDetail> details;
public Collection<QuoteDetail> getDetails() {
return details;
}
public void setDetails(Collection<QuoteDetail> newValue) {
this.details = newValue;
}
...
}
Then define your embeddable class:@Embeddable
public class QuoteDetail {
...
@ManyToOne(fetch=FetchType.LAZY, optional=false) // 1
private Product product;
@Required // 2
private BigDecimal unitPrice;
@Required
private int quantity;
private Date availabilityDate;
@Column(length=30)
private String remarks;
@Column(precision=10, scale=2)
@Depends("unitPrice, quantity")
public BigDecimal getAmount() { // 3
return getUnitPrice().multiply(new BigDecimal(getQuantity()));
}
...
}
As you can see, an embeddable class used in an element collection can
contain references(1), validations(2) and calculated properties(3) among
other things.@OneToMany(mappedBy="project", cascade=CascadeType.ALL)
@OrderColumn
private List<ProjectTask> tasks;
The user interface allows the user to change the order of the elements and
this order is persisted in database. Moreover, if you change the order of
the elements programmatically this order is persisted in database too.ALTER TABLE PROJECTTASK
ADD TASKS_ORDER INTEGER
In the current implementation drag and drop is used by the user to change
the order, with @OneToMany collections the order is persisted
just after drop, while in @ElementCollection the order is
persisted after saving the container entity.public void increasePrice() {
setUnitPrice(getUnitPrice().multiply(new BigDecimal("1.02")).setScale(2));
}
Methods are the sauce of the objects, without them the object would only
be a silly wrapper of data. When possible it is better to put the business
logic in methods (model layer) instead of in actions (controller layer).public static Customer findByNumber(int number) throws NoResultException {
Query query = XPersistence.getManager().createQuery(
"from Customer as o where o.number = :number");
query.setParameter("number", number);
return (Customer) query.getSingleResult();
}
public static Collection findAll() {
Query query = XPersistence.getManager().createQuery("from Customer as o");
return query.getResultList();
}
public static Collection findByNameLike(String name) {
Query query = XPersistence.getManager().createQuery(
"from Customer as o where o.name like :name order by o.name desc");
query.setParameter("name", name);
return query.getResultList();
}
This methods can be used this way:Customer customer = Customer.findByNumber(8);
Collection javieres = Customer.findByNameLike("%JAVI%");
As you see, using finder methods creates a more readable code than using
the verbose query API of JPA. But this is only a style recommendation, you
can choose not to write finder methods and to use directly JPA queries.@EntityValidator(
value=class, // 1
onlyOnCreate=(true|false), // 2
properties={ @PropertyValue ... } // 3
)
@EntityValidator(value=org.openxava.test.validators.CheapProductValidator.class, properties= {
@PropertyValue(name="limit", value="100"),
@PropertyValue(name="description"),
@PropertyValue(name="unitPrice")
})
public class Product {
And the validator code:package org.openxava.test.validators;
import java.math.*;
/**
* @author Javier Paniza
*/
public class CheapProductValidator implements IValidator { // 1
private int limit;
private BigDecimal unitPrice;
private String description;
public void validate(Messages errors) { // 2
if (getDescription().indexOf("CHEAP") >= 0 ||
getDescription().indexOf("BARATO") >= 0 ||
getDescription().indexOf("BARATA") >= 0) {
if (getLimiteBd().compareTo(getUnitPrice()) < 0) {
errors.add("cheap_product", getLimitBd()); // 3
}
}
}
public BigDecimal getUnitPrice() {
return unitPrice;
}
public void setUnitPrice(BigDecimal decimal) {
unitPrice = decimal;
}
public String getDescription() {
return description==null?"":description;
}
public void setDescription(String string) {
description = string;
}
public int getLimit() {
return limit;
}
public void setLimit(int i) {
limit = i;
}
private BigDecimal getLimitBd() {
return new BigDecimal(Integer.toString(limit));
}
}
This validator must implement IValidator (1), this forces you to
write a validate(Messages messages) (2). In this method you add
the error message ids (3) (whose texts are in the i18n files). And if the
validation process (that is the execution of all validators) produces some
error, then OpenXava does not save the object and displays the errors to
the user.@EntityValidator(value=org.openxava.test.validators.CheapProductValidator.class, properties= {
@PropertyValue(name="limit", value="100"),
@PropertyValue(name="description"),
@PropertyValue(name="unitPrice")
})
@EntityValidator(value=org.openxava.test.validators.ExpensiveProductValidator.class, properties= {
@PropertyValue(name="limit", value="1000"),
@PropertyValue(name="description"),
@PropertyValue(name="unitPrice")
})
@EntityValidator(value=org.openxava.test.validators.ForbiddenPriceValidator.class,
properties= {
@PropertyValue(name="forbiddenPrice", value="555"),
@PropertyValue(name="unitPrice")
},
onlyOnCreate=true
)
public class Product {
With OpenXava previous to 6.1 you need to use @EntityValidators
to define several validators:@EntityValidators({ // Only needed until v6.0.2
@EntityValidator(value=org.openxava.test.validators.CheapProductValidator.class, properties= {
@PropertyValue(name="limit", value="100"),
@PropertyValue(name="description"),
@PropertyValue(name="unitPrice")
}),
@EntityValidator(value=org.openxava.test.validators.ExpensiveProductValidator.class, properties= {
@PropertyValue(name="limit", value="1000"),
@PropertyValue(name="description"),
@PropertyValue(name="unitPrice")
}),
@EntityValidator(value=org.openxava.test.validators.ForbiddenPriceValidator.class,
properties= {
@PropertyValue(name="forbiddenPrice", value="555"),
@PropertyValue(name="unitPrice")
},
onlyOnCreate=true
)
})
public class Product {
@EntityValidator is defined as a Bean
Validation constraint since v5.3 and as a Hibernate
Validator constraint until v5.2.x.@RemoveValidator(
value=class, // 1
properties={ @PropertyValue ... } // 2
)
@RemoveValidator(value=DeliveryTypeRemoveValidator.class,
properties=@PropertyValue(name="number")
)
public class DeliveryType {
And the validator:package org.openxava.test.validators;
import org.openxava.test.model.*;
import org.openxava.util.*;
import org.openxava.validators.*;
/**
* @author Javier Paniza
*/
public class DeliveryTypeRemoveValidator implements IRemoveValidator { // 1
private DeliveryType deliveryType;
private int number; // We use this (instead of obtaining it from deliveryType)
// for testing @PropertyValue for simple properties
public void setEntity(Object entity) throws Exception { // 2
this.deliveryType = (DeliveryType) entity;
}
public void validate(Messages errors) throws Exception {
if (!deliveryType.getDeliveries().isEmpty()) {
errors.add("not_remove_delivery_type_if_in_deliveries", new Integer(getNumber())); // 3
}
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
As you see this validator must implement IRemoveValidator (1)
this forces you to write a setEntity() (2) method that receives
the object to remove. If validation error is added to the Messages
object sent to validate() (3) the validation fails. If after
executing all validations there are validation errors, then OpenXava does
not remove the object and displays a list of validation messages to the
user.@PrePersist
private void prePersist() {
setDescription(getDescription() + " CREATED");
}
In this case each time that a DeliveryType is created a suffix
to description is added.@PreUpdate
private void preUpdate() {
setDescription(getDescription() + " MODIFIED");
}
In this case whenever that a DeliveryType is modified a suffix
is added to its description.@PreCreate
public void onPreCreate() {
// Automatically create a new customer
if (getCustomer() == null) {
Customer cust = new Customer();
cust.setName(getName());
cust.setAddress(getAddress());
cust = XPersistence.getManager().merge(cust);
setCustomer(cust);
}
}
The entity manager operation will not affect the callbacks behavior. Along
with @PreCreate is @PostCreate and @PreDelete the methods decorated with
these annotations are part of the transaction, therefore the use of these
annotations keeps the integrity of the information without further effort
from the developer. When combined with JPA annotations the order for each
callback is as follows:package org.openxava.test.model;
import javax.persistence.*;
import org.hibernate.annotations.*;
import org.openxava.annotations.*;
/**
* Base class for defining entities with a UUID oid. <p>
*
* @author Javier Paniza
*/
@MappedSuperclass
public class Identifiable {
@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;
}
}
You can define another @MappedSuperclass that extends from this
one, for example:package org.openxava.test.model;
import javax.persistence.*;
import org.openxava.annotations.*;
/**
* Base class for entities with a 'name' property. <p>
*
* @author Javier Paniza
*/
@MappedSuperclass
public class Nameable extends Identifiable {
@Column(length=50) @Required
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Now you can use Identifiable or Nameable for defining
your entities, as following:package org.openxava.test.model;
import javax.persistence.*;
/**
*
* @author Javier Paniza
*/
@Entity
@DiscriminatorColumn(name="TYPE")
@DiscriminatorValue("HUM")
@Table(name="PERSON")
@AttributeOverrides(
@AttributeOverride(name="name", column=@Column(name="PNAME"))
)
public class Human extends Nameable {
@Enumerated(EnumType.STRING)
private Sex sex;
public enum Sex { MALE, FEMALE };
public Sex getSex() {
return sex;
}
public void setSex(Sex sex) {
this.sex = sex;
}
}
And now, the real entity inheritance, an entity that extends other entity:package org.openxava.test.model;
import javax.persistence.*;
/**
*
* @author Javier Paniza
*/
@Entity
@DiscriminatorValue("PRO")
public class Programmer extends Human {
@Column(length=20)
private String mainLanguage;
public String getMainLanguage() {
return mainLanguage;
}
public void setMainLanguage(String mainLanguage) {
this.mainLanguage = mainLanguage;
}
}
You can create an OpenXava
module for Human and Programmer (not for Identifiable
or Nameble directly). In the Programmer module the
user can only access to programmers, in the other hand using Human
module the user can access to Human and Programmer
objects. Moreover when the user tries to view the detail of a Programmer
from the Human module the Programmer view will be
show. True polymorphism.package org.openxava.test.model;
import javax.persistence.*;
import org.openxava.annotations.*;
import org.openxava.jpa.*;
/**
*
* @author Javier Paniza
*/
@Entity
@IdClass(WarehouseKey.class)
public class Warehouse {
@Id
// Column is also specified in WarehouseKey because a bug in Hibernate, see
// http://opensource.atlassian.com/projects/hibernate/browse/ANN-361
@Column(length=3, name="ZONE")
private int zoneNumber;
@Id @Column(length=3)
private int number;
@Column(length=40) @Required
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public int getZoneNumber() {
return zoneNumber;
}
public void setZoneNumber(int zoneNumber) {
this.zoneNumber = zoneNumber;
}
}
You also need to declare your id class, a serializable regular class with
all key properties from the entity:package org.openxava.test.model;
import java.io.*;
import javax.persistence.*;
/**
*
* @author Javier Paniza
*/
public class WarehouseKey implements Serializable {
@Column(name="ZONE")
private int zoneNumber;
private int number;
@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 "WarehouseKey::" + zoneNumber+ ":" + number;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public int getZoneNumber() {
return zoneNumber;
}
public void setZoneNumber(int zoneNumber) {
this.zoneNumber = zoneNumber;
}
}
package org.openxava.test.model;
import javax.persistence.*;
import org.openxava.annotations.*;
/**
*
* @author Javier Paniza
*/
@Entity
public class Warehouse {
@EmbeddedId
private WarehouseKey key;
@Column(length=40) @Required
private String name;
public WarehouseKey getKey() {
return key;
}
public void setKey(WarehouseKey key) {
this.key = key;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
And you key is an embeddable class that holds the key properties:package org.openxava.test.model;
import javax.persistence.*;
/**
*
* @author Javier Paniza
*/
@Embeddable
public class WarehouseKey implements java.io.Serializable {
@Column(length=3, name="ZONE")
private int zoneNumber;
@Column(length=3)
private int number;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public int getZoneNumber() {
return zoneNumber;
}
public void setZoneNumber(int zoneNumber) {
this.zoneNumber = zoneNumber;
}
}
import javax.persistence.*;
import org.openxava.annotations.*;
import org.openxava.model.*
import javax.validation.constraints.*;
@Entity
public class Driver extends Identifiable{
@Required
@Column(length=40)
private String name;
@AssertTrue(message="{disapproved_driving_test}")
private boolean approvedDrivingTest;
@OneToMany(mappedBy="driver")
private Collection vehicles = new ArrayList();
//getters and setters...
}
{disapproved_driving_test} is the message identifier that is
declared under i18n file like this:disapproved_driving_test=Driver {name} can not be registered: must approve the driving test
If we try to create an entity with name=MIGUEL GRAU and approvedDrivingTest=false
the next error message will be shown:import javax.persistence.*;
import org.openxava.annotations.*;
import org.openxava.model.*;
import javax.validation.constraints.*;
@Entity
public class Vehicle extends Identifiable{
@Required
@Column(length=15)
private String type;
@Required
@Column(length=7)
private String licensePlate;
private boolean roadworthy;
@ManyToOne
private Driver driver;
@AssertTrue(message="{not_roadworthy}")
private boolean isRoadworthyToAssignTheDriver(){
return driver == null || roadworthy;
}
//getters and setters...
}
{not_roadworthy} is the message identifier that is declared under
i18n file like this:not_roadworthy={type} plate {licensePlate} is not roadworthy. It can not be assigned to the driver {driver.name}
If we have an entity: type=AUTO, licensePlate=A1-0001
and roadworthy=false, and try to assign driver (name =
MIGUEL GRAU), the validation method will fail and display the error
message: