Introducing the Alfresco Java Content Repository API

From AlfrescoWiki

Jump to: navigation, search

Back to JSR-170


Contents

[edit] Introduction

This article introduces you to the Alfresco implementation of the Java Content Repository API (aka JCR or JSR-170) by designing and developing a simple WIKI like back-end using both Level 1 and Level 2 JCR features. Some parts of this article may look familiar as it is heavily based on Introducing the Java Content Repository API held at IBM Developer Works written by Titus Barik.

It is assumed that the reader has some familiarity with Alfresco, in particular, how the Spring Framework is utilised, and how to build a client application with Alfresco. This is not essential however to get a feel for JCR usage.

[edit] JCR Concepts

Before diving into Alfresco specifics, it's worth familiarising yourself with the the JCR Repository Model.

You may also wish to browse the JCR API and specification found at http://www.jcp.org/en/jsr/detail?id=170.

[edit] Access to the Sample Source Code

Throughout this article, snippets of JCR example code are provided.

From v1.3 onwards: The example code is located in the Alfresco SDK within the sample project JCRSamples.

Pre v1.3: The example code is located in the Alfresco source code bundle, within the JCR project under the Java package org/alfresco/jcr/example.

The source includes:

  • WIKIExample.java - complete Java example containing all of the above code
  • wiki-context.xml - Spring context file for WIKI Example
  • wikiModel.xml - WIKI Model Definition
  • wikiImage.gif - WIKI Image

The above source code is also available from SVN.

For your reference, you may also wish to view this complete simple example.

[edit] Alfresco First Steps

JSR-170 does not dictate how to obtain a JCR Repository interface. With Alfresco, this is achieved by accessing the Spring Bean called "JCR.Repository".

From v1.3 onwards:

The JCR.Repository bean implementation is found in the jar file repository.jar whose Spring configuration is found in jcr-api-context.xml. Both files are available in the Alfresco SDK.

Pre v1.3:

The JCR.Repository bean implementation (and the rest of Alfresco's JCR layer) is found in the jar file alfresco-jcr.jar whose Spring configuration is found in jcr-context.xml.

Within the Alfresco source tree, these files are located in:

  • /root/projects/jcr/config/alfresco/jcr-context.xml
  • /root/projects/jcr/build/dist/alfresco-jcr.jar (only available after a build)

[edit] Repository Configuration

There is only one configuration parameter; the default workspace name. This is used when a JCR client performs a login without specifying the workspace.

From v1.3 onwards: Within jcr-api-context.xml you'll find...
Pre v1.3 onwards: Within jcr-context.xml you'll find...

<bean id="JCR.Repository" class="org.alfresco.jcr.repository.RepositoryImpl" init-method="init">
  <property name="serviceRegistry"><ref bean="ServiceRegistry"/></property>
  <property name="defaultWorkspace"><value>SpacesStore</value></property>
</bean>

Out-of-the-box, the default workspace is the one used by Alfresco's Web Client application called SpacesStore

[edit] Repository Access

The JCR Repository is accessed using standard Spring Framework approaches. Essentially, you need to get hold of the Repository interface of the Spring bean called "JCR.Repository" which can be achieved as follows.

[edit] Programmatically

From v1.3 onwards: The following snippet of code demonstrates how to retrieve the Spring Bean manually...

ApplicationContext context = new ClassPathXmlApplicationContext("classpath:alfresco/application-context.xml");
Repository repository = (Repository)context.getBean("JCR.Repository");

Note: A fundamental difference between v1.3 and prior releases is that it is no longer necessary to initialise the Spring context using a JCR specific context file. The standard application-context.xml may now be used giving access to both JCR and non-JCR api's. For backwards compatibility, jcr-context.xml, but this is essentially deprecated.

Pre v1.3: The following snippet of code demonstrates how to retrieve the Spring Bean manually...

ApplicationContext context = new ClassPathXmlApplicationContext("classpath:alfresco/jcr-context.xml");
Repository repository = (Repository)context.getBean("JCR.Repository");

[edit] Dependency Injection

The following snippets of configuration and code demonstrate how to inject the JCR Repository into your own Spring bean...

spring configuration:

<bean id="myBean" class="myBeanImplementation">
  <property name="JCRRepository"><ref bean="JCR.Repository"/></property>
  ...
</bean>

java class implementation:

public class myBeanImplementation
{
   Repository repository;
   
public void setJCRRepository(Repository repository) { this.repository = repository; }
... }

[edit] Repository Login

The JCR API provides a simple mechanism for login. Behind this API, Alfresco uses the authentication system of the repository which by default is Alfresco's own, but it could also be NTLM, LDAP or your own depending on how the repository has been configured.

SimpleCredentials credentials = new SimpleCredentials("admin",	"admin".toCharArray());
Session session = repository.login(credentials);

// now we're logged in, display some information about the root node Node rootNode = session.getRootNode(); System.out.println("Root node: path=" + rootNode.getPath() + ", type=" + rootNode.getPrimaryNodeType().getName());

If a workspace is not provided, the default as defined earlier will be used.

The Session returned from login is tied to the workspace and allows read and write upon that workspace.

Executing the above will result in...

Root node: path=/, type=sys:store_root

[edit] Transaction Management

Alfresco provides Transaction capabilities across all of its persistence based services. The JCR API is built upon these services and as such Alfresco provides a fully transactional JCR interface.

[edit] Implicit Transactions

By default, an Alfresco JCR Session is backed by a transaction. Work performed by the session is not committed until Session.save() is called. The typical interaction sequence is as follows...

// login and establish session (this starts a transaction)
Session session = repository.login(credentials);

// perform work within session Node node = session.getRootNode(); Node childNode = node.addNode(....); childNode.setProperty(...); ...
// save session (this commits the transaction) session.save();
// continue to perform more work on session and save again ...
// logout (this rolls back the transaction - unsaved items are lost) session.logout();

It's not just JCR calls that are bound to the transaction. Calls to native Alfresco service API's are also included. So, if you happen to mix JCR and Alfresco calls between Session.login() and Session.logout(), they are all be bound to the same transaction and controlled by the JCR session.

[edit] Explicit Transactions

It's also possible to explicitly control Transaction boundaries using Alfresco's Transaction API. Using this technique, a JCR client can control when JCR actions are committed.

An example of explicit transaction management follows...

UserTransaction trx = serviceRegistry.getTransactionService().getUserTransaction;
trx.begin();
try
{
   // login and establish session (explicit transaction started above is inherited)
   Session session = repository.login(credentials);
   
// perform session work ...
// save the session, but do not commit just yet session.save();
// perform more session work and save again (still no commit) ... session.save();
// now commit the transaction trx.commit(); } catch(Throwable e /* note: catch throwable for demonstration purposes only */) { // oh dear, an error, so rollback trx.rollback(); }

In the above example, the UserTransaction is retrieved via the serviceRegistry variable. The Service Registry is a component that provides access to all of Alfresco's services. It's a Spring bean too, so can be accessed programatically or via dependency injection.

[edit] WIKI Model and Overview

Now we've got past the first steps, it's time to start building our WIKI. We'll build a structure similar to the one outlined in the IBM Developer Works article. For reference, the structure is as follows...

Image:Wiki.jpg

To make things more interesting, we'll make some slight modifications which will allow us to see the above structure and content in Alfresco's Web Client application. First, we'll place our WIKI under a node called 'app:company_home' which is the root node of the Alfresco Web Client and secondly we'll use the node types 'cm:folder' and 'wiki:page'. Luckily, Alfresco already provides a definition of cm:folder, however, it doesn't (yet) provide a WIKI definition.

[edit] Defining the WIKI Content Type

Alfresco supports the notion of a custom data model which allows for the creation of new node types. Custom models are defined via the Data Dictionary.

Our WIKI type definition is as follows (wikiModel.xml):

<model name="wiki:wikimodel" xmlns="http://www.alfresco.org/model/dictionary/1.0">
   
<imports> <import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d" /> <import uri="http://www.alfresco.org/model/content/1.0" prefix="cm" /> </imports>
<namespaces> <namespace uri="http://www.alfresco.org/model/jcr/example/wiki/1.0" prefix="wiki" /> </namespaces>
<types>
<type name="wiki:page"> <title>WIKI Page</title> <parent>cm:content</parent> <properties> <property name="wiki:restrict"> <type>d:boolean</type> <default>false</default> </property> <property name="wiki:category"> <type>d:text</type> <multiple>true</multiple> </property> </properties> <mandatory-aspects> <aspect>cm:titled</aspect> </mandatory-aspects> </type>
</types>
</model>

Notes:

  1. The WIKI type is derived from Alfresco's out-of-the-box Content type
  2. For simplicity, we use a text field to represent the category (note that Alfresco supports a sophisticated classification mechanism)
  3. The titled aspect is applied to all WIKI pages which provides cm:title and cm:description properties

[edit] Registering the WIKI Model

Now we have a model, we need to register it with Alfresco. This is done by creating a new Spring context file as follows (wiki-context.xml)...

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>

<beans> <import resource="classpath:alfresco/jcr-context.xml" />
<bean id="wikiModel" parent="dictionaryModelBootstrap" depends-on="JCR.DictionaryBootstrap"> <property name="models"> <list> <value>org/alfresco/jcr/example/wikiModel.xml</value> </list> </property> </bean> </beans>

And in our client Java code we refer to our new context file...

ApplicationContext context = new ClassPathXmlApplicationContext("classpath:org/alfresco/jcr/example/wiki-context.xml");
Repository repository = (Repository)context.getBean("JCR.Repository");

[edit] WIKI Functionality

Lets get back to some JCR coding. We'll now start to implement some basic WIKI functionality which will demonstrate JCR content manipulation and query.

[edit] Adding Content

First, we'll create the WIKI structure as outlined above...

Node companyHome = rootNode.getNode("app:company_home");
Node encyclopedia = companyHome.addNode("wiki:encyclopedia", "cm:folder");

Node page1 = encyclopedia.addNode("wiki:entry", "wiki:page"); page1.setProperty("cm:title", "rose"); page1.setProperty("cm:content", "A rose is a flowering shrub."); page1.setProperty("wiki:category", new String[] {"flower", "plant", "rose"});
Node page2 = encyclopedia.addNode("wiki:entry", "wiki:page"); page2.setProperty("cm:title", "Shakespeare"); page2.setProperty("cm:content", "A famous poet who likes roses."); page2.setProperty("wiki:category", new String[] {"poet"});
session.save();

Note how we're using the types and properties defined in our custom WIKI model. Nodes can be removed by calling Node.remove() on the desired node.

[edit] Accessing Content

JSR-170 provides two methods for accessing nodes: traversal access and direct access. Traversal access involves walking the content tree using relative paths, while direct access allows you to jump directly to a node with either an absolute path or, if the node is referenceable, with a UUID.

Traversal access is available from any Node object and its methods Node.getNode() and Node.getProperty(). Using your Wiki topology, you can obtain the encyclopedia node from the root node with the following code:

Node encyclopedia = rootNode.getNode("app:company_home/wiki:encyclopedia");
System.out.println("Encyclopedia id: " + encyclopedia.getUUID());
Node direct = session.getNodeByUUID(encyclopedia.getUUID());

You can further traverse down to a property. For example, in the "rose" encyclopedia entry from the root node, assuming prior knowledge of the Wiki topology, you can traverse to a property like so:

Node entry1 = rootNode.getNode("app:company_home/wiki:encyclopedia/wiki:entry1");
String title = entry1.getProperty("cm:title").getString();
System.out.println("Entry 1 Title: " + title);
Calendar modified = entry1.getProperty("cm:modified").getDate();
System.out.println("Entry 1 Last modified: " + modified);

You can browse through all the Wiki entries by obtaining a NodeIterator that returns the children of a specific node:

// browse all wiki entries
NodeIterator entries = encyclopedia.getNodes();
while (entries.hasNext())
{
   Node entry = entries.nextNode();
   
System.out.println(entry.getName()); System.out.println(entry.getProperty("cm:title").getString()); System.out.println(entry.getProperty("cm:content").getString()); System.out.println(entry.getPath());
Property categoryProperty = entry.getProperty("wiki:category"); Value[] categories = categoryProperty.getValues(); for (Value category : categories) { System.out.println("Category: " + category.getString()); } }

[edit] Searching Content with XPath

As you've already seen, traversal and direct access require positional knowledge of the articles. A better way to obtain a specific entry is through the JCR's XPath search facility. Because the workspace model is a lot like an XML document in that it is a tree structure, XPath is an ideal syntax for finding nodes. XPath queries are performed through the QueryManager object. The process is similar to accessing records through JDBC, as you can see...

Workspace workspace = session.getWorkspace();
QueryManager queryManager = workspace.getQueryManager();
Query query = queryManager.createQuery("//app:company_home/wiki:encyclopedia/*[@cm:content = 'rose']", Query.XPATH);
QueryResult result = query.execute();
NodeIterator it = result.getNodes();
while (it.hasNext())
{
  Node n = it.nextNode();
  System.out.println(n.getName());
  System.out.println(n.getProperty("cm:title").getString());
  System.out.println(n.getProperty("cm:content").getString());
}

You can also perform more complex queries. For example, you could query for all entries whose contents contain the word rose:

Query query = queryManager.createQuery("//app:company_home/wiki:encyclopedia/*[jcr:contains(., 'rose')]", Query.XPATH);

[edit] Exporting and Importing Content

JSR-170 has made efforts to ensure portability across JCR implementations. One of the ways it promotes this portability is through the use of its standardized XML importing and exporting features. Content from a compliant vendor repository can easily be transferred to another compliant vendor repository through the use of these facilities. Another advantage of using XML for serialization is that the exported repository can be manipulated with traditional XML parsing tools. You can perform an export with only the three lines of code...

File outputFile = new File("systemview.xml");
FileOutputStream out = new FileOutputStream(outputFile);
session.exportSystemView("/app:company_home/wiki:encyclopedia", out, false, false);

The resulting XML file can be then transferred to another fresh repository...

File inputFile = new File("systemview.xml");
FileInputStream in = new FileInputStream(inputFile);
session.importXML("/", in, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
session.save();

[edit] Adding Binary Content

Until now, you have been using string values for properties. But JCR also supports other types, including booleans, dates, and long integers. We'll add another content node to our WIKI entry to store an image:

Node page1 = rootNode.getNode("app:company_home/wiki:encyclopedia/wiki:entry1");
Node contentNode = page1.addNode("wiki:image", "cm:content");
contentNode.setProperty("cm:title", "Wiki Image");
ClassPathResource resource = new ClassPathResource("org/alfresco/jcr/example/wikiImage.gif");
contentNode.setProperty("cm:content", resource.getInputStream());
session.save();

[edit] Advanced Capabilities

It's time to visit some more advanced capabilities.

[edit] Versioning

Alfresco's JCR implementation provides the following JCR Versioning capabilities: checkout, checkin, getVersionHistory and restore.

[edit] Enable versioning for a Node

To enable versioning for a particular node the following is issued:

Node entry1 = rootNode.getNode("app:company_home/wiki:encyclopedia/wiki:entry1");
entry1.addMixin("mix:versionable");

With Alfresco, when the transaction is commited, an initial version of the node is automatically taken and placed into the version history, if no subsequent checkin is made.

[edit] Checkout and Checkin

After applying the 'mix:versionable' aspect, a node remains writable. This allows changes to be made.

// update wiki meta-data and content
entry1.setProperty("wiki:restrict", true);
entry1.setProperty("cm:content", "A rose is a flowering shrub of the genus Rosa.");

To explicitly create a new version, the node is checked-in which also marks the node as read-only.

entry1.checkin();

A checkout is required to make any subsequent changes.

entry1.checkout();
entry1.setProperty("cm:title", "The Rose part 2");
entry1.checkin();

[edit] Browsing the Version History

To browse the entire version history for a given node, the following can be issued:

VersionHistory versionHistory = entry1.getVersionHistory();
VersionIterator versionIterator = versionHistory.getAllVersions();

// for each version, output the node as it was versioned while (versionIterator.hasNext()) { Version version = versionIterator.nextVersion(); NodeIterator nodeIterator = version.getNodes(); while (nodeIterator.hasNext()) { Node versionedNode = nodeIterator.nextNode(); System.out.println(" Version label: " + version.getName()); System.out.println(" created: " + version.getCreated().getTime()); System.out.println(" restrict: " versionedNode.getProperty("wiki:restrict").getString()); System.out.println(" content: " versionedNode.getProperty("cm:content").getString()); } }

The above would display something like...

Version: 1.1
 created: Mon Dec 05 15:44:35 GMT 2005
 restrict: true
 content: A rose is a flowering shrub of the genus Rosa.
Version: 1.0
 created: Mon Dec 05 15:44:31 GMT 2005
 restrict: false
 content: A rose is a flowering shrub.

Please note: As mentioned before, Alfresco does not yet support the full JCR Version API. Some calls on the Version History and Version interfaces outside of the above example still throw UnsupportedRepositoryException.

[edit] Permission Checks

Alfresco supports a sophisticated permissioning system where node level permissions are given to users, groups and owners. Permissions may be inherited through node hierarchies (such as folders). New permissions may be configured into the Repository.

The JCR API provides the ability to test for a given permission, but does not yet provide administration of permissions. However, the Alfresco PermissionService API allows for complete control over permissions.

[edit] Do I have Permission?

You can ask via JCR whether the Session (and therefore the logged-in user) has one or more of the following permissions for a given node:

  • add_node: If checkPermission(path, "add_node") returns quietly, has permission to add a node at path, otherwise permission is denied.
  • set_property: If checkPermission(path, "set_property") returns quietly, then this Session has permission to set (add or change) a property at path, otherwise permission is denied.
  • remove: If checkPermission(path, "remove") returns quietly, then this Session has permission to remove an item at path, otherwise permission is denied.
  • read: If checkPermission(path, "read") returns quietly, then this Session has permission to retrieve (and read the value of, in the case of a property) an item at path, otherwise permission is denied.

With this, we can check to see whether the currently logged-in user has read access to the WIKI page...

// check for JCR 'read' permission
session.checkPermission("app:company_home/wiki:encyclopedia/wiki:entry1", "read");

Note that JCR will throw an AccessDeniedException if the Session does not have the permission.

[edit] Custom Alfresco Permissions

To test beyond the four standard JCR permissions, you can just pass the Alfresco permission label to checkPermission...

// check for Alfresco 'Take Ownership' permission
session.checkPermission("app:company_home/wiki:encyclopedia/wiki:entry1", PermissionService.TAKE_OWNERSHIP);

[edit] Mixing JCR and Alfresco APIs

The JCR API covers a wide variety of Repository functions, however, there will always be scenarios where custom Alfresco capabilities are required. For these scenarios, it is possible to "drop-out" to Alfresco specific APIs and have them controlled in the same transaction.

For example, we can provide extra meta-data for WIKI content properties such as encoding and mimetype. Via Alfresco, this is achieved with the setProperty method NodeService Java API.

For example...

// access the page via JCR
Node rootNode = session.getRootNode();
Node entry1 = rootNode.getNode("app:company_home/wiki:encyclopedia/wiki:entry1");

// convert the JCR Node to an Alfresco Node Reference NodeRef nodeRef = JCRNodeRef.getNodeRef(entry1);
// retrieve the Content Property (represented as a ContentData object) NodeService nodeService = registry.getNodeService(); ContentData content = (ContentData)nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT);
// update the Mimetype content = ContentData.setMimetype(content, MimetypeMap.MIMETYPE_TEXT_PLAIN); nodeService.setProperty(nodeRef, ContentModel.PROP_CONTENT, content);
// save the change session.save();

Note the use of the helper class JCRNodeRef to convert a JCR Node to an Alfresco Node Reference.

[edit] Access via the Alfresco Web Client

Now, for some fun - we've done enough coding, it's time to visualise our WIKI nodes. We can now go into the Alfresco Web Client to view (and modify, of course) the WIKI structure created by our sample JCR client.

[edit] Web Client Configuration

The Web Client must first be configured to make it aware of our custom WIKI model.

[edit] Registering the WIKI model

First, copy wikiModel.xml (as created earlier) to /config/alfresco/extension.

Then rename /config/alfresco/extension/custom-model-context.xml.sample to custom-model-context.xml and edit this file to register the model by adding the following line to the list of models already present.

<value>alfresco/extension/wikiModel.xml</value>

[edit] Enabling View, Edit and Search of WIKI Pages

For reference, the following Web Client Guide may be of use.

From v1.3 onwards, the web client configuration process has changed slightly. Extensions to the web client are now independently specified in the file web-client-config-custom.xml which is placed into /alfresco/extension. Further details on this process may be found here.

Create the file web-client-config-custom.xml within /alfresco/extension with the following skeleton configuration...

<alfresco-config>
</alfresco-config>

First, we'll allow the view and edit of our custom WIKI page properties (i.e. restrict and category).

Add the following configuration within the <alfresco-config> element...

<config evaluator="node-type" condition="wiki:page">
  <property-sheet>
    <show-property name="wiki:restrict"/>
    <show-property name="wiki:category"/>
  </property-sheet>
</config>

Now, we'll allow the creation of new WIKI pages by adding the following configuration within the <alfresco-config> element...

<config evaluator="string-compare" condition="Custom Content Types">
  <content-types>
    <type name="wiki:page"/>
  </content-types>
</config>

Now, we'll allow the advanced search of WIKI pages by adding the following configuration within the <alfresco-config> element...

<config evaluator="string-compare" condition="Advanced Search">
  <advanced-search>
     <content-types>
        <type name="wiki:page" />
     </content-types>
     <custom-properties>
        <meta-data type="wiki:page" property="wiki:restrict" />
        <meta-data type="wiki:page" property="wiki:category" />
     </custom-properties>
  </advanced-search>
</config>

[edit] Viewing the WIKI Structure

Start the Alfresco server and log-in to the Alfresco Web Client. Within the 'Company Home' space you should see the 'WIKI Encyclopedia' space as created by WIKIExample.java. Within there, you'll see the WIKI pages. Viewing the page details will present the properties 'category' and 'restrict'.

[edit] Administrator Node Browser

You may also wish to view the raw nodes created by your JCR client via the Administrator Node Browser. This tool is available when logged in as Admin via the Administration Console. Or you can go directly to the Node Browser via the url:

http://localhost:8080/alfresco/faces/jsp/admin/store-browser.jsp

[edit] Appendix 1: Complete Simple Example

package org.alfresco.jcr.example;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.Repository;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;



/**
 * Simple Example that demonstrates login and retrieval of top-level Spaces
 * under Company Home.
 * 
 * @author David Caruana
 */
public class SimpleExample
{

    public static void main(String[] args)
        throws Exception
    {
        // Setup Spring and Transaction Service
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:alfresco/application-context.xml");
        
        // Retrieve Repository
        Repository repository = (Repository)context.getBean("JCR.Repository");

        // Login to workspace
        // Note: Default workspace is the one used by Alfresco Web Client which contains all the Spaces
        //       and their documents
        Session session = repository.login(new SimpleCredentials("admin", "admin".toCharArray()));

        try
        {
            // Retrieve Company Home
            Node root = session.getRootNode();
            Node companyHome = root.getNode("app:company_home");
            
            // Iterator through children of Company Home
            NodeIterator iterator = companyHome.getNodes();
            while(iterator.hasNext())
            {
                Node child = iterator.nextNode();
                System.out.println(child.getName());

                PropertyIterator propIterator = child.getProperties();
                while(propIterator.hasNext())
                {
                    Property prop = propIterator.nextProperty();
                    if (!prop.getDefinition().isMultiple())
                    {
                        System.out.println(" " + prop.getName() + " = " + prop.getString());
                    }
                }
            }
        }
        finally
        {
            session.logout();
            System.exit(0);
        }
        
    }
    
}