Client SDKs

Java Automation Client

Updated: November 13, 2017 Page Information Edit on GitHub

Java Automation Client has been deprecated for Nuxeo LTS 2016. Please use Nuxeo Java Client.

Nuxeo provides a high level client implementation for Java programmers: Nuxeo Automation Client API simplifies your task since it handles all the protocol level details.

Dependencies

  • This documentation applies for nuxeo-automation-client versions greater or equal to 5.4.
  • The Nuxeo Automation Client is also available with all dependencies included (JAR shaded - nuxeo-automation-XX-jar-with-dependencies)

To use the Java Automation client you need to put a dependency on the following Maven artifact:

<dependency>
    <groupId>org.nuxeo.ecm.automation</groupId>
    <artifactId>nuxeo-automation-client</artifactId>
    <version>...</version>
</dependency>

with dependencies

<dependency>
    <groupId>org.nuxeo.ecm.automation</groupId>
    <artifactId>nuxeo-automation-client</artifactId>
    <version>...</version>
    <classifier>jar-with-dependencies</classifier>
 </dependency>

For a direct download, see https://maven.nuxeo.org/.

The client library depends on:

  • net.sf.json-lib:json-lib, net.sf.ezmorph:ezmorph - for JSON support
  • org.codehaus.jackson:jackson-core-asl
  • org.codehaus.jackson:jackson-mapper-asl.
  • org.apache.httpcomponents:httpcore, org.apache.httpcomponents:httpclient - for HTTP support
  • javax.mail - for multipart content support

Automation Client API

Let's take an example and execute the SELECT * FROM Document query against a remote Automation server:

import org.nuxeo.ecm.automation.client.jaxrs.impl.HttpAutomationClient;
import org.nuxeo.ecm.automation.client.model.Documents;
import org.nuxeo.ecm.automation.client.Session;

public static void main(String[] args) throws Exception {
    HttpAutomationClient client = new HttpAutomationClient(
           "http://localhost:8080/nuxeo/site/automation");

    Session session = client.getSession("Administrator", "Administrator");
    Documents docs = (Documents) session.newRequest("Document.Query").set(
           "query", "SELECT * FROM Document").execute();
    System.out.println(docs);

    client.shutdown();
}

You can see the code above has three distinctive parts:

  1. Opening a connection.
  2. Invoking remote operations.
  3. Destroying the client.

Opening a Connection

  1. Before using the Automation client you should first create a new client that is connecting to a remote address you can specify through the constructor URL argument. As the remote server URL you should use the URL of the Automation service.

    // create a new client instance
    HttpAutomationClient client = new HttpAutomationClient("http://localhost:8080/nuxeo/site/automation");
    

    No connection to the remote service is made at this step. The Automation service definition will be downloaded the first time you create a session.

    A local registry of available operations is created from the service definition sent by the server.

    The local registry of operations contains all operations on the server - but you can invoke only operations that are accessible to your user - otherwise a 404 (operation not found) will be sent by the server.

    Once you created a client instance you must create a new session to be able to start to invoke remote operations. When creating a session you should pass the credentials to be used to authenticate against the server.

  2. So you create a new session by calling:

    import org.nuxeo.ecm.automation.client.Session;
    
    Session session = client.getSession("Administrator", "Administrator");
    

    This will authenticate you onto the server using the basic authentication scheme.

    If needed, you can use another authentication scheme by setting an interceptor.

    client.setInterceptor(new PortalSSOAuthInterceptor("nuxeo5secretkey", "Administrator"));
    Session session = client.getSession();
    

    Make sure to not open a session each time you call an operation, otherwise you will have performances issues.

    We recommend to open the session once before calling any operation, like during application startup or when the user needs to authenticate. Then you may close it when the user logout or during application shutdown.

Invoking Remote Operations

Using a session you can now invoke remote operations.

  1. To create a new invocation request you should pass in the right operation or chain ID.

    OperationRequest request = session.newRequest("Document.Query");
    
  2. Then populate the request with all the required arguments.

    request.set("query", "SELECT * FROM Document");
    

    You can see in the example that you have to specify only the query argument. If you have more arguments, call the set method in turn for each of these arguments. If you need to specify execution context parameters you can use request.setContextProperty method. On the same principle, if you need to specify custom HTTP headers you can use the request.setHeader method.

  3. After having filled all the required request information you can execute the request by calling the execute method.

    Object result = request.execute();
    

    The client API provides both synchronous and asynchronous execution

    For an asynchronous execution you can make a call to the execute(AsyncCallback<Object> cb) method.

    Beware that you can set the timeout in milliseconds for the wait of the aynchronous thread pool termination at client shutdown by passing it to the HttpAutomationClient constructor: public HttpAutomationClient(String url, long httpConnectionTimeout, long asyncAwaitTerminationTimeout).

    Default value is 2 seconds. If you don't use any asynchronous call you can set this timeout to 0 in order not to wait at client shutdown.

    Setting the HTTP connection timeout

    You can set a timeout in milliseconds for the HTTP connection in order to avoid an infinite wait in case of a network cut or if the Nuxeo server is not responding.

    Just pass it to the HttpAutomationClient constructor: public HttpAutomationClient(String url, long httpConnectionTimeout, long asyncAwaitTerminationTimeout).

    Default value is 0 = no timeout.

    Executing a request will either throw an exception or return the result. The result object can be null if the operation has no result (i.e. a void operation) - otherwise a Java object is returned. The JSON result is automatically decoded into a proper Java object. The following objects are supported as operation results:

    • Document - a document object
    • Documents - a list of documents
    • Blob - a file
    • Blobs - a file list

    In case the operation invocation fails - an exception described by a JSON entity will be sent by the server and the client will automatically decode it into a real Java exception derived from org.nuxeo.ecm.automation.client.jaxrs.RemoteException.

    Before sending the request the client will check the operation arguments to see if they match the operation definition and will throw an exception if some required argument is missing. The request will be sent only after validation successfully completes.

The query example is pretty simply. The query operation doesn't need an input object to be executed. (i.e. the input can be null). But most operations require an input. In that case you must call request.setInput method to set the input. We will see more about this in the following examples.

If you prefer a most compact notation you can use the fluent interface way of calling methods:

Object result = session.newRequest("OperationId").set("var1", "val1").set("var2", "val2").execute();

Destroying the Client

When you are done with the client you must call the client.disconnect method to free any resource held by the client. Usually this is done only once when the client application is shutdown. Creating new client instances and destroying them may be costly so you should use a singleton client instance and use it from different threads (which is safe).

If you need different logins then create one session per login. A session is thread safe.

Create Read Update Delete

In this example we assume we already have a session instance.

Here is an example with Document.Create, Document.Update, Document.Delete, Document.Fetch operations.

You can see The Automation Client Service Adapter section to know how CRUD operations are wrapped into DocumentService adapter and how to use it.

import org.nuxeo.ecm.automation.client.model.Document;
import org.nuxeo.ecm.automation.client.Session; 
import org.nuxeo.ecm.automation.client.Constants;
import org.nuxeo.ecm.automation.client.model.IdRef;

// Fetch the root of Nuxeo repository
Document root = (Document) session.newRequest("Document.Fetch").set("value", "/").execute();

// Instantiate a new Document with the simple constructor
Document document = new Document("myDocument", "File");
document.set("dc:title", "My File");
document.set("dc:description", "My Description");

// Create a document of File type by setting the parameter 'properties' with String metadata values delimited by comma ','
document = (Document) session.newRequest("Document.Create").setHeader(Constants.HEADER_NX_SCHEMAS, "*").setInput(root).set("type", document.getType()).set("name", document.getId()).set("properties", document).execute();

// Set another title to update
document.set("dc:title", "New Title");

// Update the document
document = (Document) session.newRequest("Document.Update").setInput(document).set("properties", document).execute();

// Delete the document
session.newRequest("Document.Delete").setInput(document).execute();

Operations examples above fetch the document with common properties: common, dublincore, file. If you wish to fetch all properties or a specific schema, you can do as this following example:

import org.nuxeo.ecm.automation.client.model.Document;
import org.nuxeo.ecm.automation.client.Session;
import org.nuxeo.ecm.automation.client.Constants;
import org.nuxeo.ecm.automation.client.model.IdRef;

Document document = new Document("myDocument", "File");

// For all operations -> use setHeader method to specify specific schemas or every ones (*)

Document root = (Document) session.newRequest("Document.Fetch").setHeader(Constants.HEADER_NX_SCHEMAS, "*").set("value", "/").execute();

document = (Document) session.newRequest("Document.Create").setHeader(Constants.HEADER_NX_SCHEMAS, "dublincore,blog").setInput(root).set("type", document.getType()).set("name", document.getId()).set("properties", document).execute();

document = (Document) session.newRequest("Document.Update").setHeader(Constants.HEADER_NX_SCHEMAS, "dublincore,blog").setInput(document).set("properties", document).execute();

You can fetch all updated properties during the session with the following method of the document:

// Fetch the dirty properties (updated values) from the document
PropertyMap dirties = document.getDirties();

To see complex properties example and the use of propertyMap, see the Managing Complex Properties section.

Updating Multi-line String Properties

Multi-lines values lines must end with \\. For example:

document.set("mySchema:myProperty", "line1\\\nline2");
documentService.update(document);

Managing Blobs

In this example we assume we already have a session instance.

The example will create a new File document into the root "/" document and then will upload a file into it. Finally we will download back this file.

  1. Get the root document and create a new File document at location /myfile.

    import org.nuxeo.ecm.automation.client.model.Document;
    import org.nuxeo.ecm.automation.client.Session; 
    
    // get the root
    Document root = (Document) session.newRequest("Document.Fetch").set("value", "/").execute();
    
    // create a file document
    session.newRequest("Document.Create").setInput(root).set("type", "File").set("name", "myfile").set("properties", "dc:title=My File").execute();
    

    Note the usage of setInput() method. This is to specify that the create operation must be executed in the context of the root document - so the new document will be created under the root document. Also you can notice that the input object is a Document instance.

  2. Now get the file to upload and put it into the newly created document.

            File file = getTheFileToUpload();
            FileBlob fb = new FileBlob(file);
            fb.setMimeType("text/xml");
            // uploading a file will return null since we used HEADER_NX_VOIDOP
            session.newRequest("Blob.Attach").setHeader(
                    Constants.HEADER_NX_VOIDOP, "true").setInput(fb)
                    .set("document", "/myfile").execute();
    

    The last execute call will return null since the HEADER_NX_VOIDOP header was used. This is to avoid receiving back from the server the blob we just uploaded.

    Note that to upload a file we need to use a Blob object that wrap the file to upload.

  3. Now get the the file document where the blob was uploaded.
  4. Then retrieve the blob remote URL from the document metadata. We can use this URL to download the blob.

            import org.nuxeo.ecm.automation.client.model.Document;
            import org.nuxeo.ecm.automation.client.Session;
            import org.nuxeo.ecm.automation.client.Constants;
    
            // get the file document where blob was attached
            Document doc = (Document) session.newRequest(
                    "Document.Fetch").setHeader(
                    Constants.HEADER_NX_SCHEMAS, "*").set("value", "/myfile").execute();
            // get the file content property
            PropertyMap map = doc.getProperties().getMap("file:content");
            // get the data URL
            String path = map.getString("data");
    

    You can see we used the special HEADER_NX_SCHEMAS header to specify we want all properties of the document to be included in the response.

  5. Now download the file located on the server under the path we retrieved from the document properties:

            // download the file from its remote location
            blob = (FileBlob) session.getFile(path);
            // ... do something with the file
            // at the end delete the temporary file
            blob.getFile().delete();
    

    We can do the same by invoking the Blob.Get operation.

            // now test the GetBlob operation on the same blob
            blob = (FileBlob) session.newRequest("Blob.Get").setInput(doc).set(
                    "xpath", "file:content").execute();
            // ... do something with the file
            // at the end delete the temporary file
            blob.getFile().delete();
    

Managing Complex Properties

Complex properties can have different levels complexity. This part of the documentation is about managing them with Automation Client API using JSON format.

Let's see a complex property schema example:

<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns:mc="http://nuxeo.org/schemas/dataset/"
           elementFormDefault="qualified"
           targetNamespace="http://nuxeo.org/schemas/dataset/">
...
    <xs:complexType name="field">
        <xs:sequence>
            <xs:element name="name" type="xs:string"/>
            <xs:element name="description" type="xs:string"/>
            <xs:element name="fieldType" type="mc:fieldType">

            </xs:element>
            <xs:element name="columnName" type="xs:string"/>
            <xs:element name="sqlTypeHint" type="xs:string"/>

            <xs:element name="roles">
                <xs:complexType>
                    <xs:sequence>
                        <xs:element name="role" type="mc:roleType" minOccurs="0" maxOccurs="unbounded"/>
                    </xs:sequence>
                </xs:complexType>
            </xs:element>
        </xs:sequence>
    </xs:complexType>
...
</xs:schema>

Let's see a JSON example of these complex property values:

creation.json

[
    {
        "fieldType": "string",
        "description": "desc field0",
        "roles": [
            "Decision",
            "Score"
        ],
        "name": "field0",
        "columnName": "col0",
        "sqlTypeHint": "whatever"
    },
    {
        "fieldType": "string",
        "description": "desc field1",
        "roles": [
            "Decision",
            "Score"
        ],
        "name": "field1",
        "columnName": "col1",
        "sqlTypeHint": "whatever"
    },
    {
        "fieldType": "string",
        "description": "desc field2",
        "roles": [
            "Decision",
            "Score"
        ],
        "name": "field2",
        "columnName": "col2",
        "sqlTypeHint": "whatever"
    },
    {
        "fieldType": "string",
        "description": "desc field3",
        "roles": [
            "Decision",
            "Score"
        ],
        "name": "field3",
        "columnName": "col3",
        "sqlTypeHint": "whatever"
    },
    {
        "fieldType": "string",
        "description": "desc field4",
        "roles": [
            "Decision",
            "Score"
        ],
        "name": "field4",
        "columnName": "col4",
        "sqlTypeHint": "whatever"
    }
]

Finally updating a document with the JSON values into its related metadata looks like this.

// Fetch the document
Document document = (Document) session.newRequest(DocumentService.GetDocumentChild).setInput(new PathRef("/")).set("name", "testDoc").execute();

// Send the fields representation as json

// Read the json file
File fieldAsJsonFile = FileUtils.getResourceFileFromContext("creation.json");
String fieldsDataAsJSon = FileUtils.readFile(fieldAsJsonFile);

// Don't forget to replace CRLF or LF
fieldsDataAsJSon = fieldsDataAsJSon.replaceAll("\n", "");
fieldsDataAsJSon = fieldsDataAsJSon.replaceAll("\r", "");

// Set the json values to the related metadata
document.set("ds:fields", fieldsDataAsJSon);

// Document Update
session.newRequest(UpdateDocument.ID).setInput(document).set("properties",document).execute();

Managing Business Objects

It is possible to manage business objects (Plain Old Java Object client side for mapping the Nuxeo Document Model Adapter (Documentation) server side).

Why manage business object? To manipulate business object to avoid Nuxeo Document manipulation on client side:

pull : BusinessObject (POJO) <----JSON---- DocumentModelAdapter <---- DocumentModel

push : BusinessObject (POJO) -----JSON---> DocumentModelAdapter ----> DocumentModel

Let's see an example.

  1. Create a Nuxeo Document Model Adapter registered on server side and which should extend the BusinessAdapter class:

    import org.nuxeo.ecm.automation.core.operations.business.adapter.BusinessAdapter;
    import org.codehaus.jackson.annotate.JsonCreator;
    import org.codehaus.jackson.annotate.JsonProperty;
    import org.nuxeo.ecm.core.api.DocumentModel;
    
     /**
     * Document Model Adapter example server side
     */
    public class BusinessBeanAdapter extends BusinessAdapter {
    
        private static final Log log = LogFactory.getLog(BusinessBeanAdapter.class);
    
        /**
         * Default constructor is needed for jackson mapping
         */
        public BusinessBeanAdapter() {
            super();
        }
    
        public BusinessBeanAdapter(DocumentModel documentModel) {
            super(documentModel);
        }
    
        public String getTitle() {
            try {
                return (String) getDocument().getPropertyValue("dc:title");
            } catch (ClientException e) {
                log.error("cannot get property title", e);
            }
            return null;
        }
    
        public void setTitle(String value) {
            try {
                getDocument().setPropertyValue("dc:title", value);
            } catch (ClientException e) {
                log.error("cannot set property title", e);
            }
        }
    
        public String getDescription() {
            try {
                return (String) getDocument().getPropertyValue("dc:description");
            } catch (ClientException e) {
                log.error("cannot get description property", e);
            }
            return null;
        }
    
        public void setDescription(String value) {
            try {
                getDocument().setPropertyValue("dc:description", value);
            } catch (ClientException e) {
                log.error("cannot set description property", e);
            }
        }
    }
    
  2. Declare a POJO on client side as following:

    import org.nuxeo.ecm.automation.client.annotations.EntityType;
    
    //Automation client File pojo example annotated with EntityType setting the adapter server side built previously (just the simple name)
    @EntityType("BusinessBeanAdapter")
    public class BusinessBean {
        protected String title;
        protected String description;
        protected String id;
        protected String type;
    
        /**
        * this default contructor is mandatory for jackson mapping on the nuxeo server side
        **/
        public BusinessBean() {
        }
    
        public BusinessBean(String title, String description, String type) {
            this.title = title;
            this.type = type;
            this.description = description;
        }
        public String getTitle() {
            return title;
        }
        public void setTitle(String title) {
            this.title = title;
        }
        public String getDescription() {
            return description;
        }
        public void setDescription(String description) {
            this.description = description;
        }
        public String getId() {
            return id;
        }
        public void setId(String id) {
            this.id = id;
        }
       public String getType() {
            return type;
        }
        public void setType(String type) {
            this.type = type;
        }
     }
    

    We assume that the POJO client side stub is similar to the document model adapter stub server side.

    So now we have on client side a POJO BusinessBean and on server side a Document Model Adapter BusinessBeanAdapter.

    We are going to use two operations for creating/updating process:

  3. Let's see how to map them directly using these operations:

    import org.nuxeo.ecm.automation.client.jaxrs.spi.JsonMarshalling;
    
    // Let's instiante a BusinessBean 'File':
    BusinessBean file = new BusinessBean("File", "File description", "File");
    
    // BusinessBean 'File' Marshaller has to be registered in Automation Client following this way:
    JsonMarshalling.addMarshaller(PojoMarshaller.forClass(file.getClass()));
    
    // Injecting the BusinessBean 'File' into the operation BusinessCreateOperation let you create a document server side.
    file = (BusinessBean) session.newRequest("Operation.BusinessCreateOperation").setInput(file).set("name",file.getTitle()).set("parentPath","/").execute();
    
    // Update document on server (pojo <-> adapter update) using BusinessUpdateOperation
    file.setTitle("Title Updated");
    file = (BusinessBean) session.newRequest("Operation.BusinessUpdateOperation").setInput(file).execute();
    

GitHub Nuxeo Automation Tests

Here is the complete code of the example. For more examples, see the unit tests in nuxeo-automation-test project.

Automation Client Service Adapter

Default Service Adapter

Nuxeo Automation Client provides an "Adapter Service" to encapsulate business logic.

Default services provided:

Powered by yFiles

DocumentService Usage

Here is an example of using DocumentService API:

import org.nuxeo.ecm.automation.client.model.Document;
import org.nuxeo.ecm.automation.client.adapters.DocumentService;
import org.nuxeo.ecm.automation.client.Session;

// Test document creation
Document myDocument = new Document("myfolder2", "Folder");
myDocument.set("dc:title", "My Test Folder");
myDocument.set("dc:description", "test");
myDocument.set("dc:subjects", "a,b,c\\,d");

DocumentService documentService = session.getAdapter(DocumentService.class);

// Here is the way to create a document by using DocumentService
// Note you can put in first parameter, the Id or Path of the parent document
myDocument = documentService.createDocument(DocumentParent.getId(), myDocument);

// Fetch the document with its dublincore properties
// You can add several schemas like ...(myDocument, "dublincore", "yourSchema");
myDocument = documentService.getDocument(myDocument, "dublincore");

// Update the document title
myDocument.set("dc:title", "New Title");
documentService.update(myDocument);

// Fetch the document with all its properties (all schemas)
myDocument = documentService.getDocument(folder, "*");
BusinessService Usage

Here is an example of using BusinessService API (this example is the same previous one with Business Objects but this time using BusinessService):

import org.nuxeo.ecm.automation.client.adapters.BusinessService;
import org.nuxeo.ecm.automation.client.Session;

// A simple custom POJO client side
BusinessBean file = new BusinessBean("File", "File description", "File");

// Fetching the business service adapter
BusinessService<BusinessBean> businessService = session.getAdapter(BusinessService.class);

// Create server side injecting the POJO to the service method create
file = (BusinessBean) businessService.create(file, file.getTitle(), "/");

// Process to update with POJO
file.setTitle("Update");
file = (BusinessBean) businessService.update(file);
Contributing a New Custom Service Adapter

You can contribute your own adapter service by Java to encapsulate your own business logic.

To achieve this registration, you have to provide an adapter service factory that is going to instantiate your adapter. You can find an example of factory for the DocumentService class: DocumentServiceFactory (source code on GitHub).

Here is an example of registration:

HttpAutomationClient client = new HttpAutomationClient("http://localhost:8080/nuxeo/site/automation");

// Register you own service factory
client.registerAdapter(new MyServiceFactory());

// And then retrieve your custom adapter service
MyService myService = session.getAdapter(MyService.class);

Automation Client in OSGi Environment

Nuxeo Automation Client is OSGi compliant.

Example to Get Automation Client in an OSGi Environment

(Here we're using Apache Felix Framework)

import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import javax.inject.Inject;

@Inject
BundleContext bundleContext;

// Example with Felix ServiceReference
ServiceReference serviceReference = bc.getServiceReference(AutomationClientFactory.class.getName());

// Fetch the Automation Client Factory
AutomationClientFactory factory = (AutomationClientFactory) bundleContext.getService(serviceReference);

// Get the Automation Client with given URL of you server
AutomationClient client = factory.getClient(new URL("http://localhost:8080/nuxeo/site/automation"));

// Get client with http connection timeout as parameter in milliseconds
client = factory.getClient(new URL("http://localhost:8080/nuxeo/site/automation"), 3600);

MANIFEST.MF information declared into the Automation Client bundle

Bundle-SymbolicName: org.nuxeo.ecm.automation.client

Bundle-Activator: org.nuxeo.ecm.automation.client.jaxrs.impl.AutomationClientActivator

Export-Package: org.nuxeo.ecm.automation.client.adapters;version="0.0.
 0.qualifier",org.nuxeo.ecm.automation.client;version="0.0.0.qualifier
 ",org.nuxeo.ecm.automation.client.model;version="0.0.0.qualifier"

Import-Package: javax.activation,javax.mail;version="1.4",javax.mail.i
 nternet;version="1.4",org.apache.commons.lang;version="2.6",org.apach
 e.commons.logging;version="1.1",org.apache.http;version="4.2",org.apa
 che.http.auth;version="4.2",org.apache.http.client;version="4.2",org.
 apache.http.client.methods;version="4.2",org.apache.http.conn;version
 ="4.2",org.apache.http.entity;version="4.2",org.apache.http.impl.clie
 nt;version="4.2",org.apache.http.impl.conn.tsccm;version="4.2",org.ap
 ache.http.params;version="4.2",org.apache.http.protocol;version="4.2"
 ,org.codehaus.jackson;version="1.8",org.codehaus.jackson.map;version=
 "1.8",org.codehaus.jackson.map.annotate;version="1.8",org.codehaus.ja
 ckson.map.deser;version="1.8",org.codehaus.jackson.map.introspect;ver
 sion="1.8",org.codehaus.jackson.map.module;version="1.8",org.codehaus
 .jackson.map.type;version="1.8",org.codehaus.jackson.type;version="1.
 8",org.nuxeo.ecm.automation.client;version="0.0",org.nuxeo.ecm.automa
 tion.client.adapters;version="0.0",org.nuxeo.ecm.automation.client.mo
 del;version="0.0",org.osgi.framework;version="1.5"

5 days ago manonlumeau Added content-review-lts2017 label
a month ago manonlumeau NXDOC-1346-FT review screenshot
a year ago Manon Lumeau 79
2 years ago Vladimir Pasquier 78
2 years ago Vincent Dutat 77
2 years ago Ronan Daniellou 76 | Added block how to update multi-lines properties.
3 years ago Solen Guitter 75 | Format
3 years ago Maxime Hilaire 74
3 years ago Vladimir Pasquier 73
3 years ago Vladimir Pasquier 72 | Reverted from v. 70
3 years ago Vladimir Pasquier 71
3 years ago Solen Guitter 70 | Fix links to point to latest version
3 years ago Harlan Brown 69
4 years ago Solen Guitter 68
4 years ago Solen Guitter 67 | Updated links, formatted steps
4 years ago Alain Escaffre 65
4 years ago Alain Escaffre 66
4 years ago Solen Guitter 64
4 years ago Solen Guitter 63
4 years ago Vladimir Pasquier 62
4 years ago Vladimir Pasquier 61
4 years ago Vladimir Pasquier 60
4 years ago Vladimir Pasquier 59
4 years ago Vladimir Pasquier 58
4 years ago Vladimir Pasquier 57
4 years ago Vladimir Pasquier 55
4 years ago Vladimir Pasquier 56
4 years ago Thibaud Arguillere 54
4 years ago Vladimir Pasquier 53
4 years ago Vladimir Pasquier 52
4 years ago Vladimir Pasquier 51
4 years ago Vladimir Pasquier 50
4 years ago Vladimir Pasquier 49
4 years ago Vladimir Pasquier 48
4 years ago Vladimir Pasquier 47
4 years ago Vladimir Pasquier 46
4 years ago Vladimir Pasquier 45
4 years ago Vladimir Pasquier 44
4 years ago Vladimir Pasquier 43
4 years ago Vladimir Pasquier 42
4 years ago Vladimir Pasquier 41
4 years ago Vladimir Pasquier 40
4 years ago Vladimir Pasquier 39
4 years ago Vladimir Pasquier 38
4 years ago Vladimir Pasquier 37
4 years ago Solen Guitter 36 | Added TOC
4 years ago Vladimir Pasquier 35
4 years ago Vladimir Pasquier 34
4 years ago Vladimir Pasquier 33
4 years ago Brendan Coveney 32
5 years ago Antoine Taillefer 31
5 years ago Antoine Taillefer 30
5 years ago Antoine Taillefer 29
6 years ago Julien Carsique 27
6 years ago Julien Carsique 28 | Migrated to Confluence 4.0
6 years ago Julien Carsique 26
6 years ago Stéphane Lacoin 25
6 years ago Olivier Grisel 24 | fix automation client download link
6 years ago Solen Guitter 23
7 years ago Julien Carsique 22
7 years ago Julien Carsique 21 | Add download link from http://maven.nuxeo.org/
7 years ago Stéphane Lacoin 20
7 years ago Stéphane Lacoin 19
7 years ago Stéphane Lacoin 18
7 years ago Stéfane Fermigier 17
7 years ago Stéfane Fermigier 15
7 years ago Stéfane Fermigier 16
7 years ago Stéfane Fermigier 14
7 years ago Bogdan Stefanescu 13
7 years ago Bogdan Stefanescu 12
7 years ago Bogdan Stefanescu 11
7 years ago Bogdan Stefanescu 10
7 years ago Bogdan Stefanescu 9
7 years ago Bogdan Stefanescu 8
7 years ago Bogdan Stefanescu 7
7 years ago Bogdan Stefanescu 6
7 years ago Bogdan Stefanescu 5
7 years ago Bogdan Stefanescu 4
7 years ago Bogdan Stefanescu 3
7 years ago Bogdan Stefanescu 2
7 years ago Bogdan Stefanescu 1
History: Created by Bogdan Stefanescu