Table Of Contents

  1. Introduction
  2. Getting Started
  3. Architecture
  4. Annotations
  5. Customization
  6. Constrained Map

Introduction

This validation framework does not implement the JSR-303 specification. The main features of this implementation are:

In next chapters you will find detailed information on all of the above features.

Getting Started

In this chapter, we consider a simple example that will give basic knowledge on the use of this framework. For this you need JDK 1.6 or higher and Apache Maven.

Maven Setup

In the pom.xml file you need to add the following dependency:

<dependencies>
    <dependency>
        <groupId>org.foxlabs</groupId>
        <artifactId>foxlabs-validation</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

No other dependencies are required.

Writing Bean

Lets create an Account class:

import org.foxlabs.validation.constraint.*;

public class Account {
    
    private Long id;
    private String username;
    private String email;
    private String password;
    
    @NotNull
    public Long getId() {
        return id;
    }
    
    public void setId(Long id) {
        this.id = id;
    }
    
    @Coalesce @NotEmpty
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    @Despace @NotEmpty @EmailAddress
    public String getEmail() {
        return email;
    }
    
    public void setEmail(String email) {
        this.email = email;
    }
    
    @NotEmpty @Size(min = 6, max = 32)
    public String getPassword() {
        return password;
    }
    
    public void setPassword(String password) {
        this.password = password;
    }
    
}

Here you define the following constraints:

Validating Bean

In order to validate bean you need a few lines of code.

Account account = new Account();
account.setId(1L);
account.setUsername("Fox Mulder");
account.setEmail("foxinboxx@gmail.com");
account.setPassword("The truth is out there");

Validator<Account> validator = ValidatorFactory.getDefault().newValidator(Account.class);
try {
    validator.validateEntity(account);
} catch (ValidationException e) {
    e.printViolations();
}

In the example above, we create a new Account, initialize its properties and perform validation. All violations will be printed to the System.err. You can experiment with the values ​​of the properties to see validation results.

Architecture

On the following class diagram you will see the key classes of this framework.

Class Diagram

Next sections describe all these classes in more details.

Validation Components

Validation components are basic blocks of this framework. The Validation interface defines generic abstraction of the validation component. For the moment there are two component types: Constraint and Converter.

The ValidationContext interface provides information about current validation state to the components. Implementation of that interface passed to all validation methods of the components.

The Constraint interface defines component, suitable for check whether a test value conforms to some rules or not. Also it allows to modify (correct) test value if it is possible. If for some reason test value doesn’t fit the validation rules and it cannot be corrected then the result of validation should be ConstraintViolationException. In other cases constraint should return (possibly modified) test value.

The Converter interface defines component, suitable for convert value to and from text representation. If for some reason value cannot be converted from string representation then the result of conversion should be MalformedValueException. Conversion of a value to string representation doesn’t assume any exceptions. The ValidationContext.isLocalizedConvert() method specifies if localized representation is required.

Validation Messages

Validation component may provide error message that will be used when validation process fails. That message may depend on validation context or component parameters. Because of that Validation interface defines two methods that allow to obtain required information for error message generation. The Validation.getMessageTemplate() method allows to obtain localized error message template. The Validation.appendMessageArguments() method provides arguments to be substituted into the error message template. Locale of the error message and its arguments should be obtained from the ValidationContext.getMessageLocale() method.

The MessageResolver is used for resolving localized message templates. That abstraction allows to store error messages anywhere. By default org/foxlabs/validation/resource/validation-messages resource bundle is used. Validation components could use ValidationContext.resolveMessage() to obtain error message templates. Fully qualified class name of the validation component is usually used as message key.

Having localized error message template and its arguments, goal of the MessageBuilder is to build error message. By default the DefaultMessageBuilder is used.

Validation Exceptions

Each validation component can throw ViolationException that contains necessary information about validation state on the moment of violation, invalid value and source component which generates exception. In case of multiple violations the ValidationException represents full stack of violations with detailed information about each violation.

Errors in the validation component declaration propagated as the ValidationDeclarationException and its subclasses.

Entity Metadata

Metadata needed to describe the entity and its properties, constraints and converters imposed on them.

The EntityMetaData interface defines entity metadata. The PropertyMetaData interface defines property metadata. For Java Beans and POJOs there is the BeanMetaData class that allows to gather all information about entity from annotations. The MapMetaData class can be used to describe metadata for validation of the java.util.Map entities.

Validator And Its Factory

All architecture components binded together through the Validator class. That class has a set of shortcut methods to perform validation with default parameters. If other configuration is required then the Validator.newContext() method should be used. Returned Validator.ContextBuilder instance allows to configure validation parameters as you need.

To create a new validator the ValidatorFactory should be used. The main purpose of that factory is maintaining validators configuration. You can configure factory once and then create validators with different metadata.

...
ValidatorFactory factory = ValidatorFactory.getDefault();
Validator<Account> validator = factory.newValidator(Account.class);
Validator<Account>.ContextBuilder context = validator.newContext();
context.setLocale(Locale.ROOT).setFailFast(true).validateEntity(account);
...

Validating Groups

Validating groups are string identifiers that can be applied to constraints only and allow to check certain constraints defined on the element. If constraint is not binded to any validating group then it will be binded to default group (empty string) automatically. By default validation will be performed for all defined constraints.

The Validator.ContextBuilder.setValidatingGroups() method allows to change validating groups.

...
Validator<Account>.ContextBuilder context = validator.newContext();
context.setValidatingGroups("correct", "update").validateEntity(account);
...

Annotations

For Java Beans and POJOs validation components can be defined through annotations.

Constraint Annotations

Each constraint annotation should be annotated by the @ConstrainedBy annotation that specifies constraint implementation classes. Also constraint annotation can define the following elements:

For example, you can take a look at the @NotNull annotation.

There are several specific annotations.

Annotation Description
@Composition Defines composition (AND) of constraints of the annotated element.
@Conjunction Defines conjunction (AND) of constraints of the annotated element.
@Disjunction Defines disjunction (OR) of constraints of the annotated element.
@Negation Defines negation (NOT) of constraints of the annotated element.
@NotNull @NotEmpty
@Url(targets = ValidationTarget.ELEMENTS)
@EmailAddress(targets = ValidationTarget.ELEMENTS)
@Disjunction(targets = ValidationTarget.ELEMENTS)
public List<String> getContactList() {
    ...
}

In the example above we define constraint for java.util.List property type. This list cannot be null or empty, each element of the list must be a well-formed URL or email address.

Also elements of the list in example above can be null because @Url and @EmailAddress constraints allow nulls.

Converter Annotations

Each converter annotation should be annotated by the @ConvertedBy annotation that specifies converter implementation classes. Also converter annotation can define the following elements:

For example, you can take a look at the @NumberPattern annotation.

@DateStyle(date = DateFormat.SHORT)
public Date getCurrentDate() {
    ...
}

In the example above we define converter for java.util.Date property type with the specified date style.

Also you can use converter annotations in conjunction with the Tokenizer annotations for arrays, java.util.Collection and java.util.Map types (for example, you can use the @TokenDelimiters annotation).

Custom Annotations

Custom annotations are annotations constructed from other annotations. Custom annotations must not be annotated by the @ConstrainedBy or @ConvertedBy annotations.

Lets create custom constraint annotation that will check whether the specified string is well-formed URL or email address.

import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import org.foxlabs.validation.ValidationTarget;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Disjunction @Url @EmailAddress
public @interface ContactAddress {
    
    String message() default "";
    
    String[] groups() default {};
    
    ValidationTarget[] targets() default {};
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
    public static @interface List {
        
        ContactAddress[] value();
        
    }
    
}

Now you can apply @ContactAddress annotation for any property of the string type.

Customization

In this chapter, we will consider opportunities to customize components of the proposed framework.

Implementing Your Own Constraint

To define a new constraint you need to implement the Constraint interface. Depending on what kind of constraint you need to, you can use one of the existing classes.

Class Description
CorrectConstraint Allows to modify (correct) test value and never throws ConstraintViolationException.
CheckConstraint Only checks a test value and doesn’t modify it.
RegexConstraint Checks whether a test string matches the regular expression.
EnumerationConstraint Checks whether a test value is one of the allowed constants.
IgnoreCaseEnumerationConstraint Checks whether a test string is one of the allowed strings using ignore case comparison.
SizeConstraint Checks whether the size of a test value is within allowed minimum and maximum bounds.

You can also extend the AbstractValidation class in conjunction with Constraint interface.

Lets implement StartsWithConstraint that will check whether a test string starts with some prefix.

import org.foxlabs.validation.ValidationContext;
import org.foxlabs.validation.constraint.CheckConstraint;

import org.foxlabs.util.Assert;

public class StartsWithConstraint extends CheckConstraint<String> {
    
    private final String prefix;
    
    public StartsWithConstraint(String prefix) {
        this.prefix = Assert.notEmpty(prefix, "Prefix cannot be null or empty!");
    }
    
    @Override
    public Class<?> getType() {
        return String.class;
    }
    
    @Override
    public boolean appendMessageArguments(ValidationContext<?> context, Map<String, Object> arguments) {
        super.appendMessageArguments(context, arguments);
        arguments.put("prefix", prefix);
        return true;
    }
    
    @Override
    protected <T> boolean check(String value, ValidationContext<T> context) {
        return value == null || value.startsWith(prefix);
    }
    
}

Don’t forget to add constraint error message template in the default message bundle (see Validation Messages).

If you plan to use StartsWithConstraint for validating Java beans or POJOs then you need to define constraint annotation and add corresponding constructor in the constraint implementation class.

import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import org.foxlabs.validation.ValidationTarget;
import org.foxlabs.validation.constraint.ConstrainedBy;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
@ConstrainedBy(StartsWithConstraint.class)
public @interface StartsWith {
    
    String value(); // prefix
    
    String message() default "";
    
    String[] groups() default {};
    
    ValidationTarget[] targets() default {};
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
    public static @interface List {
        
        StartsWith[] value();
        
    }
    
}

Also you need to change StartsWithConstraint.

...

public class StartsWithConstraint extends CheckConstraint<String> {
    
    ...
    
    public StartsWithConstraint(StartsWith annotation) {
        this(annotation.value());
    }
    
    ...
    
}

That’s it. Now you can apply @StartsWith annotation for any property of the string type.

Implementing Your Own Converter

To define a new converter you need to implement the Converter interface.

You can extend the AbstractConverter class or AbstractValidation class in conjunction with Converter interface.

Lets implement RgbColorConverter that will convert #RRGGBB color text representation into the java.awt.Color and vice versa.

import java.awt.Color;

import org.foxlabs.validation.ValidationContext;
import org.foxlabs.validation.converter.AbstractConverter;
import org.foxlabs.validation.converter.MalformedValueException;

public class RgbColorConverter extends AbstractConverter<Color> {
    
    @Override
    public Class<Color> getType() {
        return Color.class;
    }
    
    @Override
    protected <T> Color doDecode(String value, ValidationContext<T> context) {
        if (value.length() == 7 && value.charAt(0) == '#') {
            try {
                int r = Integer.parseInt(value.substring(1, 3), 16);
                int g = Integer.parseInt(value.substring(3, 5), 16);
                int b = Integer.parseInt(value.substring(5, 7), 16);
                
                return new Color(r, g, b);
            } catch (NumberFormatException e) {}
        }
        throw new MalformedValueException(this, context, value);
    }
    
    @Override
    protected <T> String doEncode(Color value, ValidationContext<T> context) {
        StringBuilder rgb = new StringBuilder(7);
        rgb.append('#');
        
        String r = Integer.toHexString(value.getRed());
        if (r.length() < 2) {
            rgb.append('0');
        }
        rgb.append(r);
        
        String g = Integer.toHexString(value.getGreen());
        if (g.length() < 2) {
            rgb.append('0');
        }
        rgb.append(g);
        
        
        String b = Integer.toHexString(value.getBlue());
        if (b.length() < 2) {
            rgb.append('0');
        }
        rgb.append(b);
        
        return rgb.toString();
    }
    
}

Don’t forget to add converter error message template in the default message bundle (see Validation Messages).

If you want to use RgbColorConverter as default converter for java.awt.Color value types you need to register it in the ConverterFactory.

ConverterFactory.addDefaultConverter(new RgbColorConverter());

But if you want to use RgbColorConverter for Java Beans or POJOs and you have different converters for java.awt.Color type or you want to parameterize your RgbColorConverter (for example, you may want to encode colors in upper or lower case) then you need an annotation.

import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import org.foxlabs.validation.ValidationTarget;
import org.foxlabs.validation.converter.ConvertedBy;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
@ConvertedBy(RgbColorConverter.class)
public @interface RgbColor {
    
    boolean upperCase() default true; // encoding parameter
    
    String message() default "";
    
    ValidationTarget[] targets() default {};
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
    public static @interface List {
        
        RgbColor[] value();
        
    }
    
}

Also you need to change the RgbColorConverter.

...

public class RgbColorConverter extends AbstractConverter<Color> {
    
    private final boolean upperCase;
    
    public RgbColorConverter(boolean upperCase) {
        this.upperCase = upperCase;
    }
    
    public RgbColorConverter(RgbColor annotation) {
        this(annotation.upperCase());
    }
    
    ...
    
    @Override
    protected <T> String doEncode(Color value, ValidationContext<T> context) {
        ...
        
        String text = rgb.toString();
        return upperCase ? text.toUpperCase() : text.toLowerCase();
    }
    
}

That’s it. Now you can apply @RgbColor annotation for any property of the java.awt.Color type.

Customizing Other Components

In most cases you don’t need to customize anything in this framework except Constraint and Converter. But in some cases you may need to customize EntityMetaData, MessageResolver or MessageBuilder and even ValidatorFactory.

Extending Metadata

Extending metadata is required when your entities are not represented as Java Beans or POJOs, or you define entities or properties by other criteria (for example, you can have @Property annotation to define entity properties). To define your own metadata you can use existing abstract classes.

Class Description
AbstractEntityMetaData Provides base implementation of the entity metadata.
AbstractPropertyMetaData Provides base implementation of the property metadata.

After you defined your own metadata, it can be used to create validators.

EntityMetaData<T> myMeta = ...
ValidatorFactory factory = ...
Validator<T> validator = factory.newValidator(myMeta);

Extending Message Resolver And Builder

If your error message templates are not stored as Java resources (for example, they can be stored in a database) then you need to provide your own MessageResolver and initialize your ValidatorFactory.

MessageResolver resolver = ...
ValidatorFactory factory = ...
factory.setMessageResolver(resolver);

Also you may want to store your error message templates in another Java resource bundle. If so then you need to tell about that to ValidatorFactory.

ValidatorFactory myFactory = new ValidatorFactory("my/message/bundle/name");

The MessageResolverChain class can be used to create a chain of the MessageResolvers.

The goal of the MessageBuilder is to render error message templates for the current ValidationContext and message arguments provided by the validation components. The DefaultMessageBuilder is used by default to render message templates. If you need more sophisticated message renderer then you should provide your own MessageBuilder implementation (for example, you can use one of template engines to render messages) and tell about that to ValidatorFactory.

MessageBuilder builder = ...
ValidatorFactory factory = ...
factory.setMessageBuilder(builder);

You can also extend the AbstractMessageBuilder abstract class to provide your own implementation.

Extending Validator Factory

The main goal for extending ValidatorFactory is to provide additional configuration to the Validator. After, these configuration parameters can be accessed in the validation components. The right way is to extend Validator too, because factory configuration is supposed as mutable.

Constrained Map

This validation framework is not tied only to validating Java Beans or POJOs. You can validate any entity having EntityMetaData descriptor. So it is a good idea to validate java.util.Map entities.

The ConstrainedMap class allows to maintain a set of properties with constraints. It also provides modifications in transaction-like manner and convertation of property values into and from string representation. More over, this class is thread-safe and suitable for maintaining configuration properties in multithreaded environment such as web application.

The following example will show how you can create such map.

import org.foxlabs.validation.ConstrainedMap;
import org.foxlabs.validation.ValidatorFactory;
import org.foxlabs.validation.metadata.MapMetaData;

import static org.foxlabs.validation.constraint.ConstraintFactory.*;

@SuppressWarnings("unchecked")
public class Configuration {
    
    public static final ConstrainedMap SETTINGS;
    static {
        MapMetaData.Builder builder = new MapMetaData.Builder();
        
        builder.property("admin.email", String.class, join(despace(), notBlank(), emailAddress()), "x@y.z")
               .property("session.timeout", Integer.class, join(notNull(), range(10, 1440)), 30)
               .property("output.encoding", String.class, supportedEncoding(), "ISO-8859-1");
        
        SETTINGS = new ConstrainedMap(ValidatorFactory.getDefault().newValidator(builder.build()));
    }
    
}

In the example above, we build SETTINGS map with 3 constrained properties.

The next step is to modify values.

// changing single property
try {
    Configuration.SETTINGS.setValue("session.timeout", 60);
} catch (ValidationException e) {
    e.printViolations();
}

// changing multiple properties
ConstrainedMap.Transaction tx = Configuration.SETTINGS.newTransaction();
tx.setValue("admin.email", "foxinboxx@gmail.com");
tx.setValue("session.timeout", 5);
tx.setValue("output.encoding", "UTF-8");
try {
    tx.commit(false);
    System.out.println(Configuration.SETTINGS);
} catch (ValidationException e) {
    e.printViolations();
}

ConstrainedMap validates property values internally when you change them. So if you need to change multiple properties it is better to use transaction. Also transaction needed if you want to read a number of properties and avoid inconsistency in multithreaded environment.

If you need to store (or restore) property values in a file or stream the code below can be helpful.

File file = new File("settings.properties");
Configuration.SETTINGS.save(file, "My configuration");
try {
    Configuration.SETTINGS.load(file);
} catch (ValidationException e) {
    e.printViolations();
}