Data validation is a common task that occurs in all layers of an application, including persistence. The Java Persistence API (JPA) 2.0 provides support for the Bean Validation API so that data validation can be done at run time. This topic includes a usage scenario where bean validation is used in the JPA environment of a sample digital image gallery application.
The Bean Validation API provides seamless validation across technologies on Java Enterprise Edition 6 (Java EE 6) and Java Platform, Standard Edition (JSE) environments. In addition to JPA 2.0, these technologies include JavaServer Faces (JSF) 2.0 and Java EE Connector Architecture (JCA) 1.6. You can read more about bean validation in the topic, Bean Validation API.
There are three core concepts of bean validation: constraints, constraint violation handling, and the validator. If you are running applications in an integrated environment like JPA, there is no need to interface directly with the validator.
Validation constraints are annotations or XML code that are added to a class, field, or method of a JavaBeans component. Constraints can be built in or user-defined. They are used to define regular constraint definitions and for composing constraints. The built-in constraints are defined by the bean validation specification and are available with every validation provider. For a list of built-in constraints, see the topic, Bean validation built-in constraints. If you need a constraint different from the built-in constraints, you can build your own user-defined constraint.
The following usage scenario illustrates how a built-in constraint is used in the JPA architecture of a sample digital image gallery application.
package org.apache.openjpa.example.gallery.model;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
@Entity
public class Image {
private long id;
private ImageType type;
private String fileName;
private byte[] data;
@Id
@GeneratedValue
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
@NotNull(message="Image type must be specified.")
@Enumerated(EnumType.STRING)
public ImageType getType() {
return type;
}
public void setType(ImageType type) {
this.type = type;
}
@Pattern(regexp = ".*\\.jpg|.*\\.jpeg|.*\\.gif",
message="Only images of type JPEG or GIF are supported.")
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public byte[] getData() {
return data;
}
public void setData(byte[] data) {
this.data = data;
}
}
The Image class uses two built-in constraints, @NotNull and @Pattern. The @NotNull constraint ensures that an ImageType element is specified and the @Pattern constraint uses regular expression pattern matching to ensure that the image file name is suffixed with a supported image format. Each constraint has corresponding validation logic that gets started at run time when the image entity is validated. If either constraint is not met, the JPA provider throws a ConstraintViolationException with the defined message. The JSR-303 specification also makes provisions for the use of a variable within the message attribute. The variable references a keyed message in a resource bundle. The resource bundle supports environment-specific messages and globalization, translation, and multicultural support of messages.
package org.apache.openjpa.example.gallery.constraint;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import javax.validation.Constraint;
import javax.validation.Payload;
import org.apache.openjpa.example.gallery.model.ImageType;
@Documented
@Constraint(validatedBy = ImageContentValidator.class)
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
public @interface ImageContent {
String message() default "Image data is not a supported format.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
ImageType[] value() default { ImageType.GIF, ImageType.JPEG };
}
Next, you must create the validator class, ImageContentValidator.
The logic within this validator gets implemented by the validation
provider when the constraint is validated. The validator class is
bound to the constraint annotation through the validatedBy attribute
on the @Constraint annotation as shown in the following code:package org.apache.openjpa.example.gallery.constraint;
import java.util.Arrays;
import java.util.List;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.apache.openjpa.example.gallery.model.ImageType;
/**
* Simple check that file format is of a supported type
*/
public class ImageContentValidator implements ConstraintValidator<ImageContent, byte[]> {
private List<ImageType> allowedTypes = null;
/**
* Configure the constraint validator based on the image
* types it should support.
* @param constraint the constraint definition
*/
public void initialize(ImageContent constraint) {
allowedTypes = Arrays.asList(constraint.value());
}
/**
*Validate a specified value.
*/
public boolean isValid(byte[] value, ConstraintValidatorContext context) {
if (value == null) {
return false;
}
// Verify the GIF header is either GIF87 or GIF89
if (allowedTypes.contains(ImageType.GIF)) {
String gifHeader = new String(value, 0, 6);
if (value.length >= 6 &&
(gifHeader.equalsIgnoreCase("GIF87a") ||
gifHeader.equalsIgnoreCase("GIF89a"))) {
return true;
}
}
// Verify the JPEG begins with SOI and ends with EOI
if (allowedTypes.contains(ImageType.JPEG)) {
if (value.length >= 4 &&
value[0] == 0xff && value[1] == 0xd8 &&
value[value.length - 2] == 0xff && value[value.length -1] == 0xd9) {
return true;
}
}
// Unknown file format
return false;
}
}
Apply this new constraint to the getData() method on
the Image class; for example:@ImageContent
public byte[] getData() {
return data;
}
When validation of the data attribute occurs, the
isValid() method in the ImageContentValidator is started. This method
contains logic for performing simple validation of the format of the
binary image data. A potentially overlooked feature in the ImageContentValidator
is that it can also validate for a specific image type. By definition,
it accepts JPEG or GIF formats, but it can also validate for a specific
format. For example, by changing the annotation to the following code
example, the validator is instructed to only permit image data with
valid JPEG content:@ImageContent(ImageType.JPEG)
public byte[] getData() {
return data;
}
Type-level constraints are also a consideration
because you might need to validate combinations of attributes on an
entity. In the previous examples validation constraints were used
on individual attributes. Type-level constraints make it possible
to provide collective validation. For example, the constraints applied
to the image entity validate that an image type is set (not null),
the extension on the image file name is of a supported type, and the
data format is correct for the indicated type. But, for example, it
does not collectively validate that a file named img0.gif is
of type GIF and the format of the data is for a valid GIF file image.
For more information about type-level constraints, see the white paper,
OpenJPA Bean Validation Primer, and the section "Type-level constraints."Bean validation uses validation groups to determine what type of validation and when validation occurs.
For more information about validation groups, read the white paper, OpenJPA Bean Validation Primer, and the section "Validation groups."
In addition to the Image entity are Album, Creator and Location persistent types. An Album entity contains a reference to collection of its Image entities. The Creator entity contains a reference to the album entities that the image Creator contributed to and a reference to the Image entities created. This provides full navigational capabilities to and from each of the entities in the domain. An embeddable location, has been added to image to support storing location information with the image.
private Location location;
@Valid
@Embedded
public Location getLocation() {
return location;
}
public void setLocation(Location location) {
this.location = location;
}
The @Valid annotation provides chained validation
of embeddable objects within a JPA environment. Therefore, when image
is validated, any constraints on the location it references are also
validated. If @Valid is not specified, the location is not validated.
In a JPA environment, chained validation through @Valid is only available
for embeddable objects. Referenced entities and collections of entities
are validated separately to prevent circular validation. The JPA 2.0 specification makes integration with the Bean Validation API simple. In a JSE environment, bean validation is enabled by default when you provide the Bean Validation API and a bean validation provider on your runtime class path. In a Java EE 6 environment, the application server includes a bean validation provider so there is no need to bundle one with your application. In both environments, you must use a Version 2.0 persistence.xml file.
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0" >
...
</persistence>
Enables bean validation if a validation provider is available within the class path. Auto is the default.
When callback mode is specified, a bean validation provider must be available for use by the JPA provider. If not, the JPA provider throws an exception upon instantiation of a new JPA entity manager factory.
Disables bean validation for a particular persistence unit.
<persistence-unit name="auto-validation">
...
<!-- Validation modes: AUTO, CALLBACK, NONE -->
<validation-mode>AUTO</validation-mode>
...
</persistence-unit>
The other way is to configure
the validation mode programmatically by specifying the javax.persistence.validation.mode
property with value auto, callback, or none when creating a new JPA
entity manager factory as shown in the following example:Map<String, String> props = new HashMap<String, String>();
props.put("javax.persistence.validation.mode", "callback");
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("validation", props);
<persistence-unit name="non-default-validation-groups">
<class>my.Entity</class>
<validation-mode>CALLBACK</validation-mode>
<properties>
<property name="javax.persistence.validation.group.pre-persist"
value="org.apache.openjpa.example.gallery.constraint.SequencedImageGroup"/>
<property name="javax.persistence.validation.group.pre-update"
value="org.apache.openjpa.example.gallery.constraint.SequencedImageGroup"/>
<property name="javax.persistence.validation.group.pre-remove"
value="javax.validation.groups.Default"/>
</property>
</persistence-unit>
The following example shows
various stages of the JPA life cycle, including persist, update, and
remove:EntityManagerFactory emf =
Persistence.createEntityManagerFactory("BeanValidation");
EntityManager em = emf.createEntityManager();
Location loc = new Location();
loc.setCity("Rochester");
loc.setState("MN");
loc.setZipCode("55901");
loc.setCountry("USA");
// Create an Image with non-matching type and file extension
Image img = new Image();
img.setType(ImageType.JPEG);
img.setFileName("Winter_01.gif");
loadImage(img);
img.setLocation(loc);
// *** PERSIST ***
try {
em.getTransaction().begin();
// Persist the entity with non-matching extension and type
em.persist(img);
} catch (ConstraintViolationException cve) {
// Transaction was marked for rollback, roll it back and
// start a new one
em.getTransaction().rollback();
em.getTransaction().begin();
// Fix the file type and re-try the persist.
img.setType(ImageType.GIF);
em.persist(img);
em.getTransaction().commit();
}
// *** UPDATE ***
try {
em.getTransaction().begin();
// Modify the file name to a non-matching file name
// and commit to trigger an update
img.setFileName("Winter_01.jpg");
em.getTransaction().commit();
} catch (ConstraintViolationException cve) {
// Handle the exception. The commit failed so the transaction
// was already rolled back.
handleConstraintViolation(cve);
}
// The update failure caused img to be detached. It must be merged back
// into the persistence context.
img = em.merge(img);
// *** REMOVE ***
em.getTransaction().begin();
try {
// Remove the type and commit to trigger removal
img.setType(ImageType.GIF);
em.remove(img);
} catch (ConstraintViolationException cve) {
// Rollback the active transaction and handle the exception
em.getTransaction().rollback();
handleConstraintViolation(cve);
}
em.close();
emf.close();
Validation errors can occur in any part of JPA life cycle.
private void handleConstraintViolation(ConstraintViolationException cve) {
Set<ConstraintViolation<?>> cvs = cve.getConstraintViolations();
for (ConstraintViolation<?> cv : cvs) {
System.out.println("------------------------------------------------");
System.out.println("Violation: " + cv.getMessage());
System.out.println("Entity: " + cv.getRootBeanClass().getSimpleName());
// The violation occurred on a leaf bean (embeddable)
if (cv.getLeafBean() != null && cv.getRootBean() != cv.getLeafBean()) {
System.out.println("Embeddable: " +
cv.getLeafBean().getClass().getSimpleName());
}
System.out.println("Attribute: " + cv.getPropertyPath());
System.out.println("Invalid value: " + cv.getInvalidValue());
}
}
Constraint violation processing is typically simple
when using attribute-level constraints. If you are using a type-level
validator with type-level constraints, it can be more difficult to
determine which attribute or combination of attributes failed to validate.
Also, the entire object is returned as the invalid value instead of
an individual attribute. In cases where specific failure information
is required, use of an attribute-level constraint or a custom constraint
violation might be provided as described in the Bean Validation specification. The JPA model and image gallery application usage scenario provided in this topic can be implemented through a sample that is provided in the white paper, OpenJPA Bean Validation primer.