CMIS (Content Management Interoperability Services) is a vendor-neutral OASIS Web services interface specification that enables interoperability between Enterprise Content Management (ECM) systems. CMIS allows rich information to be shared across Internet protocols in vendor-neutral formats, among document systems, publishers and repositories, in a single enterprise and between companies.
Content Services fully implements both the CMIS 1.0 and 1.1 standards to allow your application to manage content and metadata in a repository. This section gives a brief overview of the URL format for CMIS ReST API calls, and explains the format of responses.
You can use basic HTTP methods to invoke CMIS methods, or you can use one of the many language-specific libraries that wrap CMIS. One such example for the Java language is the OpenCMIS Client API provided by the Apache Chemistry project. Apache Chemistry provides client libraries for many other languages such as Python, PHP, and .NET. The OpenCMIS library is covered in this section.
You can use methods described by both CMIS 1.0 and 1.1 in the same application, although in practice it is advisable to write all new applications to the latest 1.1 specification.
- CMIS basics: CMIS is built around a number of concepts. This information provides an overview of those that are shared between all CMIS versions.
- Content Services configuration settings: Information about repository configuration related to CMIS.
- Getting started with the AtomPub binding (XML): CMIS 1.0 introduces the XML based AtomPub binding.
- Getting started with the Browser binding (JSON): CMIS 1.1 introduces a number of new concepts that are supported by Alfresco. You can now use the new browser binding to simplify flows for web applications, use Alfresco aspects, and use the append data support to manage large items of content.
- Working with the CMIS API from Java: Introduction to the OpenCMIS Java library that wraps the CMIS ReST API.
CMIS basics
CMIS is built around a number of concepts. This information provides an overview of those that are shared between all CMIS versions.
- CMIS repository: At the root of the CMIS model and services is a repository, which is an instance of the content management system and its store of metadata, content, and indexes.
- CMIS query: A CMIS query is based upon SQL-92. The query is read-only and presents no data manipulation capabilities.
- CMIS services: CMIS provides services that you can access using SOAP or AtomPub, depending on your preferred architectural style.
- CMIS object model: The CMIS object model is similar to the Alfresco object model without the support of aspects. It supports versioning, policy, document, and folder objects.
- CMIS bindings: Clients can communicate with a CMIS repository using one of three protocol bindings: AtomPub, SOAP Web Services, and in CMIS 1.1, the Browser bindings. CMIS repositories provide a service endpoint, or URL, for each of these bindings.
CMIS repository
At the root of the CMIS model and services is a repository, which is an instance of the content management system and its store of metadata, content, and indexes.
The repository is the end point to which all requests are directed. In the RESTful model, it is the root path of the resources being addressed in CMIS. The repository is capable of describing itself and its capabilities.
CMIS query
A CMIS query is based upon SQL-92. The query is read-only and presents no data manipulation capabilities.
The syntax consists of the following clauses:
SELECT
with a target listFROM
with the object types being queriedJOIN
to perform a join between object typesWHERE
with the predicateIN
andANY
to query multi-value propertiesCONTAINS
to specify a full-text qualificationIN_FOLDER
andIN_TREE
to search within a folder hierarchyORDERBY
to sort the results
The CMIS query maps the object type into a relational structure where object type approximates a table, the object
approximates a row, and the property approximates a column that can be multi-valued. You can query the actual binary
content using a full text query and folder path information using the in_folder
and in_tree
functions.
A query can also be paged for user interface presentation.
CMIS services
CMIS provides services that you can access using SOAP or AtomPub, depending on your preferred architectural style.
CMIS services include the following:
- Repository services let you discover available repositories, get the capabilities of these repositories, and provide basic Data Dictionary information of what types are available in the repository.
- Navigation services let you navigate the repository by accessing the folder tree and traversing the folder/child hierarchy. You can use these services to get both children and parents of an object.
- Object services provide the basic CRUD (Create, Read, Update, Delete) and Control services on any object, including document, folder, policy, and relationship objects. For document objects, this includes setting and getting of properties, policies, and content streams. Object services retrieve objects by path or object ID. Applications may also discover what actions users are allowed to perform.
- Multi-filing services let you establish the hierarchy by adding or removing an object to or from a folder.
- Discovery services provide Query and Change services, and a means of paging the results of the query.
- Change services let you discover what content has changed since the last time checked, as specified by a special token. You can use Change services for external search indexing and replication services.
- Versioning services control concurrent operation of the Object services by providing Check In and Check Out services. Version services also provide version histories for objects that are versioned.
- Relationship services let you create, manage, and access relationships or associations between objects as well as allow an application to traverse those associations.
- Policy services apply policies on document objects. Policies are free-form objects and can be used by implementations for security, record, or control policies.
- ACL services let you create, manage, and access Access Control Lists to control who can perform certain operations on an object.
CMIS object model
The CMIS object model is similar to the Alfresco repository object model without the support for aspects. It supports versioning, policy, document, and folder objects.
CMIS supports object types that define properties associated with each type. Each object has an object type, properties defined by that object type, and an object ID.
Object types support inheritance and are sub-typed as document object types and folder object types. Document object types can have content streams to store and access binary data. Object types can also be related through relationship object types.
CMIS policy object
A policy object represents an administrative policy that can be enforced by a repository, such as a retention management policy.
An Access Control List (ACL) is a type of policy object. CMIS allows applications to create or apply ACLs. The Alfresco repository also uses policy objects to apply aspects.
CMIS document object
Document objects have properties and content streams for accessing the binary information that is the document, properties that can be multi-valued, and versions.
Document objects can also have renditions that represent alternate file types of the document. Only one rendition type, a thumbnail, is well defined.
CMIS versioning
Versioning in CMIS is relatively simple to encompass the various versioning models of different CMIS implementations.
Each version is a separate object with its own object ID. For a given object ID, you can retrieve the specific version, the current version, or all versions of the object, as well as delete specific or all versions of a Document object. Document versions are accessed as a set of Document objects organized on the time stamp of the object. CMIS does not provide a history graph.
CMIS folder object
Document objects live in a folder hierarchy. As in the Alfresco repository, a folder can exist in another folder to create the hierarchy. The relationship between a folder and document is many-to-many if the repository supports multi-filing, allowing a document to appear in more than one folder. Otherwise, it is one-to-many relationship.
CMIS bindings
Clients can communicate with a CMIS repository using one of three protocol bindings: AtomPub, SOAP Web Services, and in CMIS 1.1, the Browser bindings. The CMIS repositories provide a service endpoint, or URL, for each of these bindings.
AtomPub binding
This RESTful binding is based on the Atom Publishing Protocol. Clients communicate with the repository by requesting the service document, which is obtained through a well-known URI. In Content Services, the service document is at:
http://<hostname>:<port>/alfresco/api/-default-/public/cmis/versions/1.1/atom
Response format is XML.
Web service binding
This binding is based on the SOAP protocol All services and operations defined in the CMIS domain model specification are present in the Web Services binding. You can get a summary of the CMIS services from Alfresco from the following URL:
http://<hostname>:<port>/alfresco/cmisws
Response format is XML.
Browser binding
From version 1.1 of the specification, CMIS provides a simpler JSON-based binding. The browser binding is designed for web applications, and is easy to use with HTML and JavaScript. It uses just two verbs, GET and POST, and resources are referenced using simple and predictable URLs. You can get a summary of the repository information from Alfresco from the following URL:
http://<hostname>:<port>/alfresco/api/-default-/public/cmis/versions/1.1/browser
Response format is JSON.
Content Services configuration settings
It is possible to configure the way that CMIS requests are processed by adding property settings in the
alfresco-global.properties
file.
Change the default file limit
The default limit for the length of a file to upload is 4GB (4096MB).
To change this limit, for example to 5GB (5120MB), add the following property:
opencmis.maxContentSizeMB=5120
To ignore the size check, use the following property setting:
opencmis.maxContentSizeMB=-1
Change the memory threshold
The default threshold for memory is 4MB (4096KB). This sets the size threshold for content kept in memory. Documents bigger than this threshold will be cached in a temporary directory.
To change threshold, for example to 5MB (5120KB), add the following property:
opencmis.memoryThresholdKB=5120
To ignore the memory threshold, use the following property setting:
opencmis.memoryThresholdKB=-1
Getting started with the AtomPub binding (XML)
To get you started with CMIS AtomPub binding, review the format of the URL you will use, and what responses to expect.
Note: If you are accessing an on-premise instance, the term repository means the same thing in Content Services and CMIS.
What does a request look like?
You call a method on the CMIS ReST API by issuing an authenticated HTTP request with a URL.
The four HTTP methods are used to support the traditional Create, Read, Update, and Delete (CRUD) operations of content management when using the AtomPub binding:
- POST: is used to create a new entities
- GET: is used to retrieve information
- PUT: is used to update a single entity
- DELETE: is used to delete a single entity
Each request is a URL with a specific format. The format is dependent on the type of target repository.
For an on-premise Content Services repository it looks as follows for the AtomPub binding (CMIS 1.0):
https://localhost:8080/alfresco/api/-default-/public/cmis/versions/1.0/atom/content?id=a99ae2db-0e40-4fb6-bf67-3f331a358cfc
Each request URL is made up of the following elements:
- The protocol, which can be
http
orhttps
. - The hostname. This will be the host and port number of your alfresco instance. So if your Alfresco instance is running on the local machine on port 8080 this will be
localhost:8080
. - The fixed string
-default-
. - The API you want to call. In this case it is the public Alfresco CMIS API identified as
/public/cmis
. /versions/n
. This specifies the version of the CMIS API you are using.1.1
or1.0
.- The CMIS binding. Alfresco supports the
atom
binding for the CMIS 1.0 protocol, and both theatom
andbrowser
bindings for the CMIS 1.1 protocol. - The CMIS method itself. In this case the request is to get the content of a CMIS document with a specific id.
Getting the service document
The capabilities available to your application from an instance of an on-premise Content Services are described in an AtomPub service document returned when calling the base URL. The service document contains information on the repository, the CMIS methods that can be called on it, and the parameters for those methods.
To retrieve the service document use the HTTP GET method with this URL:
https://localhost:8080/alfresco/api/cmis/versions/1.1/atom/
The response body is an AtomPub XML document which describes the CMIS capabilities in a standard way. See the CMIS specification for more details.
Getting information on a node
You can get information on a specific node in the repository by using its id
. The resulting AtomPub XML document
describes the node. You can tailor the information returned by providing HTML parameters.
Here is an example of a URL to retrieve information on a specific node in a Content Services on-premise instance:
https://localhost:8080/alfresco/api/-default-/public/cmis/versions/1.1/atom/id?id=5dba1525-44a6-45ed-a42e-4a155a3f0539
The response body is an AtomPub XML document which describes the CMIS capabilities in a standard way. See the CMIS specification for more details.
You can add the following optional HTTP parameters to the URL:
Parameter | Default value | Description |
---|---|---|
filter | Repository specific | A comma-separated list of query names that defines which properties must be returned by the repository. |
includeAllowableActions | false |
A boolean value. A value of true specifies that the repository must return the allowable actions for the node. |
includeRelationships | IncludeRelationships.NONE |
The relationships in which the node participates that must be returned in the response. |
renditionFilter | cmis:none |
A filter describing the set of renditions that must be returned in the response. |
includePolicyIds | false |
A boolean value. A value of true specifies the repository must return the policy ids for the node. |
includeAcl | false |
A boolean value. A value of true specifies the repository must return the Access Control List (ACL) for the node. |
Getting the children of a node
You can get the children of a specific node in the repository by using its id
. The resulting AtomPub XML document
describes children of the node. You can tailor the information returned by providing HTML parameters. You can use this
method to navigate a folder tree in the repository.
Here is an example of a URL to retrieve the children of a specific node in a Content Services on-premise instance:
https://localhost:8080/alfresco/api/-default-/public/cmis/versions/1.1/atom/children?id=5dba1525-44a6-45ed-a42e-4a1a1a3f0539
The response body is an AtomPub XML document which describes the child nodes in a standard way. See the CMIS specification for more details.
You can add the following optional HTTP parameters to the URL:
Parameter | Default value | Description |
---|---|---|
filter | Repository specific | A comma-separated list of query names that defines which properties must be returned by the repository. |
orderBy | Repository specific | A comma-separated list of query names that defines the order of the results set. Each query name in the list must be followed by the string ASC or DESC to specify the direction of the order, ascending or descending. |
includeAllowableActions | false |
A boolean value. A value of true specifies that the repository must return the allowable actions for each node. |
includeRelationships | IncludeRelationships.NONE |
The relationships in which each node participates that must be returned in the response. |
renditionFilter | cmis:none |
A filter describing the set of renditions that must be returned in the response. |
includePathSegment | false |
A boolean value. A value of true returns a path segment in the response for each child object that can be used to construct that object’s path. |
maxItems | Repository specific | The maximum number of items to return in the response. |
skipCount | 0 |
The number of objects to skip over before returning any results. |
Getting the contents of a document
You can get the contents of a specific document in the repository by using its id
. The format of the URl and the
parameters that you can use are detailed in the service document.
Here is an example of a URL to retrieve the contents of a specific document in a Content Services on-premise instance:
https://localhost:8080/alfresco/api/-default-/public/cmis/versions/1.1/atom/content?id=824ba7cd-dcee-4908-8917-7b6ac0611c97
The response body is the content of the document. The format is specific to the type of content, so for example, getting the contents of a text document returns a text response body.
Updating the contents of a document
You can replace the contents of a specific document in the repository by using its id
. The format of the URl and the
parameters that you can use are detailed in the service document.
Here is an example of a URL to update the contents of a specific document in a Content Services on-premise instance:
https://localhost:8080/alfresco/api/-default-/public/cmis/versions/1.1/atom/content?id=824ba7cd-dcee-4908-8917-7b6ac0611c97
The request Content-Type
must be of the same mime-type as the target document. In this example, we are updating a
plain text document.
Content-Type: text/plain; charset=utf-8
The request body is the new content of the document.
Some updated text.
If the request is successful an HTTP CREATED
response (status 201) is returned.
Getting started with the browser binding (JSON)
CMIS 1.1 introduces a number of new concepts that are supported by the Alfresco repository. You can now use the new browser binding to simplify flows for web applications, use Alfresco aspects, and use the append data support to manage large items of content.
In addition to the existing XML-based AtomPub and Web services bindings, CMIS 1.1 provides a simpler JSON-based binding. The browser binding is designed for web applications and is easy to use just with HTML and JavaScript. It uses just two verbs, GET and POST, and resources are referenced using simple and predictable URLs.
You reference content in the repository by using the two URLs returned by the getRepositories
or getRepositoryInfo
service:
rootFolderUrl
repositoryUrl
Objects can then be referenced in two ways:
- By their ID:
{rootFolderUrl}?objectId={objectId}
- By their path:
{rootFolderUrl}/{object path}
Content that is independent of a folder, for example a Type definition, can be accessed using the repositoryUrl
service: {repositoryUrl}?cmisselector={selector}
Getting content
You use the HTTP GET command with parameters to retrieve content from a repository.
Use the cmisselector
parameter to define which content you want returned on a resource. For example if you want the
children of an object:
cmisselector=children
The URL to get all of the children of the root/test node in the repository looks like this:
http://localhost:8080/alfresco/api/-default-/public/cmis/versions/1.1/browser/root/test?cmisselector=children
All content will be returned as JSON by default.
In some cases you might want to request data from a server in a different domain, this is normally prohibited by web browsers due to their same origin policy.
CMIS 1.1 uses the callback
parameter to return JSONP.
This format also known as JSON with padding returns JavaScript code. It is evaluated by the JavaScript interpreter,
not parsed by a JSON parser. You use the callback
parameter to provide a JavaScript function to cope with the
returned JSONP.
For example the following function would write repository information into an HTML page:
<script type="text/javascript">
function showRepositoryInfo(repositoryInfo) {
for (repId in repositoryInfo) {
var ri = repositoryInfo[repId];
document.write("<h1>Information</h1>");
document.write("<ul>");
document.write("<li>ID..."
+ ri.repositoryID+"</li>");
document.write("<li>Name..."
+ ri.productName+"</li>");
document.write("<li>Description..."
+ ri.productVersion);
document.write("</li>");
document.write("</ul>");
}
}
</script>
The following function would invoke the CMIS URL GET with the callback function showRepositoryInfo
.
<script type="text/javascript"
src="/alfresco/api/-default-/public/cmis/versions/1.1/browser?callback=showRepositoryInfo">
</script>
The JSONP returned would look like this:
showRepositoryInfo (
{"-default-":{
”vendorName":”Alfresco",
”productName" : ”Alfresco Enterprise”,
"productVersion": "4.2.0 (r56201)“
}
}
)
Creating content
You use the HTTP POST command to create, update, and delete content from a repository. In an application a user would use an HTML form in a browser.
You use the cmisaction
element to control the action. So for example to create a document you would set
cmisaction=createDocument
.
You define other CMIS properties as form elements for example: propertyId[0]… propertyValue[0]
.
You define the content stream for a create or an update using the file
input form element:
<input id="content” type="file”
The form shows an example of a document create command:
<form id="cd1" action="http://localhost:8080/alfresco/api/…" method="post">
<table>
<tr>
<td><label for="name">Name:</label></td>
<td><input name="propertyValue[0]" type="text" id="name”/></td>
<td><input id="content" name="Browse" type="file" height="70px" size="50"/></td>
</tr>
</table>
<input id="cd" type="submit" value="Create Document"/></td>
<input name="propertyId[0]" type="hidden" value="cmis:name" />
<input name="propertyId[1]" type="hidden" value="cmis:objectTypeId" />
<input name="propertyValue[1]" type="hidden" type="text" id="typeId" value="cmis:document"/> </td>
<input name="cmisaction" type="hidden" value="createDocument" />
</form>
The form action URL is more specifically put together as follows. To create the document directly under /Company Home use:
<form id="cd1" action="http://localhost:8080/alfresco/api/browser/root" method="post">
And to store the document in a specific folder specify the folder path as the display path leaving out /Company Home:
<form id="cd1" action="http://localhost:8080/alfresco/api/browser/root/MyFolder" method="post">
Compact JSON return values
The JSON returned on a browser binding call includes type and property definitions, which can be quite large. Your
application might not need this information. You can use succinct
to produce more compact responses. succinct
is
expressed as a parameter on HTTP GET calls and as a control on HTTP POST calls.
In the following example the succint
parameter is used on an HTTP GET call to retrieve information on some children
of the Presentations folder in the test site. Specifying succint
reduces the size of the returned JSON significantly.
http://localhost:8080/alfresco/api/-default-/public/cmis/versions/1.1/browser/root/sites/test/documentLibrary/Presentations?cmisselector=children&succinct=true
Using aspects
Alfresco aspects are exposed as secondary types in CMIS 1.1. You can dynamically add aspects to an Alfresco object using the API.
You add an aspect to an object by updating the cmis:secondaryObjectTypeIds
property with the Type Id of the Aspect.
You can add and set an aspect in the same call.
cmis:secondaryObjectTypeIds
is an array of strings, each of which is an aspect type, for example, dublinCoreAspect
.
Appending content
In some applications such as journaling, or when using very large files, you want to upload a file in chunks. You might
have large files that time out during an upload, or fail because of a bad connection. You can use the CMIS 1.1 append
parameter in these situations.
You can use the isLastChunk
parameter to indicate to the server that the chunked data is complete. The following
example puts a chunk of data to a specific existing Alfresco object:
http://localhost:8080/alfresco/api/-default-/public/cmis/versions/1.1/atom/content?id=915b2b00-7bf6-40bf-9a28-c780a75fbd68&append=true
CMIS Item support
You can use cmis:item
to query some Content Services object types, and your own custom types, that are outside the
CMIS definitions of document, folder, relationship, or policy.
You can find a user, or a set of users, via a CMIS query. For example, the following query will return all information for all users:
SELECT * FROM cm:person
The following query will return the selected fields for users with names like “smith” and “smithers” all users:
SELECT cm:userName, cm:homeFolder FROM cm:person where cm:userName like 'smi%'
Working with the CMIS API from Java
The Apache Chemistry project provides a Java API called OpenCMIS that wraps the CMIS ReST API. It contains a number of libraries that abstract the CMIS low-level protocol bindings. OpenCMIS is the library used by Java developers. It provides an abstraction layer on top of all the CMIS protocol bindings, the AtomPub binding, the Web Service binding, and the Browser binding.
To use the OpenCMIS library, we need to first configure it in the Maven POM file’s dependency section, open the pom.xml
file in your Maven Java project, and add the following (check that you are using the latest version of the library):
<project ...
<dependencies>
...
<!-- Bring in the OpenCMIS library for talking to CMIS servers -->
<dependency>
<groupId>org.apache.chemistry.opencmis</groupId>
<artifactId>chemistry-opencmis-client-impl</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>
</project>
Before we start working with the repository we must create a session and connect to it. In your CMIS client class, such
as CmisClient
, add the following Hash map that will contain active sessions:
public class CmisClient {
private static Map<String, Session> connections = new ConcurrentHashMap<String, Session>();
public CmisClient() { }
}
The Session
interface is from the org.apache.chemistry.opencmis.client.api
package in the OpenCMIS library.
It represents a session/connection for a specific user with the CMIS repository. A session holds the configuration
settings and cache settings to use across multiple calls to the repository. The session is also the entry point to
perform all operations on the repository, such as listing folders, creating documents and folders, finding out the
capabilities of the repository, and searching.
To create a new connection with the repository, use the Session Factory interface and query it for all the available
repositories, and then create a new session for one of them. We will create a new getSession
method in the CmisClient
class to do the job as follows:
public Session getSession(String connectionName, String username, String pwd) {
Session session = connections.get(connectionName);
if (session == null) {
System.out.println("Not connected, creating new connection to" +
" Alfresco with the connection id (" + connectionName + ")");
// No connection to Alfresco available, create a new one
SessionFactory sessionFactory = SessionFactoryImpl.newInstance();
Map<String, String> parameters = new HashMap<String, String>();
parameters.put(SessionParameter.USER, username);
parameters.put(SessionParameter.PASSWORD, pwd);
parameters.put(SessionParameter.ATOMPUB_URL, "http://localhost:8080/alfresco/api/-default-/cmis/versions/1.1/atom");
parameters.put(SessionParameter.BINDING_TYPE, BindingType.ATOMPUB.value());
parameters.put(SessionParameter.COMPRESSION, "true");
parameters.put(SessionParameter.CACHE_TTL_OBJECTS, "0");
// If there is only one repository exposed (e.g. Alfresco),
// these lines will help detect it and its ID
List<Repository> repositories = sessionFactory.getRepositories(parameters);
Repository alfrescoRepository = null;
if (repositories != null && repositories.size() > 0) {
System.out.println("Found (" + repositories.size() + ") Alfresco repositories");
alfrescoRepository = repositories.get(0);
System.out.println("Info about the first Alfresco repo [ID=" +
alfrescoRepository.getId() + "][name=" +
alfrescoRepository.getName() + "][CMIS ver supported=" +
alfrescoRepository.getCmisVersionSupported() + "]");
} else {
throw new CmisConnectionException("Could not connect to the Alfresco Server, " +
"no repository found!");
}
// Create a new session with the Alfresco repository
session = alfrescoRepository.createSession();
// Save connection for reuse
connections.put(connectionName, session);
} else {
System.out.println("Already connected to Alfresco with the " +
"connection id (" + connectionName + ")");
}
return session;
}
This method starts off by checking if the Hash map already has a connection available for the connection identifier
passed in. We don’t want to create a new connection for every call that we do to the repository. If there is no connection,
we will use the SessionFactoryImpl
class to create a new SessionFactory
interface, which we can use to get a list of
repositories for the CMIS server.
A CMIS server can provide more than one repository, so we need to tell the server about which one we want to talk to.
This is usually done by passing in a repository ID. All OpenCMIS operations require a repository ID parameter. However,
there is one operation named getRepositories
that doesn’t, so it is used to get a list of the available repositories.
When the repository information is fetched from the server, we pass in a map of configuration parameters that tells
OpenCMIS what username and password to use to connect to the CMIS server, what protocol binding to use underneath OpenCMIS,
and so on.
We are connecting to Content Services, and it only provides one repository, so we can grab the first Repository
object
in the repositories list and use it to create a session/connection. The Repository
object provides information about
the repository, such as its ID, name, and the version of CMIS it supports. In case of Alfresco, the ID is -default-
,
and if running with the older AtomPub URL, it will be a universally unique identifier (UUID) that looks something like
f0ebcfb4-ca9f-4991-bda8-9465f4f11527
.
Now add the following code to the main()
method in your project:
public static void main(String[] args) {
CmisClient cmisClient = new CmisClient();
String connectionName = "martinAlf01";
Session session = cmisClient.getSession(connectionName, "admin", "admin");
}
Now that we have a connection/session to the Alfresco server it’s time to start calling different CMIS endoints from Java.
One of the first things we might want to do is to get a list of all the content in the top folder in the repository,
referred to as /Company Home in Alfresco. The top folder is referred to as the root folder in CMIS. To get the root
folder and then a listing of its content, add the following code in a new method named listTopFolder
:
public void listTopFolder(Session session) {
Folder root = session.getRootFolder();
ItemIterable<CmisObject> contentItems= root.getChildren();
for (CmisObject contentItem : contentItems) {
if (contentItem instanceof Document) {
Document docMetadata = (Document)contentItem;
ContentStream docContent = docMetadata.getContentStream();
System.out.println(docMetadata.getName() + " [size=" +
docContent.getLength()+"][Mimetype=" +
docContent.getMimeType()+"][type=" +
docMetadata.getType().getDisplayName()+"]");
} else {
System.out.println(contentItem.getName() + "[type="+contentItem.getType().getDisplayName()+"]");
}
}
}
Now we probably want to create content. Let’s start by creating a folder, which is easy, just get a Folder
object for
the parent folder in which you want to create a new folder and then use the createFolder
method on the parent folder
object as in the following code:
public Folder createFolder(Session session) {
String folderName = "OpenCMISTest";
Folder parentFolder = session.getRootFolder();
// Make sure the user is allowed to create a folder
// under the root folder
if (parentFolder.getAllowableActions().getAllowableActions().
contains(Action.CAN_CREATE_FOLDER) == false) {
throw new CmisUnauthorizedException(
"Current user does not have permission to create a " +
"sub-folder in " + parentFolder.getPath());
}
// Check if folder already exist, if not create it
Folder newFolder = (Folder) getObject(session, parentFolder, folderName);
if (newFolder == null) {
Map<String, Object> newFolderProps = new HashMap<String, Object>();
newFolderProps.put(PropertyIds.OBJECT_TYPE_ID, "cmis:folder");
newFolderProps.put(PropertyIds.NAME, folderName);
newFolder = parentFolder.createFolder(newFolderProps);
System.out.println("Created new folder: " + newFolder.getPath() +
" [creator=" + newFolder.getCreatedBy() + "][created=" +
date2String(newFolder.getCreationDate().getTime()) + "]");
} else {
System.out.println("Folder already exist: " + newFolder.getPath());
}
return newFolder;
}
Here we are creating the new folder under the root folder, which is represented by the / path, and is the same as
/Company Home in Alfresco. Before we go ahead and create the folder, we first check if the current user is authorized
to create a subfolder under the root folder. We can do this by getting the allowed actions on the root folder.
If they contain the canCreateFolder
action, we can go ahead and create the folder. If not, then we throw an unauthorized
runtime exception that will stop execution. This is actually the same exception that will be thrown by the OpenCMIS library
if we do not check anything before creating the folder with an unauthorized user.
When we know we are allowed to create a folder, we call a custom method named getObject
, which we will define in a second.
This method will return a Folder
object if it can find it, or null if it can’t. If the folder was not found, it will be
created via the createFolder
method.
The createFolder
method takes a map of metadata that should be set for the new folder. The name and type of the folder
are mandatory properties, so this is the minimum metadata we can use to create a folder.
The createFolder
method returns a new CMIS object that represents the newly created folder, which we can use in future
methods to create documents in it and to log some information about the new folder.
Before we can run the code, we need to implement the getObject
method as follows:
private CmisObject getObject(Session session, Folder parentFolder, String objectName) {
CmisObject object = null;
try {
String path2Object = parentFolder.getPath();
if (!path2Object.endsWith("/")) {
path2Object += "/";
}
path2Object += objectName;
object = session.getObjectByPath(path2Object);
} catch (CmisObjectNotFoundException nfe0) {
// Nothing to do, object does not exist
}
return object;
}
The getObject
method is quite useful as it can be used to easily get a CMIS object, such as a Folder
or
a Document
.
There is also the date2String
convenience method that we used to format the date, it’s implemented as follows and used
when printing date properties:
private String date2String(Date date) {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z").format(date);
}
After creating some folders, we probably want to create or upload documents to them. Creating a document, or file if you like, is almost the same as creating a folder. However, a document object can also contain content bytes in the form of a so-called content stream that represents the physical bytes of the file.
So, to create a document object with content, we first create a content stream object and then use that object when creating the document object as follows:
public Document createDocument(Session session, Folder parentFolder)
throws IOException {
String documentName = "OpenCMISTest.txt";
// Make sure the user is allowed to create a document
// in the passed in folder
if (parentFolder.getAllowableActions().getAllowableActions().
contains(Action.CAN_CREATE_DOCUMENT) == false) {
throw new CmisUnauthorizedException("Current user does not "+
"have permission to create a document in " +
parentFolder.getPath());
}
// Check if document already exist, if not create it
Document newDocument = (Document) getObject(session, parentFolder, documentName);
if (newDocument == null) {
// Setup document metadata
Map<String, Object> newDocumentProps =
new HashMap<String, Object>();
newDocumentProps.put(PropertyIds.OBJECT_TYPE_ID, "cmis:document");
newDocumentProps.put(PropertyIds.NAME, documentName);
// Setup document content
String mimetype = "text/plain; charset=UTF-8";
String documentText = "This is a test document!";
byte[] bytes = documentText.getBytes("UTF-8");
ByteArrayInputStream input = new ByteArrayInputStream(bytes);
ContentStream contentStream = session.getObjectFactory().createContentStream(
documentName, bytes.length, mimetype, input);
// Create versioned document object
newDocument = parentFolder.createDocument(
newDocumentProps, contentStream, VersioningState.MAJOR);
System.out.println("Created new document: " +
getDocumentPath(newDocument) + " [version=" +
newDocument.getVersionLabel() + "][creator=" +
newDocument.getCreatedBy() + "][created=" +
date2String(newDocument.getCreationDate().getTime())+"]");
} else {
System.out.println("Document already exist: " + getDocumentPath(newDocument));
}
return newDocument;
}
The new document should be created in the OpenCMISTest folder. To do this, we feed the folder reference into the
createDocument
method as follows:
Folder folder = cmisClient.createFolder(session);
Document document = cmisClient.createDocument(session, folder);
The following code implements the custom getDocumentPath
method used above. It’s handy in a lot of situations to get
the absolute repository path for a document::
private String getDocumentPath(Document document) {
String path2Doc = getParentFolderPath(document);
if (!path2Doc.endsWith("/")) {
path2Doc += "/";
}
path2Doc += document.getName();
return path2Doc;
}
What this method does is call another custom method named getParentFolderPath
to get the path for the parent folder
of the document object passed in. When it has this path, it checks if it ends in /, if not, it adds /
(if it is the root folder, it will end in slash as it is represented by /
). To complete the full path for the document,
it then adds the name of the document to the parent folder path and returns the result. The getParentFolderPath
method
is implemented as follows:
private String getParentFolderPath(Document document) {
Folder parentFolder = getDocumentParentFolder(document);
return parentFolder == null ? "Un-filed" : parentFolder.getPath();
}
This code just calls another custom method named getDocumentParentFolder
to get the parent Folder
object for the
passed in Document
object. It then checks if it is null
, which means that the document has not been filed/contained
in any folder and is in a state called unfiled
. If we have a parent folder object, we just return the absolute repository
path for it.
The getDocumentParentFolder
custom method is implemented as follows:
private Folder getDocumentParentFolder(Document document) {
// Get all the parent folders (could be more than one if multi-filed)
List<Folder> parentFolders = document.getParents();
// Grab the first parent folder
if (parentFolders.size() > 0) {
if (parentFolders.size() > 1) {
System.out.println("The " + document.getName() +
" has more than one parent folder, it is multi-filed");
}
return parentFolders.get(0);
} else {
System.out.println("Document " + document.getName() +
" is un-filed and does not have a parent folder");
return null;
}
}
A document can have multiple folders as parents (that is, multifiled
), so we start out by finding out what parents the
document have by calling getParents
on it. Then we grab the first parent in the list assuming that most document
objects will only be filed/contained in one folder. If it is multifiled
, we print out a message about that. If no
parent folders could be found for the document, then it is unfiled
and null
is returned as the document does not
have a parent folder.
This was a short introduction to working with OpenCMIS Java CMIS API. There are multiple books covering OpenCMIS and if you are going to work extensively with this API get one of those.
If you are wondering about how to work with Alfresco aspects using OpenCMIS see next section.
Working with Alfresco aspects from OpenCMIS
It’s possible to work with Alfresco aspects directly via OpenCMIS using CMIS secondary types.
Alfresco has two types of classes that can be used to classify content, types and aspects. A node in Alfresco (that is, a CMIS object) can have one and only one type set but zero or more aspects applied.
We can use so-called CMIS secondary types to manage the aspects for an object in Alfresco, as Alfresco exposes any aspects that are set on an object as secondary types.
This will work if you are running Alfresco 4.2.e Community, Alfresco 4.2.0 Enterprise, or newer versions. With earlier versions, you have to use a special Alfresco OpenCMIS extension to manage aspects.
When we want to manage aspects via CMIS secondary types, we will just use standard OpenCMIS library functions. Secondary
object types are managed in a specific multivalued property named cmis:secondaryObjectTypeIds
.
See this section for how to add aspects to a CMIS object, such as a folder or document.
See this section for how to remove aspects from a CMIS object.
Adding aspects to a document or folder
Aspects can be applied when creating or updating a document or folder.
To demonstrate how to add an aspect when we are creating an object, we will add one of the out-of-the-box Alfresco
aspects called Titled (cm:titled
) when we create a folder. This aspect, or the CMIS secondary type, requires two extra
properties to be filled in, title and description:
public void createFolderWithTitledAspect(Session session) {
String folderName = "OpenCMISTestTitled";
Folder parentFolder = session.getRootFolder();
// Check if folder already exist, if not create it
Folder newFolder = (Folder) getObject(session, parentFolder, folderName);
if (newFolder == null) {
List<Object> aspects = new ArrayList<Object>();
aspects.add("P:cm:titled");
Map<String, Object> newFolderProps = new HashMap<String, Object>();
newFolderProps.put(PropertyIds.OBJECT_TYPE_ID, "cmis:folder");
newFolderProps.put(PropertyIds.NAME, folderName);
newFolderProps.put("cmis:secondaryObjectTypeIds", aspects);
newFolderProps.put("cm:title", "Folder Title");
newFolderProps.put("cm:description", "Folder Description");
newFolder = parentFolder.createFolder(newFolderProps);
System.out.println("Created new folder with Titled aspect: " +
newFolder.getPath() + " [creator=" + newFolder.getCreatedBy()
+ "][created=" + date2String(newFolder.getCreationDate().getTime()) + "]");
} else {
System.out.println("Cannot create folder, it already exist: " +
newFolder.getPath());
}
}
For information on how to get a Session
object, getObject
method implementation, and date2String
method, see
this section.
Here we first check whether the folder we intend to create already exists. If it doesn’t, we go ahead and create a list
of aspects that we want to set for the folder object. In this case, it is just the one aspect called P:cm:titled
(P stands for policy; it’s the way Alfresco traditionally exposes aspects, and you still have to use this prefix),
but the cmis:secondaryObjectTypeIds
property is a multivalued property, so we need to keep the aspect name in a list.
Then the standard properties map is created where one of the properties is the cmis:secondaryObjectTypeIds
property,
keeping the list of aspects. The folder is then created with this map of properties, and the aspect is set for us and
exposed as a secondary type via CMIS.
If we already have an object and want to add an aspect to it, we can also use the cmis:secondaryObjectTypeIds
property
and update it via the updateProperties
operation. We are going to use another of Alfresco’s out-of-the-box aspects
called Effectivity (cm:effectivity
). It can be used to set a from date and a to date for an object, representing some
form of time period when the object is effective. To do this for a document object, do as follows:
public void addAspectToExistingDocument(Document document) {
String aspectName = "P:cm:effectivity";
// Make sure we got a document, and then add the aspect to it
if (document != null) {
// Check that document don't already got the aspect applied
List<Object> aspects = document.getProperty("cmis:secondaryObjectTypeIds").getValues();
if (!aspects.contains(aspectName)) {
aspects.add(aspectName);
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("cmis:secondaryObjectTypeIds", aspects);
properties.put("cm:from", new Date());
Calendar toDate = Calendar.getInstance();
toDate.add(Calendar.MONTH, 2);
properties.put("cm:to", toDate.getTime());
Document updatedDocument = (Document) document.updateProperties(properties);
System.out.println("Added aspect " + aspectName + " to " + getDocumentPath(updatedDocument));
} else {
System.out.println("Aspect " + aspectName + " is already applied to " + getDocumentPath(document));
}
} else {
System.out.println("Document is null, cannot add aspect to it!");
}
}
The document object that we want to apply the aspect to is passed to the method. We start by getting currently set
aspects, so we can see if the cm:effectivity
aspect is already set. We also need to keep a list of aspects that are
already set as we need to add them to the aspect list together with the new aspect. If we don’t include the aspects
that are already set, we will basically unset them when we update the properties.
For information on how to implement the getDocumentPath
method see this section.
Removing aspects from a document or folder
To remove aspects from an existing object, such as a document or folder, you must first get all aspects and then remove the unwanted ones from the list before updating.
If we have a document or folder, and we want to remove an aspect from it, then we can use the cmis:secondaryObjectTypeIds
property and update it via the updateProperties
operation. Let’s take an example where a document has the
out-of-the-box aspect called Effectivity (cm:effectivity
) applied, and we want to remove it. To do this for
a document object, do as follows:
public void removeAspectFromDocument(Document document) {
String aspectName = "P:cm:effectivity";
// Make sure we got a document, and then remove the aspect from it
if (document != null) {
// Check that document got the aspect applied
List<Object> aspects = document.getProperty("cmis:secondaryObjectTypeIds").getValues();
if (aspects.contains(aspectName)) {
aspects.remove(aspectName);
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("cmis:secondaryObjectTypeIds", aspects);
Document updatedDocument = (Document) document.updateProperties(properties);
System.out.println("Removed aspect " + aspectName + " from " + getDocumentPath(updatedDocument));
} else {
System.out.println("Aspect " + aspectName + " is not applied to " + getDocumentPath(document));
}
} else {
System.out.println("Document is null, cannot remove aspect from it!");
}
}
The document object that we want to remove the aspect from is passed into the method. We start by getting currently set
aspects, so we can make sure that the cm:effectivity
aspect is indeed set. We need to keep a list of all the aspects
that are already set, and which we want to keep when updating. There is no method to remove just one aspect, we need to
set all aspects that we want to keep when we update the properties.
Note that when you remove an aspect in this way, all the associated properties are removed as well automatically, in
this case cm:from
and cm:to
.
For information on how to implement the getDocumentPath
method see this section.
Using the CMIS Workbench with Alfresco
The CMIS Workbench is a CMIS desktop client for developers. It is a repository browser and an interactive test bed for the OpenCMIS client API.
- Download the CMIS workbench zip file from the Apache Chemistry website.
- Unpack the contents of the zip file to a new directory.
- Navigate to the directory and run the following command to install the workbench:
- Unix:
workbench.sh
- Windows:
workbench.bat
- Unix:
- During the installation:
-
In the URL field, enter the Alfresco CMIS URL:
http://localhost:8080/alfresco/api/-default-/cmis/versions/1.1/atom
Note: This URL has changed since Alfresco One 4.2.1.
For a Browser binding, use
http://localhost:8080/alfresco/api/-default-/cmis/versions/1.1/browser
. - Enter the username and password.
- Click Load Repositories.
- Click Login.
-
- In the CMIS workbench, check that you can connect to the repository by running CMIS functions such as creating, updating, and deleting folders.