@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"), onlyOnCreate=true ) private String description;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"), onlyOnCreate=true ) }) private String description;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 valueOr 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> details) { this.details = details; } ... }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 INTEGERIn 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<Vehicle> vehicles = new ArrayList<Vehicle>(); //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: