Nuxeo Server

Field Constraints and Validation

Updated: November 13, 2017 Page Information Edit on GitHub

This page explains how to add validation constraints to a document type by using the Nuxeo component org.nuxeo.ecm.core.api.DocumentValidationService.

Document validation is a feature which:

  • Lets you express constraints on Document's fields: field type, format, content, ...
  • Provides the APIs to validate documents
  • Reports the set of violations (localized)
  • Provides the APIs to get constraints on document fields
  • Lets you express that some fields are references to other documents, directory entries, Nuxeo users or Nuxeo groups
  • Provides the APIs to fetch objects referenced by a document field
  • Lets you define references to your own business objects

For now, document validation does not:

  • Manage cross field validation
  • Manage list field size
  • Manage existing data cleaning

Concepts

Constraints are defined in schemas, on any field. It is based on XML Schema facets (W3C - XML Schema - Datatypes). But Nuxeo does not implement the whole XML Schema specification and Nuxeo adds non-XSD features.

Constraints are loaded by Nuxeo Core as Java objects implementing org.nuxeo.ecm.core.schema.types.constraints.Constraint.

org.nuxeo.ecm.core.schema.types.constraints.Constraint

public interface Constraint extends Serializable {
    boolean validate(Object object);
    String getErrorMessage(Object invalidValue, Locale locale);
    Description getDescription();
    class Description {
        public String getName() {...}
        public Map<String, Serializable> getParameters() {...}
    }
}

Constraints on a document field are available through the API by calling org.nuxeo.ecm.core.schema.types.Field.getConstraints().

Constraints on a field type are available through the API by calling org.nuxeo.ecm.core.schema.types.SimpleType.getConstraints(). Please note that constraints on fields integrate the constraints of the field type.

Documents, document properties or simple values can be validated through Nuxeo service org.nuxeo.ecm.core.api.validation.DocumentValidationService.

org.nuxeo.ecm.core.api.validation.DocumentValidationService

public interface DocumentValidationService {
    ConstraintViolationReport validate(DocumentModel document);
    ConstraintViolationReport validate(Property property);
    ConstraintViolationReport validate(Field field, Object value);
    ...
}

Validation can be enabled/disabled at different level by means of the extension point activations of the org.nuxeo.ecm.core.api.DocumentValidationService component.

Setting up Constraints

Below are the properties of the different constraints that you can set on schema fields.

Text and Number Format

Name: PatternConstraint

Parameters:

  • Pattern: the regular expression which constrains the format

Availability: Any XSD simple type derived from datatype xsd:string, xsd:integer, xsd:long or xsd:decimal (or alternatives like xsd:negativeinteger).

Behavior: This constraint ensures that a field matches a regular expression. null value is always valid (use the NotNullConstraint in combination).

Examples:

  • An email

    <xs:element name="email">
      <xs:simpleType>
        <xs:restriction base="xs:string">
          <xs:pattern value="[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?*" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
    
  • US Zip Code + 4 digit extension Postal Code

    <xs:simpleType name="zipcodetype">
      <xs:restriction base="xs:integer">
        <xs:pattern value="[0-9]{5}([- /]?[0-9]{4})?" />
      </xs:restriction>
    </xs:simpleType>
    
    <xs:element name="zipcode" type="zipcodetype" />
    

List of Values

Name: EnumConstraint

Parameters:

  • Values: A list of possible value for the field

Availability: Any XSD simple type derived from datatype xsd:string, xsd:integer, xsd:long or xsd:decimal (or alternatives like xsd:negativeinteger).

Behavior: This constraint ensures that the value of a field belongs to a set of values. null value is always valid (use the NotNullConstraint in combination).

Example: Preference for use of a hand

<xs:element name="mainColor">
  <xs:simpleType>
    <xs:restriction base="xs:string">
      <xs:enumeration value="red" />
      <xs:enumeration value="green" />
      <xs:enumeration value="blue" />
      <xs:enumeration value="black" />
      <xs:enumeration value="white" />
    </xs:restriction>
  </xs:simpleType>
</xs:element>

Length of Text

Name: LengthConstraint

Parameters:

  • Minimum: The minimum number of characters the field can contain, if specified.
  • Maximum: The maximum number of characters the field can contain, if specified.

Availability: Any XSD simple type derived from datatype xsd:string (or alternatives like xsd:name).

Behavior: This constraint ensures that a field contains a given quantity of characters. null value is always valid (use the NotNullConstraint in combination).

LengthConstraint may alter the database schema when using an SQL backend. If the minLenght is defined, a varchar with the given length will be created.

Examples:

  • A login with a minimal size of 4 characters

    <xs:element name="login">
      <xs:simpleType>
        <xs:restriction base="xs:string">
          <xs:minLength value="4" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
    
  • A credit card code with exactly 15 characters

    <xs:element name="code">
      <xs:simpleType>
        <xs:restriction base="xs:string">
          <xs:minLength value="15" />
          <xs:maxLength value="15" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
    

Numeric Range

Name: NumericIntervalConstraint

Parameters:

  • Minimum: The lower bound, if specified.
  • MinimumInclusive: true if the minimum bound is included, false otherwise.
  • Maximum: The upper bound, if specified.
  • MaximumInclusive: true if maximum bound is included, false otherwise.

Availability: Any XSD simple type derived from datatype xsd:integer, xsd:long or xsd:decimal (or alternatives like xsd:negativeinteger).

Behavior: This constraint ensures that a number is in a given range, null value is always valid (use the NotNullConstraint in combination).

Examples:

  • A month (Western calendar)

    <xs:element name="month">
      <xs:simpleType>
        <xs:restriction base="xs:integer">
          <xs:minInclusive value="1" />
          <xs:maxInclusive value="12" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
    
  • The temperature in the universe (Fahrenheit degrees - above absolute zero)

    <xs:element name="temperature">
      <xs:simpleType>
        <xs:restriction base="xs:integer">
          <xs:minExclusive value="-459.67" />
        </xs:restriction>
      </xs:simpleType>
    </xs:element>
    

Time Range

Name: NumericIntervalConstraint

Parameters:

  • Minimum: The start date of the time range, if specified.
  • MinimumInclusive: true if the start date is included, false otherwise.
  • Maximum: The start date of the time range, if specified.
  • MaximumInclusive: true if end date is included, false otherwise.

Availability: Any XSD simple type derived from datatype xsd:date (or alternatives like xsd:dateTime).

Behavior: This constraint ensures that a date is in a given time range. null value is always valid (use the NotNullConstraint in combination).

Example: Any date during 21 century

<xs:element name="event21century">
  <xs:simpleType>
    <xs:restriction base="xs:date">
      <xs:minInclusive value="2001-01-01" />
      <xs:maxInclusive value="2100-12-31" />
    </xs:restriction>
  </xs:simpleType>
</xs:element>

Mandatory Field

Name: NotNullConstraint

Parameters: none

Availability: Any document's field.

Behavior: This constraint ensures that a document's field has a value.

The NotNullConstraint Is the only constraint defined on the <xsd:element>. Other constraints are defined under <xsd:simpleType>. This constraint is also accessible through Field.getConstraints() but not through SimpleType.getConstraints().

Unlike XML Schema, Nuxeo's fields are optional if nothing was specified. To manage this difference, you have to write the nillable attribute from the XSD norm plus another specific to Nuxeo.

The non-standard nillable attribute must belong to a specific XML namespace: http://www.nuxeo.org/ecm/schemas/core/validation/. Without this namespace, the field will not be mandatory.

Example: A mandatory description

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns:nxsv="http://www.nuxeo.org/ecm/schemas/core/validation/"
           xmlns:nxs="http://www.nuxeo.org/ecm/schemas/example"
           targetNamespace="http://www.nuxeo.org/ecm/schemas/example">

  <xs:element name="description" type="xs:string" nillable="false" nxsv:nillable="false" />

</xs:schema>

Constraint Based on the Type

This constraint is automatically added according to XSD datatype on which the type of the document's field is based.

Names: string, long, integer, double, date, boolean and binary

Parameters: none

Behavior: This constraint controls the Java types supported by the field. It depends on the XSD datatype on which the element is based.null value is always valid (use the NotNullConstraint in combination).

Constraint nameSupported Java types
stringany
longany java.lang.Number
integerany java.lang.Number
doubleany java.lang.Number
dateany java.util.Date or java.util.Calendar
booleanjava.lang.Boolean
binary-

Setting up References

Below are the properties of the different references you can set on schema fields.

Reference to Another Document

Name: documentResolver

Parameters:

  • store: id or path. If id, a document's id is expected. If path, a document's place is expected.

Expected value: a concatenation of repository name and id (ex: default:45678-5678-45678-2346) or repository name and path (ex: default:/domain/places/doc).

Availability: Any XSD simple type derived from datatype xsd:string (or alternatives like xsd:name).

Behavior: This constraint ensures that a referenced document exists. null value is always valid (use the NotNullConstraint in combination).

Please note that the namespace http://www.nuxeo.org/ecm/schemas/core/external-references/ is necessary to define a resolver and its parameters.

Examples:

  • A reference to another document in a field named "favorite" (note the attribute ref:store="id", the reference will follow the document moves)

    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
               xmlns:ref="http://www.nuxeo.org/ecm/schemas/core/external-references/"
               xmlns:nxs="http://www.nuxeo.org/ecm/schemas/example"
               targetNamespace="http://www.nuxeo.org/ecm/schemas/example">
      <xs:element name="favorite">
        <xs:simpleType>
          <xs:restriction base="xs:string" ref:resolver="documentResolver" ref:store="id" />
        </xs:simpleType>
      </xs:element>
    </xs:schema>
    
  • A reference to another document in a field named "workspace" (note the attribute ref:store="path", the reference will not follow the document moves)

    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
               xmlns:ref="http://www.nuxeo.org/ecm/schemas/core/external-references/"
               xmlns:nxs="http://www.nuxeo.org/ecm/schemas/example"
               targetNamespace="http://www.nuxeo.org/ecm/schemas/example">
      <xs:element name="workspace">
        <xs:simpleType>
          <xs:restriction base="xs:string" ref:resolver="documentResolver" ref:store="path" />
        </xs:simpleType>
      </xs:element>
    </xs:schema>
    

Reference to a Nuxeo User or a Nuxeo Group

Name: userManagerResolver

Parameters:

  • includeUsers: true if the constraint search for user, false otherwise.
  • includeGroups: true if the constraint search for group, false otherwise.

Expected value: a user name prefixed by "user:" (user:Administrator) or a group name prefixed by group: (group:members).

Availability: Any XSD simple type derived from datatype xsd:string (or alternatives like xsd:name).

Behavior: This constraint ensures that a referenced user or group exists. null value is always valid (use the NotNullConstraint in combination).

Please note that the namespace http://www.nuxeo.org/ecm/schemas/core/external-references/ is necessary to define a resolver and its parameters.

Examples:

  • A reference to an author which must be a Nuxeo user (the ref:type="user" indicates the field must contain a user - not a group).

    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
               xmlns:ref="http://www.nuxeo.org/ecm/schemas/core/external-references/"
               xmlns:nxs="http://www.nuxeo.org/ecm/schemas/example"
               targetNamespace="http://www.nuxeo.org/ecm/schemas/example">
      <xs:element name="author">
        <xs:simpleType>
          <xs:restriction base="xs:string" ref:resolver="userManagerResolver" ref:type="user" />
        </xs:simpleType>
      </xs:element>
    </xs:schema>
    
  • A reference to a Nuxeo group (the ref:type="group" indicates the field must contain a group - not a user).

    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
               xmlns:ref="http://www.nuxeo.org/ecm/schemas/core/external-references/"
               xmlns:nxs="http://www.nuxeo.org/ecm/schemas/example"
               targetNamespace="http://www.nuxeo.org/ecm/schemas/example">
      <xs:element name="group">
        <xs:simpleType>
          <xs:restriction base="xs:string" ref:resolver="userManagerResolver" ref:type="group" />
        </xs:simpleType>
      </xs:element>
    </xs:schema>
    
  • A reference to a set of Nuxeo user or Nuxeo group (here, there is no ref:type attribute - the field accept user and group).

    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
               xmlns:ref="http://www.nuxeo.org/ecm/schemas/core/external-references/"
               xmlns:nxs="http://www.nuxeo.org/ecm/schemas/example"
               targetNamespace="http://www.nuxeo.org/ecm/schemas/example">
      <xs:element name="access">
        <xs:complexType>
          <xs:sequence>
            <xs:element name="principal" minOccurs="0" maxOccurs="unbounded">
              <xs:simpleType>
                <xs:restriction base="xs:string" ref:resolver="userManagerResolver" />
              </xs:simpleType>
            </xs:element>
          </xs:sequence>
        </xs:complexType>
      </xs:element>
    </xs:schema>
    

Reference to Nuxeo Directory/Vocabulary Entry

Name: directoryResolver

Parameters:

  • directory: the referenced directory.

Expected value: An id of an directory entry.

Availability: Any XSD simple type derived from datatype xsd:string (or alternatives like xsd:name).

Behavior: This constraint ensures that a referenced directory entry exists. null value is always valid (use the NotNullConstraint in combination).

Please note that the namespace http://www.nuxeo.org/ecm/schemas/core/external-references/ is necessary to define a resolver and its parameters.

Example: A car brand (the ref:directory="carBrands" indicates the field must refer to an entry of the carBrands directory).

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns:ref="http://www.nuxeo.org/ecm/schemas/core/external-references/"
           xmlns:nxs="http://www.nuxeo.org/ecm/schemas/example"
           targetNamespace="http://www.nuxeo.org/ecm/schemas/example">
  <xs:element name="carBrand">
    <xs:simpleType>
      <xs:restriction base="xs:string" ref:resolver="directoryResolver" ref:directory="carBrands" />
    </xs:simpleType>
  </xs:element>
</xs:schema>

Document Validation APIs

Explicit Validation

Once you described your data constraints, you can use the validation API to be sure your data are correct.

The validation API provides multiple operations to manage that.

Get the validation service

import org.nuxeo.ecm.core.api.validation.DocumentValidationService;
// ...
DocumentValidationService validator = Framework.getService(DocumentValidationService.class);

Validating a full document

DocumentModel doc = session.createDocumentModel("/", "doc1", "DocumentType");
// update doc ...
DocumentValidationReport report = validator.validate(doc);

Validating the updated fields of a document

DocumentModel doc = session.getDocument(new PathRef("/doc1"));
// update the existing document
DocumentValidationReport report = validator.validate(doc, true); // the second parameter enable "only dirty" validation

Validating a document property

DocumentModel doc = session.getDocument(new PathRef("/doc1"));
// update the existing document
DocumentValidationReport report = validator.validate(doc, true); // the second parameter enable "only dirty" validation

Validating data according to a field definition

SchemaManager schemaManager = Framework.getService(SchemaManager.class);
Schema schema = schemaManager.getSchema("userprofile");
Field field = schema.getField("birthdate");
GregorianCalendar birthdate = new GregorianCalendar(1984, 5, 15);
DocumentValidationReport report = validator.validate(field, birthdate);

Validating data according to a field XPath

GregorianCalendar birthdate = new GregorianCalendar(1984, 5, 15);
DocumentValidationReport report = validator.validate("userprofile:birthdate", birthdate);

Validating data according to a field XPath (complex fields)

This feature allows to validate a value according to the definition of a simple field which is a child of a complex types hierarchy.

Given this schema:

<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns:nxv="http://nuxeo.com/schemas/validationSample"
           targetNamespace="http://nuxeo.com/schemas/validationSample"
           xmlns:nxsv="http://www.nuxeo.org/ecm/schemas/core/validation/">
  <xs:element name="users">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="user" minOccurs="0" maxOccurs="unbounded">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="firstname" nillable="false" nxsv:nillable="false">
                <xs:simpleType>
                  <xs:restriction base="xs:string">
                    <xs:pattern value=".*\S.*" />
                  </xs:restriction>
                </xs:simpleType>
              </xs:element>
            </xs:sequence>
            <xs:attribute name="lastname" use="required">
              <xs:simpleType>
                <xs:restriction base="xs:string">
                  <xs:pattern value="[A-Z][a-z '-]+" />
                </xs:restriction>
              </xs:simpleType>
            </xs:attribute>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>

You can do:

String firtname = "John";
DocumentValidationReport report = validator.validate("schemprefix:users:user:firstname", firstname);

Automated Validation

Validation can be enable/disable at different level by contributing to the validation service:

<extension target="org.nuxeo.ecm.core.api.DocumentValidationService" point="activations">
  <validation context="createDocument" activated="true" />
  <validation context="saveDocument" activated="true" />
  <validation context="importDocument" activated="true" />
</extension>

By default, the validation is fully activated (create, save, import).

If activated, validation could throw DocumentValidationException while calling:

CoreSession.createDocument(DocumentModel);
CoreSession.createDocument(DocumentModel[]);
CoreSession.saveDocument(DocumentModel);
CoreSession.saveDocuments(DocumentModel[]);
CoreSession.importDocuments(List<DocumentModel>);

Analysing Validation Reports

DocumentValidationService's methods return DocumentValidationReport.

When activated, validation will throw a DocumentValidationException when creating, saving or importing invalid documents. This exception contains a DocumentValidationReport. You can get it by calling DocumentValidationException.getReport().

This report contains all the ConstraintViolation met while validating a Document.

You can get a localized error message for each violation by calling ConstraintViolation.getMessage(Locale).

You can also get each violation, the broken constraint, the corresponding invalid value and the place where the violation append.

DocumentValidationReport report = validator.validate(document);
// document constains a list of users with lastname/firtname (lastname and firstname should not be null and must contains non-space characters.
if (report.hasError()) {
    System.out.println(String.format("%s errors found :", report.numberOfErrors()));
    for (ConstraintViolation violation : report.asList()) {
        System.out.println(String.format("- Broken constraint %s", violation.getConstraint().getDescription()));
        System.out.println(String.format("  invalid value : \"%s\"", violation.getInvalidValue()));
        System.out.print("  location: ");
        for (PathNode node : violation.getPath()) {
            System.out.print(node.getField().getName());
            if (node.isListItem()) {
                System.out.print("[" + node.getIndex() + "]");
            }
            System.out.print(":");
        }
        System.out.println();
        System.out.println();
    }
}
// output:
3 errors found :
- Broken constraint NotNullConstraint{}
  invalid value : "null"
  location: vs:users:user[0]:lastname:
- Broken constraint PatternConstraint{Pattern=.*\S.*}
  invalid value : "  "
  location: vs:users:user[0]:firstname:
- Broken constraint NotNullConstraint{}
  invalid value : "null"
  location: vs:users:user[1]:firstname:

Playing with Constraints

Through the APIs, you can get the constraints defined for each fields using:

  • org.nuxeo.ecm.core.schema.types.Type.getConstraints(): Will return all constraints defined on type (all except NotNullConstraint)
  • org.nuxeo.ecm.core.schema.types.Field.getConstraints(): Will return all constraint including NotNullConstraint (only field can be defined as nillable)

Example: a constraint introspection

for (Schema schema : doc.getDocumentType().getSchemas()) {
    for (Field field : schema.getFields()) {
        constraintIntrospection(field, Arrays.asList(field.getName().getPrefix()));
    }
}

private void constraintIntrospection(Field field, List<String> path) {
    List<String> newPath = new ArrayList<String>(path);
    newPath.add(field.getName().getLocalName());
    System.out.println(StringUtils.join(newPath, ':') + " has constraints: " + field.getConstraints());
    if (field.getType().isListType()) {
        Field listField = ((ListType) field.getType()).getField();
        constraintIntrospection(listField, newPath);
    } else if (field.getType().isComplexType()) {
        for (Field childField : ((ComplexType) field.getType()).getFields()) {
            constraintIntrospection(childField, newPath);
        }
    }
}
// output:
vs:users has constraints: []
vs:users:user has constraints: []
vs:users:user:lastname has constraints: [PatternConstraint{Pattern=[A-Z][a-z '-]+}, NotNullConstraint{}, string{}]
vs:users:user:firstname has constraints: [NotNullConstraint{}, PatternConstraint{Pattern=.*\S.*}, string{}]

Fetching Referenced Entities

When a field is defined as a reference and is associated to a resolver, you can use an ObjectResolver to manage the relation between the field and the corresponding object.

Example, a reference to another document:

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns:ref="http://www.nuxeo.org/ecm/schemas/core/external-references/"
           xmlns:nxs="http://www.nuxeo.org/ecm/schemas/example"
           targetNamespace="http://www.nuxeo.org/ecm/schemas/example">
  <xs:element name="favorite">
    <xs:simpleType>
      <xs:restriction base="xs:string" ref:resolver="documentResolver" ref:store="id" />
    </xs:simpleType>
  </xs:element>
</xs:schema>

To know if a field is a reference:

SchemaManager schemaManager = Framework.getService(SchemaManager.class);
Schema schema = schemaManager.getSchema("schema");
Field field = schema.getField("favorite");
if (field.getType().getObjectResolver() != null) {
  System.out.println("it's a reference to an object");
}

To know if a property is a reference:

DocumentModel document = ...;
if (document.getObjectResolver("schema:favorite") != null) {
  System.out.println("it's a reference to an object");
}
// or
Property property = document.getProperty("schema:favorite");
if (property.getObjectResolver() != null) {
  System.out.println("it's a reference to an object");
}
// or finally through the type
if (property.getType().getObjectResolver() != null) {
  System.out.println("it's a reference to an object");
}

To get the referenced value:

DocumentModel referenced = document.getObjectResolver("schema:favorite").fetch(DocumentModel.class);
// or
DocumentModel referenced = document.getProperty("schema:favorite").getObjectResolver().fetch(DocumentModel.class);
// both of the 2 previous approach are shortcut for:
Serializable reference = document.getPropertyValue("schema:favorite");
ObjectResolver resolver = document.getProperty("schema:favorite").getType().getObjectResolver();
DocumentModel referenced = resolver.fetch(DocumentModel.class, reference);

To set the referenced document:

DocumentModel referenced = ...;
document.getObjectResolver("schema:favorite").setObject(referenced);
// or
document.getProperty("schema:favorite").getObjectResolver().setObject(referenced);
// both of the 2 previous approach are shortcut for:
ObjectResolver resolver = document.getProperty("schema:favorite").getType().getObjectResolver();
Serializable reference = resolver.getReference(referenced);
document.setPropertyValue("schema:favorite", reference);

5 days ago manonlumeau Added content-review-lts2017 label
a month ago manonlumeau NXDOC-1346-FT review screenshot
2 years ago Manon Lumeau 48 | ix Explorer lin
3 years ago Solen Guitter 47 | Add links to explorer.nuxeo.com
3 years ago Solen Guitter 46
3 years ago Thibaud Arguillere 45
3 years ago Solen Guitter 44 | Moving Extending section in a dedicated how-to
3 years ago Alain Escaffre 43
3 years ago Alain Escaffre 42
3 years ago Solen Guitter 41 | typos and formatting
3 years ago Solen Guitter 40 | Typo
3 years ago Solen Guitter 39
3 years ago Manon Lumeau 38 | Formatting
3 years ago Nicolas Chapurlat 37
3 years ago Nicolas Chapurlat 36
3 years ago Nicolas Chapurlat 35
3 years ago Nicolas Chapurlat 34
3 years ago Nicolas Chapurlat 33
3 years ago Nicolas Chapurlat 32
3 years ago Nicolas Chapurlat 31
3 years ago Nicolas Chapurlat 30
3 years ago Nicolas Chapurlat 29
3 years ago Nicolas Chapurlat 28
3 years ago Nicolas Chapurlat 27
3 years ago Nicolas Chapurlat 26
3 years ago Nicolas Chapurlat 25
3 years ago Nicolas Chapurlat 24
3 years ago Nicolas Chapurlat 23
3 years ago Nicolas Chapurlat 22
3 years ago Nicolas Chapurlat 21
3 years ago Nicolas Chapurlat 20
3 years ago Nicolas Chapurlat 19
3 years ago Nicolas Chapurlat 18
3 years ago Nicolas Chapurlat 17
3 years ago Nicolas Chapurlat 16
3 years ago Nicolas Chapurlat 15
3 years ago Nicolas Chapurlat 14
3 years ago Nicolas Chapurlat 13
3 years ago Nicolas Chapurlat 12
3 years ago Nicolas Chapurlat 11
3 years ago Nicolas Chapurlat 10
3 years ago Nicolas Chapurlat 9
3 years ago Nicolas Chapurlat 8
3 years ago Nicolas Chapurlat 7
3 years ago Nicolas Chapurlat 5
3 years ago Nicolas Chapurlat 6
3 years ago Nicolas Chapurlat 4
3 years ago Nicolas Chapurlat 3
3 years ago Nicolas Chapurlat 2
3 years ago Nicolas Chapurlat 1
History: Created by Nicolas Chapurlat