I recently had the opportunity to play with CMIS AtomPub bindings, in collaboration with our partners in Canada, as they were building a VBScript API to access a Nuxeo repository via the CMIS standard from a Microsoft-based environment. I had no concrete idea of what CMIS really was, but I had read a few articles about this “greatest common factor of document management concepts between all vendors,” as Florent tweeted once ;-).
I don’t intend, in this blog post, to give a complete description of CMIS, or its AtomPub bindings. There are good posts and tutorials for this. I want to explain a few points that were not initially straightforward to me, hoping that this will accelerate your first experience with CMIS. You should not be confronted with raw XML, as we’ll see in this post, as long as you use mainstream languages: java, c#, python, etc. Indeed, there are or will be libraries to let you handle plain objects and methods. But the following approach will be very useful if you want to interface your document repository with a legacy application written in a not-so-common language that doesn’t benefit from projects like Chemistry, or if you just want to understand some basics of CMIS.
Let’s start with a few concepts:
- in CMIS, entities are called objects and are described in XML using the Atom syntax. You’ll find in the Atom entry a full description of an object (properties, linked objects, URLs, etc.)
- each object and main collection of objects are tied to an HTTP URL. You will process information with respect to the semantic of the HTTP method used to communicate with the server:
- GET is used to retrieve information on the object
- PUT is used to update an object
- POST to create
- DELETE to delete
- CMIS server gives you, in its Atom answers, URLs to get or process all the information you need. You just have to parse the answer, looking for “rel” links that give you URLs to download attached files, children, renditions, etc. You don’t have to obtain the information from anywhere else but the server, which allows you to develop highly interoperable code.
For the operations that come next, you need to deploy the Chemistry Libraries and the Nuxeo-Chemistry implementation on your Nuxeo instance (or be sure that you have any other CMIS-compliant server and adapt the lengthening of the URLs).
To send an HTTP request to the server, I will use “curl.” You can also use “wget”, or any other HTTP requester. In some of the requests, I need to send XML file content (most often an Atom entry). You will find up-to-date samples of those XML files in the chemistry source code; they are very instructive. I also attached to this blog post the Atom entries I used in the following samples.
Let’s start with the concrete experiment :)
The initial URL, to get information on the repository will be, from a shell:
curl -uAdministrator:Administrator
"http://cmis.demo.nuxeo.org/nuxeo/site/cmis/repository"
It mainly gives:
- the capabilities of the repository
- the URL of the CMIS collections (types, children of the root, query)
- URL patterns that are accepted by the server, in order to get information on an object, to make a query, etc.
Here is an extract of the server’s answer:
<?xml version='1.0' encoding='UTF-8'?>
<service xmlns="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200908/"
xmlns:cmisra="http://docs.oasis-open.org/ns/cmis/restatom/200908/">
<workspace>
<title xmlns="http://www.w3.org/2005/Atom" type="text">default</title>
<cmisra:repositoryInfo>
<cmis:repositoryId>default</cmis:repositoryId>
...
<cmis:rootFolderId>4fb1b8a1-6dfd-4da4-95b0-4ef41c27b920</cmis:rootFolderId>
<cmis:latestChangeLogToken></cmis:latestChangeLogToken>
<cmis:capabilities>
<cmis:capabilityACL>none</cmis:capabilityACL>
<cmis:capabilityAllVersionsSearchable>true</cmis:capabilityAllVersionsSearchable>
<cmis:capabilityChanges>none</cmis:capabilityChanges>
<cmis:capabilityContentStreamUpdatability>anytime</cmis:capabilityContentStreamUpdatability>
<cmis:capabilityGetDescendants>false</cmis:capabilityGetDescendants>
...
</cmis:capabilities>
...
</cmisra:repositoryInfo>
<collection href="http://cmis.demo.nuxeo.org/nuxeo/site/cmis/typechildren">
<title xmlns="http://www.w3.org/2005/Atom" type="text">Types</title>
<cmisra:collectionType>types</cmisra:collectionType>
</collection>
<collection href="http://cmis.demo.nuxeo.org/nuxeo/site/cmis/query">
<title xmlns="http://www.w3.org/2005/Atom" type="text">query
collection</title>
<cmisra:collectionType>query</cmisra:collectionType>
</collection>
<collection
href="http://cmis.demo.nuxeo.org/nuxeo/site/cmis/children/4fb1b8a1-6dfd-4da4-95b0-4ef41c27b920">
<title xmlns="http://www.w3.org/2005/Atom" type="text">children
collection</title>
<cmisra:collectionType>root</cmisra:collectionType>
</collection>
...
<cmisra:uritemplate>
<cmisra:type>objectbyid</cmisra:type>
<cmisra:mediatype>application/atom+xml;type=entry</cmisra:mediatype>
<cmisra:template>http://cmis.demo.nuxeo.org/nuxeo/site/cmis/object/{id}?filter={filter}&renditionFilter={renditionFilter}&includeRelationships={includeRelationships}&includeAllowableActions={includeAllowableActions}&includePolicyIds={includePolicyIds}&includeACL={includeACL}</cmisra:template>
...
</workspace>
</service>
You need to look for the children root collection URL to start browsing following the repository hierarchy.
curl -uAdministrator:Administrator
"http://cmis.demo.nuxeo.org/nuxeo/site/cmis/children/4fb1b8a1-6dfd-4da4-95b0-4ef41c27b920"
Doing so, you will receive a feed of atom entries which is actually the collection of children of the root. Here is the atom entry associated to “default domain” in Nuxeo:
<entry xmlns:cmisra="http://docs.oasis-open.org/ns/cmis/restatom/200908/">
<id>urn:uuid:4593d0e5-fa7f-4ae1-9472-3030c270bb1e</id>
<title type="text">default-domain</title>
<updated>2010-01-11T11:49:20.036Z</updated>
<author>
<name>system</name>
</author>
<summary type="text">default-domain</summary>
<link href="http://cmis.demo.nuxeo.org/nuxeo/site/cmis/repository"
rel="service" type="application/atomsvc+xml" />
<link
href="http://cmis.demo.nuxeo.org/nuxeo/site/cmis/object/4593d0e5-fa7f-4ae1-9472-3030c270bb1e"
rel="self" type="application/atom+xml; type=entry" />
<link
href="http://cmis.demo.nuxeo.org/nuxeo/site/cmis/object/4593d0e5-fa7f-4ae1-9472-3030c270bb1e"
rel="edit" type="application/atom+xml; type=entry" />
<link
href="http://cmis.demo.nuxeo.org/nuxeo/site/cmis/object/4593d0e5-fa7f-4ae1-9472-3030c270bb1e"
rel="alternate" type="application/atom+xml; type=entry" />
<link href="http://cmis.demo.nuxeo.org/nuxeo/site/cmis/type/Domain"
rel="describedby" type="application/atom+xml; type=entry" />
<link
href="http://cmis.demo.nuxeo.org/nuxeo/site/cmis/object/4fb1b8a1-6dfd-4da4-95b0-4ef41c27b920"
rel="up" type="application/atom+xml; type=entry" />
...
<cmisra:object>
<cmis:properties>
<cmis:propertyString propertyDefinitionId="cmis:path"
localName="def:path" displayName="Path">
<cmis:value>/default-domain</cmis:value>
</cmis:propertyString>
<cmis:propertyId propertyDefinitionId="cmis:objectTypeId"
localName="def:typeid" displayName="Type ID">
<cmis:value>Domain</cmis:value>
</cmis:propertyId>
<cmis:propertyString propertyDefinitionId="content_roots"
localName="def:nx:content_roots" displayName="content_roots" />
<cmis:propertyString propertyDefinitionId="dc:description"
localName="def:nx:dc:description" displayName="dc:description">
<cmis:value>Nuxeo 5 default domain</cmis:value>
</cmis:propertyString>
<cmis:propertyString propertyDefinitionId="cmis:lastModifiedBy"
localName="def:lastmodifiedby" displayName="Last Modified By">
<cmis:value>system</cmis:value>
</cmis:propertyString>
<cmis:propertyString propertyDefinitionId="cmis:createdBy"
localName="def:createdby" displayName="Created By">
<cmis:value>system</cmis:value>
</cmis:propertyString>
<cmis:propertyString propertyDefinitionId="dc:subjects"
localName="def:nx:dc:subjects" displayName="dc:subjects" />
<cmis:propertyString propertyDefinitionId="dc:title"
localName="def:nx:dc:title" displayName="dc:title">
<cmis:value>Default domain</cmis:value>
</cmis:propertyString>
<cmis:propertyString propertyDefinitionId="icon"
localName="def:nx:icon" displayName="icon">
<cmis:value>/icons/domain.gif</cmis:value>
</cmis:propertyString>
<cmis:propertyString propertyDefinitionId="cmis:name"
localName="def:name" displayName="Name">
<cmis:value>default-domain</cmis:value>
</cmis:propertyString>
<cmis:propertyString propertyDefinitionId="dc:contributors"
localName="def:nx:dc:contributors" displayName="dc:contributors">
<cmis:value>system</cmis:value>
</cmis:propertyString>
<cmis:propertyDateTime propertyDefinitionId="cmis:creationDate"
localName="def:creationdate" displayName="Creation Date">
<cmis:value>2010-01-11T12:49:20+01:00
</cmis:value>
</cmis:propertyDateTime>
...
</cmis:properties>
</cmisra:object>
</entry>
What is this Atom entry? It is the representation of the state of the object whose ID is 4593d0e5-fa7f-4ae1-9472-3030c270bb1e, transferred by the CMIS server (on top of our Nuxeo repository). You will find mainly four parts in it:
- the Atom header, with elements id, title, updated, summary.
- the content field (when it is “document”, not a “folder”) or an “alternate” element, that gives a URI to a remote content
- the links associated to the document that give the URLs to request to perform operations on the object, such as service, self (the URL of the object), edit (to modify the object), alternate, described, up, down (we will see later the use of those links).
- the cmisra object description, and among them you will find the custom type property values.
For some operations (like create or update metadata), and depending on how rigorous the implementation is, you need to be sure that:
- the id element is not null
- the title element is not null
- the author or source is not null
- either alternate link or content is not null
If you miss one of those rules, your request will be rejected. Now let’s go with basic operations.
To browse the repository, use the _up_ and down links:
curl -uAdministrator:Administrator "http://cmis.demo.nuxeo.org/nuxeo/site/cmis/children/4593d0e5-fa7f-4ae1-9472-3030c270bb1e"
In the result of the previous command, where I asked for the children of the “Default domain” node, I looked for the string “workspaces” to get the id of the workspaces object. I can then send a request to get the children of the workspaces node, etc.
To create a folder: Let’s suppose you want to create a folder “myfolder” under the object of id “A”, you will do a POST to the URL of the object that represents the collection of the children of “A”. You get this URL by looking for the URL of type rel=”down”, and whose content-type is application/atom+xml;type=feed. Take care not to take the “tree” object, which is of type application/cmistree+xml and returns the hierarchy of descendants.
curl -X POST -uAdministrator:Administrator "http://cmis.demo.nuxeo.org/nuxeo/site/cmis/children/5af586f4-6221-4ad5-8bc6-7c34d247bc2b" -H "Content-Type:application/atom+xml;type=entry" -d @ $localh_path/createfolder.atomentry.xml
Note the header variables. If you forget to mention them, your request will not be processed successfully. The result of the previous request is an Atom entry corresponding to the newly created object. Note also that I specified in the Atom entry the Nuxeo custom type I wanted to use: “Workspace”:
<cmisra:object> <cmis:properties> <cmis:propertyId propertyDefinitionId="cmis:objectTypeId"> <cmis:value>Workspace</cmis:value> </cmis:propertyId> </cmis:properties> </cmisra:object>
I can check, by browsing Nuxeo DM, that my folder has been created.
Document creation: To create a document with content in this folder, there are basically two methods:
- either you create an empty document (without the binary file, just the metadata) and then you update the content using the edit-media link (two requests).
- or you create the document with the content in the Atom entry (content element) encoded in base64 (one request only).Let’s go with the first one. You need to prepare your Atom entry (basically get the one used for folder creation, and modify cmis:objectTypeId element with value “File”). Note that if you choose cmis:folder, or cmis:document, the Nuxeo implementation will map to Folder and File types. Also, the summary element is mapped to dc:description, title to the document name (and dc:title). The ID value you put in that case is not very important (not taken into account), but it should not be null.
curl -X POST -uAdministrator:Administrator "http://cmis.demo.nuxeo.org/nuxeo/site/cmis/children/191c4778-6f8a-4dc0-9733-6e39725366f9" -H "Content-Type:application/atom+xml;type=entry" -d @$PATH/createdocument.atomentry.xml
In the resulting Atom entry, look for the link edit-media. You will be able to upload a file through that URL :
curl -X PUT -uAdministrator:Administrator "http://cmis.demo.nuxeo.org/nuxeo/site/cmis/file/a0fe9d4f-1805-4c3d-b69c-eb44dd70d25f" -H "Content-Type:application/pdf" -H "Slug:NuxeoStudioOverview.pdf" -d @$PATH/NuxeoStudioOverview.pdf
Note here the Slug header parameter, to give the file name, as well as the Content-Type header parameter.
Metadata update: To update your document’s metadata, you need to PUT an Atom entry on the edit URL of the object, adding the values of the metadata you want to update under the element cmis:properties of the Atom entry describing the object. Let’s say you want to update the title and to enter the “language” metadata of the previously created document. Metadata “language” belongs to the Dublin Core schema in Nuxeo.Here is the most interesting part of the updatedocument.atomentry.xml Atom entry:
<cmisra:object> <cmis:properties> <cmis:propertyId propertyDefinitionId="cmis:objectTypeId"> <cmis:value>File</cmis:value> </cmis:propertyId> <cmis:propertyString propertyDefinitionId="dc:title"> <cmis:value>Change the title of the document using CMIS </cmis:value> </cmis:propertyString> <cmis:propertyString propertyDefinitionId="dc:language"> <cmis:value>EN</cmis:value> </cmis:propertyString> </cmis:properties> </cmisra:object>
Note under cmis:properties you find only the desired metadata plus the cmis:objectTypeId property, which is mandatory to avoid an error (ndlr:this should be verified).
curl -X PUT -uAdministrator:Administrator "http://cmis.demo.nuxeo.org/nuxeo/site/cmis/object/a0fe9d4f-1805-4c3d-b69c-eb44dd70d25f" -H "Content-Type:application/atom+xml;type=entry" -d @$path/updatedocument.atomentry.xml
Document deletion:
To delete the document, you can use the self URL in combination with DELETE method:``` curl -X DELETE -uAdministrator:Administrator "http://cmis.demo.nuxeo.org/nuxeo/site/cmis/object/a0fe9d4f-1805-4c3d-b69c-eb44dd70d25f" ```
To delete a non-empty folder, you need to perform the delete on the tree object, so you need to use the down link that returns an application/cmistree+xml object:
``` curl -X DELETE -v -uAdministrator:Administrator "http://cmis.demo.nuxeo.org/nuxeo/site/cmis/descendants/67df3f01-a4e5-4173-b685-88742cf22471" ```