Using the JCR API

November 14, 2023

The topics on this page show you how to:

  • Perform CRUD operations on content using the JCR API. For example, you can create, update, and delete content on a node.
  • Automatically split nodes into subnodes for easier maintenance and improved performance. For example, you could automatically split news content into subnodes by year, month, and author.
  • Use property interceptors to transform property values before they are stored in the JCR. For example, you could use property interceptors to moderate text that is added to blogs or forums.

Using the JCR API

The main thing to understand is that in the JCR, everything happens in the context of a session (think of something like an hibernate session in case you are familiar with this framework). In the context of this session, you can create, read, update, and delete any content your session can access.

A session is opened by one user, on one workspace, in one language. This means that at anytime in the context of your session, you can only access content allowed to this user, that exists in this workspace (default or live) in this particular language (if no language is provided, you can only read non-internationalized properties) but you can still access the nodes.

So whenever you want to interact with the JCR, the first step is to get a session. If you are in an Action, then you receive this session as a parameter :

public ActionResult doExecute(HttpServletRequest req, RenderContext renderContext, Resource resource, JCRSessionWrapper session,
                              Map<String, List<String>> parameters, URLResolver urlResolver)

Otherwise, if you do not get the session directly, you can get it from a node (node.getSession()). Another way of getting a session if you develop some services is get it from the singleton JCRTemplate instance.

Once you have a session, you can manipulate your content. Remember that when creating a node, you need to first read its parent, then add the new node under this parent, set the properties on this child and then save the session.

Every time you change a node or property, remember to save the session before ending your process.

public void doSomething() {
    JCRTemplate.getInstance().doExecuteWithSystemSession(null,Constants.EDIT_WORKSPACE, Locale.ENGLISH,new JCRCallback() {
        public Object doInJCR(JCRSessionWrapper session) throws RepositoryException {
            JCRNodeWrapper node = session.getNode(“/sites/mySite/contents/news”);
            String nodeTitle = “My node”
            JCRNodeWrapper jcrNodeWrapper = node.addNode(nodeTitle, “jnt:news”);
            jcrNodeWrapper.setProperty(“jcr:title”, nodeTitle);
            jcrNodeWrapper.setProperty(“desc”, “my node content”);
            session.save();
            return null;
        }
    });
}

JCRTemplate.getInstance().doExecuteWithSystemSession(null,Constants.EDIT_WORKSPACE, Locale.ENGLISH,new JCRCallback() {

This line indicates that we want to execute a certain callback using a system session (no user in particular, but JCRTemplate allows to use any type of session), in the EDIT_WORKSPACE and in English.

JCRNodeWrapper node = session.getNode("/sites/mySite/contents/news");

This reads what will be our root node for this process.

JCRNodeWrapper jcrNodeWrapper = node.addNode(nodeTitle, "jnt:news");

We add a node under our root node, this node will be of type "jnt:news". As long as we do not save our session, this node only exist in our context and nobody knows about it. This creates an empty shell for our node and now we have to set some properties on it. This is done by calling setProperty methods on our newly created node.

session.save();

This saves the session and propagates our node to the JCR definitively where it can be read by others from now on.

To create a node call the add method on its parent, so the steps are:

  • Open the session
  • Get the parent node
  • Call the add method on the parent (do not forget to specify your nodetype)
  • Call setProperty on each properties that you want to add on your new node
  • Save the session

To delete a node call the remove method on this node, so the steps are :

  • Open the session
  • Get the node we want to delete
  • Call remove method on it
  • Save the session

To update a property on an existing node:

  • Open the session
  • Get the node we want to update
  • Call setProperty on each properties that you want to update
  • Save the session

Auto-splitting nodes

Automatic splitting feature allows for non-intrusive management of large amount of subnodes, based on the configured rules. It is mainly done for maintainability and performance reasons.

Good examples, where the auto-splitting is useful, are site news sections where auto-splitting helps creating news archives, for example, by year, month, author or splitting of user activities in Jahia social module.

Splitting can be enabled on a node by adding a jmix:autoSplitFolders mixin type and setting splitting configuration options and node type for split folders.

Split folders are created using the specified node type, for example, jnt:contentList.

Splitting configuration is a string with semicolon separated split folder settings in the form:

<folder>;<sub-folder>;<sub-sub-folder>

Each single split folder configuration is a string with comma-separated tokens that define how the folder name is determined. Following types of folder configuration are supported:

constant - specifies a predefined name for the split folder. The syntax is:

constant,<folder-name>

For example: constant,reports

property - the name of the folder is determined by the value of the corresponding node property. The syntax is:

property,<property-name>

For example: property,jcr:creator

Additionally the node name can be used if using property,jcr:nodename settings.

firstChars - the name is determined by the first characters of the corresponding property value. The syntax is:

firstChars,<property-name>,<character-count>

For example: firstChars,j:appID,3

substring - name is retrieved as a substring of the corresponding node property value. The syntax is:

substring,<property-name>,<start-position>-<end-position>

For example: substring,j:isbn,3-6. The start-position and end-position indexes are zero-based.

date - name is retrieved by applying the specified date pattern (using SimpleDateFormat) onto the value of the corresponding property. The syntax is:
date,<date-property-name>,<date-pattern>

For example: date,jcr:created,yyyy will create the split folder using the creation year.

As a example, if auto-splitting is enabled on a files node with the configuration:

property,jcr:creator;date,jcr:created,yyyy;date,jcr:created,MM

and split folder node type jnt:contentList.

In such a case the sub-node report.pdf, created in that folder by user sergiy of 1st or July 2020, will land under:

    files
        |_sergiy
               |_2020
                    |_07
                       |_report.pdf

Split folders sergiy, 2020 and 07 will be created automatically using jnt:contentList node type.

Java API

Using the Java API auto-splitting on a node can be enabled in the following way:

import org.jahia.services.content.JCRAutoSplitUtils;
...

JCRNodeWrapper node = session.getNode("/shared/files");
JCRAutoSplitUtils.enableAutoSplitting(node,
            "property,jcr:creator;date,jcr:created,yyyy;date,jcr:created,MM",
            "jnt:contentList");

The above given call enables auto-splitting of sub nodes in node myFolderName first by author (creator) than by year and at last by month, creating split "folders" of type jnt:contentList.

Rules

Jahia provides a rule consequence to enable auto-splitting on a node from within business rules. The following example enables auto-splitting by year and date for user activities (this example taken from Jahia Social Module):

rule "Auto-split user activities node on creation"
  salience 101
  when
    A new node "activities" is created
    The node has a parent
      - the parent has the type jnt:user
  then
    Enable auto-splitting for subnodes of the node into folders of type jnt:contentList using configuration "date,jcr:created,yyyy;date,jcr:created,MM"
end

 

Configuring and implementing property interceptors

Property interceptor catches all accesses to JCR node properties. They may execute an action when a property is stored in the JCR and when it is read from the JCR. It is also possible to make the property set fails if some checks fail.

Interceptors can transform the value before the storage into the JCR and/or after the property is read. They are system-wide and can be filtered base on the parent node and the property definition. Several examples of usage for an interceptor are:

  • Advanced filtering of text, for example, for automatic moderation of blog or forum posts
  • Auto-tagging and semantic text extraction
  • Custom URL rewriting in text properties

Interceptor mechanism

By default, interceptors are executed for every get and set operation on a property - but they can be disabled for a specific session. Currently interceptors are disabled when an unlocalized session is used.

When setting a property, the InterceptorChain.beforeSetValue() method is called before setting the value to the JCR. The chain will iterate on all declared interceptors, check if an interceptor needs to be called on the property, and will call the PropertyInterceptor.beforeSetValue(). The interceptor must return the property value, which is then passed to a next interceptor in the chain. If no exception has been thrown when all interceptors have been processed, the last value is set into the property.

The same chain is executed when getting a property - after the property is got from the JCR, RenderChain.afterGetValue() chains all interceptors starting from the last one to the first one, and calls PropertyInterceptor.afterGetValue() on each of them. The final value is returned to the caller.

Configuration

Core interceptors are declared in the JCRStoreService bean from the applicationContext-jcr.xml file. Custom interceptor can be declared as OSGi service in any module.

Implementing an interceptor

An interceptor needs to implement the org.jahia.services.content.interceptor.PropertyInterceptor interface. A base implementation class is provided for convenience: org.jahia.services.content.interceptor.BaseInterceptor that already supports some criteria, like node type, property required type, property name, or selector type.

Assuming we are implementing a filtering interceptor for forum or blog posts and comments. An interceptor in the following example is configured to be applied on text (String) properties of nodes with node type jnt:post. An implementation of the interceptor can look as follows (for the sake of simplicity the filtering logic is left out):

@Component(immediate = true)
public class PostFilteringInterceptor extends BaseInterceptor {
    @Activate
    public void start() {
        setRequiredTypes(Collections.singleton(“String”));
        setNodeTypes(Collections.singleton(“jnt:post”));
        jcrStoreService.addInterceptor(this);
    }
    @Deactivate
    public void stop() {
        jcrStoreService.removeInterceptor(this);
    }
    @Reference
    public void setJcrStoreService(JCRStoreService jcrStoreService) {
        this.jcrStoreService = jcrStoreService;
    }
    @Override
    public Value beforeSetValue(JCRNodeWrapper node, String name,
                                ExtendedPropertyDefinition definition, Value originalValue) throws RepositoryException {
        String content = originalValue.getString();
        if (content == null || content.length() == 0) {
            return originalValue;
        }
        String result = filter(content);
        return !result.equals(content) ? node.getSession().getValueFactory().createValue(result)
                : originalValue;
    }
    @Override
    public Value[] beforeSetValues(JCRNodeWrapper node, String name,
                                   ExtendedPropertyDefinition definition, Value[] originalValues)
            throws RepositoryException {
        Value[] res = new Value[originalValues.length];
        for (int i = 0; i < originalValues.length; i++) {
            Value originalValue = originalValues[i];
            res[i] = beforeSetValue(node, name, definition, originalValue);
        }
        return res;
    }
    private String filter(String content) {
        // TODO implement text filtering logic here
        return content;
    }
}