Manipulating content with APIs
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/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.getSesion()).
Another way of getting a session if you develop some services is to make SPRING inject the JCRTemplate class in your service.
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/property remember to save the session before ending your process.
An example of code using the JCRTemplate (this one is injected by SPRING)
private JCRTemplate jcrTemplate;
public void setJcrTemplate(JCRTemplate jcrTemplate) {
this.jcrTemplate = jcrTemplate;
}
public void doSomething() {
jcrTemplate.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.doExecuteWithSystemSession(null,Constants.EDIT_WORKSPACE, Locale.ENGLISH,new JCRCallback() {
This line indicate 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 read 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, nobody knows about it. This create an empty shell for our node, now we have to set some properties on it. This is done by calling setProperty methods on our newly created node.
session.save();
This save the session and propagate 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 we want to add on our 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 we want to update
- Save the session
Legacy REST API
Jahia provides a REST API, mostly for rendering, but some other services are available as well such: login, logout, content creation & updating and searching.
Note please, when using the REST API from your client-side code (browser side) in your external Web application, e.g. using jQuery library, you can run into a cross-domain restriction for Ajax requests as they are not allowed by default due to security concerns. In contrary, using REST API from a server side code in your external application does not have this restriction.
LOGIN
URL : /cms/login
Method : POST
Parameters :
Name | Mandatory | Description |
---|---|---|
username | yes | The username of the user that you want to login |
password | yes | The user's password |
redirectActive | no | If set to "true" or not set, the login servlet will redirect to the home page after successful authentication. If you are using AJAX calls you should set this to "false". |
redirect | no | Allows you to specify another URL to redirect after successful login |
restMode | no | Set this to true if you are performing a login from an AJAX call login |
LOGOUT
URL : /cms/logout
Method : POST
Name | Mandatory | Description |
---|---|---|
redirectActive | no | If set to "true" or not set, the login servlet will redirect to the home page after successful authentication. If you are using AJAX calls you should set this to "false". |
redirect | no | Allows you to specify another URL to redirect after successful login |
RENDER
The render servlet can either be used to render content by accessing it with a GET method, or can also be used to add content to different workspaces by using a POST method and special parameters. You can also delete content using DELETE method, or update content using POST or PUT.
HTTP Get Request
You can simply render a node by calling it in the following form :
URL : /cms/render/WORKSPACE/LANGUAGE/CONTENT_PATH(.VIEW).TEMPLATETYPE
where: * WORKSPACE : 'default' for the edit workspace, 'live' for the live (production) workspace * LANGUAGE : any language defined on the site * CONTENT_PATH : the full path to the content object to be rendered * VIEW : this is optional, specified the (alternative) view to render the content node. This depends on the available views for the content node. * TEMPLATE-TYPE : usually 'html' but you might want to use other template types such as 'json' (see more below)
JSON rendering
Jahia comes with a few default views for JSON rendering, but here we will detail the two most important ones : 'full' and 'nodetype'. The 'full' view will generate a JSON view of a content node, along with all it's properties and child node names. It also takes optional query string parameters to expand the output:
depthLimit : (default value = 1). The depth of children to output with the current node. You can use this value to output deeper tree of children, but be aware that this will make your HTTP requests slower as more data will be output and transmitted over the network.
escapeColon : (default value = false) If activated, this will replace the colon (':') character present in property names with an underscore, making it a lot easier to use Javascript code that accesses properties.
withType : (default value = false) If activated, it will, along with the property data and child nodes, also output the node type information for the node. As this will generate a lot more data for this request, it is also possible to use the 'nodetype' view to generate this output seperately. Also, the 'nodetype' view also takes parameters that can be directly called on the 'full' view but we will not repeat them here for the sake of brievety (see below for the query string parameters and their documentation).
prettyPrint : (default value = false) By default the JSON rendering will be as compact as possible, which makes it difficult to read by a human, by enabling this setting it will render a nicely indented output.
Full JSON View example query:
http://localhost:8080/cms/wise/default/en/sites/docspaces.full.json?withType=true&depthLimit=0&escapeColon=true&withHidden=true&withProtected=true
Nodetype view
The 'nodetype' view will generate a JSON output of the node type's information, making it possible for example to dynamically generate forms to create a new node. By default it will not output any hidden or protected properties or child node definitions, but this may be overridden with the following query string parameters:
withHidden
: (default value = false) If activated, it will also output the hidden properties and child node definitions for the nodewithProtected
: (default value = false) If activated, it will also output the protected properties and child node definitions for the nodedepthLimit
: (default value = 1) By default the JSON view will render the current node's nodetype, along with the child definitiond and their nodetypes, but you can change the depth limit to either 0 to output only the nodetype of the current node, or to a higher value to recursively output the types of the children of children, and so on. Of course the deeper the depth and the larger the result tree, so be careful not setting this value too high if not really needed.nodeType
: (default value = none) By default the rendered node type (and mixin types) will be of the node specified in the path, but it might be interesting to be able to query any node type registered in the system. You can do this by specified this query parameter, in which case the node type and mixin types of the node specified in the path will simply be ignored and the node type generated will be the one specified in the query string parameter instead. Be careful with the encoding of the parameter, you will have to URL encode the colon (':') character with it's URL equivalent, so for example :nt:base
will become
nt%3Abase
-
prettyPrint
: (default value = false) By default the JSON rendering will be as compact as possible, which makes it difficult to read by a human, by enabling this setting it will render a nicely indented output.
Full JSON View examples query
Basic example requesting the node type of the 'docspaces' node including all the hidden and protected properties and child definitions:
http://localhost:8080/cms/wise/default/en/sites/docspaces.nodetype.json?withHidden=true&withProtected=true
Requesting a specific nodetype, ignoring the 'docspaces' node type and mixin types:
http://localhost:8080/cms/wise/default/en/sites/docspaces.nodetype.json?nodeType=nt%3Abase
HTTP Post Request
URL : /cms/render/WORKSPACE/LANGUAGE/CONTENT_PATH
Method : POST
Request headers : see response format section below.
Name | Mandatory | Description |
---|---|---|
jcrNodeType | yes | Define the type of node you want to create. |
jcrNodeName | no | If defined we will create a node with this name (or use it as a prefix for the name. If not define you can call : /cms/render/WORKSPACE/LANGUAGE/CONTENT_PATH/* To create a subnode at the specified path using the nodetype for name. |
jcrNewNodeOutputFormat | no | Define at which format the newly created node will be returned, after the form submission (non-AJAX) is performed and a redirect is sent back to the browser. This value is used as a file extension at the end of the URL. The "html" value is the default value, but "json" or any other view type you have defined in your templates is possible. |
jcrReturnContentType | no | If you are expecting a JSON result to the REST call, you need to either set the "accept" header to "application/json" or use this parameter to force the generation of JSON output. The value must either be "json" to force JSON output or "" (or remove the parameter completely to default to "accept" header checks. This parameter is useful when using framework such as JQuery that send "accept" headers with a "*/*" value to match all types. In this case this parameter implies that we want a JSON response output. The difference between this parameter and the jcrNewNodeOutputFormat one is that this one is used to generate a direct HTTP response whereas the latter is used to generate a redirection URL. |
jcrReturnContentTypeOverride | no | When the jcrReturnContentType is activated, it will default to the following response content type: application/json; charset=UTF-8 This parameter makes it possible to override the response content type to anything you need. Note that if you don't include a charset directive it will automatically append a UTF-8 charset. |
jcrRedirectTo | no | If defined will redirect to the specified URL instead of the newly created node. |
jcrMethodToCall | no | For HTML form to override the method called, this allow to delete content with a simple POST form instead of send a DELETE HTTP request |
jcrAutoCheckin | no | Allow to force versioning of the newly created content. |
jcrTargetDirectory | no | If you have multipart form, this will be the location where the content is uploaded |
jcrVersion | no | If "true" activate the versionning on the posted content (works only for files) |
jcr:mixinTypes | no | Allow you to set a mixin on the new node |
AJAX REQUESTS
All AJAX JSON requests should have the following headers set :
accept: application/json x-requested-with : XMLHttpRequest
The second header should be set for you when accessing the API through a browser, but if you are using the REST API through an HttpClient call or even another external client software (such as a mobile device for example), be sure that both these headers are set, otherwise it will default to HTML feedback.
File uploads
In the case of a file upload, when using AJAX requests (by setting the headers as described in the previous section), you actually have to perform two requests. The first request will be a multipart form upload and will save either in the current users' "files" directory, or if the "targetDirectory" property was set it will store the uploaded files there. The response is a JSON object with a "uuids" array property that contains string of the identifiers of all the created file nodes in the repository.
The second request can then use the UUIDs returned by the first AJAX request to set references to the new file nodes.
If a file already exists at the upload target, it will be updated, and if versioning is activated (see the version parameter above), it will create a new version before updating the file.
Response format
In the case of a regular POST method, the default response format is HTML. This format may be changed to JSON by setting up the appropriate request headers (see below).
In the case of HTML we will render the newly created or updated node with its default template (unless you have specified a different template using the newNodeOutputFormat parameter).
In order to change the response format to JSON, you must set the following headers : "accept" = "application/json" and "x-requested-with" = "XMLHttpRequest".
Examples
Post Example
<form action="<c:url value='${url.base}${currentNode.path}/*'/>" method="post">
<input type="hidden" name="jcrNodeType" value="jnt:post"/>
<input type="hidden" name="jcrRedirectTo" value="<c:url value='${url.base}${renderContext.mainResource.node.path}'/>"/>
<%-- Define the output format for the newly created node by default html or by jcrRedirectTo--%>
<input type="hidden" name="jcrNewNodeOutputFormat" value="html"/>
<fieldset>
<p class="field">
<input value="<c:if test="${not empty currentNode.children}"> Re:</c:if>${currentNode.propertiesAsString['threadSubject']}" type="text" size="35" id="forum_site" name="jcr:title"
tabindex="1"/>
</p>
<p class="field">
<textarea rows="7" cols="35" id="jahia-forum-thread-${currentNode.UUID}" name="content" tabindex="2"></textarea>
</p>
<p class="forum_button">
<input type="reset" value="Reset" class="button" tabindex="3"/>
<input type="submit" value="Submit" class="button" tabindex="4"/>
</p>
</fieldset>
</form>
This will create a new comments in a thread of a forum. And after submission we want to redisplay the curren thread, this is why we used the jcrRedirectTo parameter.
Delete Example
<form action="<c:url value='${url.base}${currentNode.path}'/>" method="post"
id="jahia-forum-post-delete-${currentNode.UUID}">
<input type="hidden" name="jcrRedirectTo" value="<c:url value='${url.base}${renderContext.mainResource.node.path}'/>"/>
<%-- Define the output format for the newly created node by default html or by jcrRedirectTo--%>
<input type="hidden" name="jcrNewNodeOutputFormat" value="html"/>
<input type="hidden" name="jcrMethodToCall" value="delete"/>
</form>
<a title="<fmt:message key='delete.post'/>" href="#"
onclick="document.getElementById('jahia-forum-post-delete-${currentNode.UUID}').submit();"><span><fmt:message key="delete.post"/></span>
This example shows how to write a simple form to delete a content with a simple link.
Find
The find servlet can be used to query content inside the Jahia JCR repository. It provides a lightweight but very powerful interface to retrieve content anywhere inside the content repository. Please note that if you need to search for users or groups, you should use the findPrincipal servlet instead, since it integrates with external principal repositories such as LDAP.
Since DX 7.1, this API is disabled by default. You can re-enable it by changing the value of jahia.find.disabled, but it is advised for security reasons to keep it disabled and to use new RESTful API and the prepared queries.
URL : /cms/find/WORKSPACE/LANGUAGE
Method : POST or GET
Parameters :
Name | Mandatory | Description |
---|---|---|
query | yes | The JCR query to execute. You can reference request parameters in your query by using the following syntax : $paramName which will look for a request parameter called "paramName" |
language | no | The JCR language used in the query, for example "JCR-SQL2" or "xpath" Defaults to "JCR-SQL2" |
limit | no | The maximum number of results to return. Default set to 20 with a maximum of 100. Use the offset parameter to retrieve more than 100 results. |
offset | no | The 0-index based offset to start results with. Defaults to 0. |
depthLimit | no | The depth of the result tree. If not set it defaults to "1". If you have deep content tree hierarchies you might want to limit result depth to avoid performance issues. |
escapeColon | no | If set to true, the ':' in the node property name will be replaced '_'. Defaults to false. |
propertyMatchRexexp | no | The value you specify here will be used to match properties in the result set, and will build a matchingProperties JSON list that will let you known which properties have matched. This is especially useful when using fulltext search, since the JCR doesn't indicate which property has matched the criteria. The value you specify must comply with the JDK Pattern Regexp syntax. |
removeDuplicatePropValues | no | If activated, it will filter the matched properties (that were matched using the propertyMatchRegexp above parameter) to make sure only one node per property value is returned. This is useful when building unique list of property values for auto-completion. Defaults to false. |
Example
HTTP GET Request (formatted for legibility) :
http://localhost:8080/cms/find/default/en
?q=t
&limit=10
&query=%2Fjcr%3Aroot%2Fsites%2FmySite%2Ftags%2F%2Felement(*%2C+jnt%3Atag)%5Bjcr%3Acontains(.%2C'%7B%24q%7D*')%5D%2F%40j%3Anodename
&language=xpath
&escapeColon=false
&propertyMatchRegexp=%7B%24q%7D.*
&removeDuplicatePropValues=false
In the above example, the values are encoded so that makes a bit harder to read. Here are the unencoded values :
query = /jcr:root/sites/mySite/tags//element(*, jnt:tag)[jcr:contains(.,'{$q}*')]/@j:nodename
propertyMatchRegexp = {$q}.*
One important thing is to now the $q marker that will actually be replaced with the value of the "q" request parameter specified in the request. This makes it easy to integrate with JQuery plugins that might use specific named request parameters.
HTTP Response :
Content-Length:1721
Content-Type:application/json;charset=UTF-8
Date:Fri, 26 Feb 2010 13:31:38 GMT
Server:Apache-Coyote/1.1
[
{
"jcr:score": "1366",
"node": {
"index": 1,
"jcr:createdBy": "root",
"jcr:baseVersion": "/repository/default/jcr:system/jcr:versionStorage/5b/92/d7/5b92d76a-ee40-4b17-87e6-911a38db2ddb/jcr:rootVersion",
"jcr:versionHistory": "/repository/default/jcr:system/jcr:versionStorage/5b/92/d7/5b92d76a-ee40-4b17-87e6-911a38db2ddb",
"depth": 4,
"jcr:lastModifiedBy": "root",
"primaryNodeType": "jnt:tag",
"j:nodename": "tag1",
"j:originWS": "default",
"jcr:isCheckedOut": "true",
"jcr:lastModified": "2010-02-26T12:14:14.502+01:00",
"matchingProperties": [
"j:nodename",
"jcr:isCheckedOut"
],
"jcr:created": "2010-02-26T12:14:14.397+01:00",
"j:fullpath": "/sites/mySite/tags/tag1",
"jcr:uuid": "5b92d76a-ee40-4b17-87e6-911a38db2ddb",
"jcr:primaryType": "jnt:tag",
"identifier": "5b92d76a-ee40-4b17-87e6-911a38db2ddb",
"path": "/sites/mySite/tags/tag1"
},
"j:nodename": "tag1",
"jcr:path": "/sites/mySite/tags/tag1"
},
{
"jcr:score": "1366",
"node": {
"index": 1,
"jcr:createdBy": "root",
"jcr:baseVersion": "/repository/default/jcr:system/jcr:versionStorage/ab/63/bb/ab63bb78-8708-48aa-a040-82eee22ae5d7/jcr:rootVersion",
"jcr:versionHistory": "/repository/default/jcr:system/jcr:versionStorage/ab/63/bb/ab63bb78-8708-48aa-a040-82eee22ae5d7",
"depth": 4,
"jcr:lastModifiedBy": "root",
"primaryNodeType": "jnt:tag",
"j:nodename": "tag2",
"j:originWS": "default",
"jcr:isCheckedOut": "true",
"jcr:lastModified": "2010-02-26T12:14:18.570+01:00",
"matchingProperties": [
"j:nodename",
"jcr:isCheckedOut"
],
"jcr:created": "2010-02-26T12:14:18.477+01:00",
"j:fullpath": "/sites/mySite/tags/tag2",
"jcr:uuid": "ab63bb78-8708-48aa-a040-82eee22ae5d7",
"jcr:primaryType": "jnt:tag",
"identifier": "ab63bb78-8708-48aa-a040-82eee22ae5d7",
"path": "/sites/mySite/tags/tag2"
},
"j:nodename": "tag2",
"jcr:path": "/sites/mySite/tags/tag2"
}
]
In the above JSON response, it is important to node the following properties :
jcr:score
indicates the score of the result
node
contains all the node information, including the matchProperties array that indicates which properties have matched the propertyMatchingRegexp parameter.
Find users or groups
This servlet is dedicated to searching for users or groups, with the particularity of being able to search in external principal repositories connected with Jahia, such as an LDAP repository. Note that the query and result formats are quite different to the default find servlet, because they are not representing JCR nodes but principal data structures.
URL : /cms/findPrincipal
Method : POST or GET
Parameters :
Name | Mandatory | Description |
---|---|---|
principalType | yes | The type of principal you wish to query for. The accepted values are "users" or "groups" a request parameter called "paramName" |
siteKey | yes (if searching for groups | If you have set the principalType parameter to "groups", you will be required to specify a value for the siteKey parameter, which is the site in which you want to search for the groups. |
wildcardTerm | no | If this parameter is specified, it will be used to search for the value specified in any principal property. If you prefer to search for specific properties, you must specify them separately in the HTTP request parameters. You can use the $paramName syntax to reference other request parameters. For example $q will be replaced by the value of a parameter called "q" if it is found. If it is not found the marker will not be replaced. |
propertyMatchRexexp | no | The value you specify here will be used to match properties in the result set, and will build a matchingProperties JSON list that will let you known which properties have matched. This is especially useful when using fulltext search, since the JCR doesn't indicate which property has matched the criteria. The value you specify must comply with the JDK Pattern Regexp syntax. |
removeDuplicatePropValues | no | If activated, it will filter the matched properties (that were matched using the propertyMatchRegexp above parameter) to make sure only one node per property value is returned. This is useful when building unique list of property values for auto-completion. Defaults to false. |
includeCriteriaNames | no | A comma seperated list of parameter names that should be used to create the criterias for the search. |
any other parameter name | no | If you specify another other parameter other than the above reserved ones, the servlet will interpret them as property names that have to to be search for the specified values. You can use the $paramName syntax to reference other request parameters. For example $q will be replaced by the value of a parameter called "q" if it is found. If it is not found the marker will not be replaced. By default the servlet will use all the other found parameter names, which might not be what you expect. You should use the includeCriteriaNames to specify which criteria should be used |
Example
HTTP GET Request (formatted for legibility)
http://localhost:8080/cms/findPrincipal?principalType=users&wildcardTerm=steve
HTTP Response
[
{
"userProperties": "{j:published=true, j:nodename=sjobs, j:originWS=default, jcr:createdBy= system , j:external=false, j:firstName=Steve, jcr:primaryType=jnt:user, j:lastPublished=2010-06-16T16:13:34.094+02:00, j:email=, j:password=943+/myvMchk4TM1T11nKHx2u7g=, j:lastName=Jobs, jcr:baseVersion=164174a4-1ba0-4e3b-8143-119c9546be6c, j:lastPublishedBy= system root, jcr:created=2010-06-16T16:13:33.698+02:00, jcr:versionHistory=d17e5352-4909-48ef-94ef-e5957f741512, jcr:uuid=662cdfed-e674-4dff-85ee-dadb19243f5e, j:fullpath=/users/sjobs, jcr:isCheckedOut=true, j:checkinDate=2010-06-16T16:13:34.094+02:00, jcr:lastModified=2010-06-16T16:14:01.670+02:00, jcr:lastModifiedBy= system , j:organization=}",
"homepageID": -1,
"username": "sjobs",
"providerName": "jcr",
"userKey": "{jcr}sjobs",
"root": false,
"external": false,
"name": "sjobs",
"class": "class org.jahia.services.usermanager.jcr.JCRUser",
"properties": {
"j:firstName": "Steve",
"jcr:created": "2010-06-16T16:13:33.698+02:00",
"j:external": "false",
"j:originWS": "default",
"j:published": "true",
"jcr:lastModified": "2010-06-16T16:14:01.670+02:00",
"j:lastPublishedBy": " system root",
"j:checkinDate": "2010-06-16T16:13:34.094+02:00",
"jcr:createdBy": " system ",
"j:nodename": "sjobs",
"jcr:versionHistory": "d17e5352-4909-48ef-94ef-e5957f741512",
"jcr:lastModifiedBy": " system ",
"jcr:uuid": "662cdfed-e674-4dff-85ee-dadb19243f5e",
"preferredLanguage": "en",
"jcr:baseVersion": "164174a4-1ba0-4e3b-8143-119c9546be6c",
"j:lastName": "Jobs",
"jcr:isCheckedOut": "true",
"j:password": "943+/myvMchk4TM1T11nKHx2u7g=",
"j:lastPublished": "2010-06-16T16:13:34.094+02:00",
"j:organization": "",
"jcr:primaryType": "jnt:user",
"j:fullpath": "/users/sjobs",
"j:email": ""
},
"identifier": "662cdfed-e674-4dff-85ee-dadb19243f5e",
"passwordReadOnly": false
}
]
DEBUGGING
In order to help with the debugging with AJAX REST requests, you can use built-in inspection tools such as :
- Safari's 4.0 Web Inspector (especially the resources and script tabs)
- Firefox's Firebug plugin
- any other equivalents
Property interceptors
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 e.g. 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 in a core Spring file or in any module's Spring file as follows:
<bean name="scriptFilteringInterceptor" parent="propertyInterceptorRegistrator">
<property name="propertyInterceptor">
<bean class="org.jahia.services.content.interceptor.HtmlFilteringInterceptor" parent="abstractRichTextPropertyInterceptor">
<property name="filteredTags" value="script"/>
<property name="removeContentBetweenTags" value="true"/>
</bean>
</property>
</bean>
The aforementioned example registers an interceptor to filter out <script/> tags with content from all rich text properties before storing their value into repository.
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:
<bean name="postFilteringInterceptor" parent="propertyInterceptorRegistrator">
<property name="propertyInterceptor">
<bean class="org.jahia.modules.custom.interceptor.PostFilteringInterceptor" parent="abstractRichTextPropertyInterceptor">
<property name="nodeTypes" value="jnt:post"/>
<property name="requiredTypes" value="String"/>
</bean>
</property>
</bean>
An interceptor in the example above 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):
public class PostFilteringInterceptor extends BaseInterceptor {
@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;
}
}
Actions
PRINCIPLES
An Action is a simple class extending the org.jahia.bin.Action abstract class, which is responsible of handling permissions and other restrictions. You need to override the method doExecute to provide your own Action.
public abstract ActionResult doExecute(HttpServletRequest req, RenderContext renderContext, Resource resource,
JCRSessionWrapper session, Map<String, List<String>> parameters,
URLResolver urlResolver)
throws Exception;
You can do anything you want in an action but keep in mind that the shortest time your action takes to execute is the better end-user experience.
As said previously, Actions are called through HTTP POST methods but, as a result of an action, you can let the user stay on its current web page or redirect to another web page. The result can be a JSON serialized object. This allows to easily interact with framework like JQuery. To describe this behavior, the method doExecute returns a org.jahia.bin.ActionResult object. It contains a HTTP response code and the eventual redirect URL and org.json.JSONObject instance.
Example based on the RateContent Action
public class RateContent extends Action {
JCRTemplate jcrTemplate;
public void setJcrTemplate(JCRTemplate jcrTemplate) {
this.jcrTemplate = jcrTemplate;
}
@Override
public ActionResult doExecute(HttpServletRequest req, RenderContext renderContext, final Resource resource, JCRSessionWrapper session, final Map<String, List<String>> parameters, URLResolver urlResolver) throws Exception {
return (ActionResult) jcrTemplate.doExecuteWithSystemSession(null,session.getWorkspace().getName(),session.getLocale(),new JCRCallback<Object>() {
public Object doInJCR(JCRSessionWrapper session) throws RepositoryException {
JCRNodeWrapper node = session.getNodeByUUID(resource.getNode().getIdentifier());
if (!node.isNodeType("jmix:rating")) {
session.checkout(node);
node.addMixin("jmix:rating");
session.save();
}
List<String> values = parameters.get("j:lastVote");
node.setProperty("j:lastVote", values.get(0));
node.setProperty("j:nbOfVotes",node.getProperty("j:nbOfVotes").getLong()+1);
node.setProperty("j:sumOfVotes",node.getProperty("j:sumOfVotes").getLong()+values.get(0));
session.save();
try {
return new ActionResult(HttpServletResponse.SC_OK, node.getPath(), Render.serializeNodeToJSON(node));
} catch (IOException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
} catch (JSONException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
}
return null;
}
});
}
}
The org.jahia.bin.Render.serializeNodeToJSON method is a utility function that returns a JSONObject representing a node.
Declaration of an Action is done via Spring. In our example, this action is defined in the rating module like this:
<bean class="org.jahia.modules.rating.actions.RateContent" >
<property name="name" value="rate"/>
<property name="jcrTemplate" ref="jcrTemplate"/>
</bean>
CALLING AN ACTION FROM A FORM
Calling an action is simple as calling node/path.actionName.do. In our example for the RateContent Action, here is an extract of a jQuery function in rateable.jsp:
$.post("<c:url value='${url.base}${bindedComponent.path}'/>.rate.do", {'j:lastVote': value,'jcrMethodToCall':"post",
'jcrCookieName':"rated${bindedComponent.identifier}",
'jcrCookieValue':"${currentNode.identifier}"}, function(
result) {
<%-- Select stars from "Average rating" control to match the returned average rating value --%>
$("#avg${id}").stars("select", Math.round(result.j_sumOfVotes / result.j_nbOfVotes));
<%-- Update other text controls... --%>
$("#all_votes${id}").text(result.j_nbOfVotes);
$("#all_avg${id}").text(('' + result.j_sumOfVotes / result.j_nbOfVotes).substring(0, 3));
<%-- Display confirmation message to the user --%>
$("#messages${id}").html("<br/><fmt:message key="label.ratingSaved"/> (" + value + "). <fmt:message key="label.thanks"/>!").stop().css("opacity", 1).fadeIn(30);
<%-- Hide confirmation message and enable stars for "Rate this" control, after 2 sec... --%>
setTimeout(function() {
$("#messages${id}").fadeOut(1000, function() {
// ui.enable();
});
}, 2000);
}, "json");
Here we see that by posting on the "<c:url value='$url.base$bindedComponent.path'/>.rate.do URL, we are calling the RateContent Action on the bound component.
In rating.hidden.plusone_minorone_form.jsp, you can see another example of calling our Action, this time from a form:
<c:set var="cookieName" value="rated${currentNode.identifier}"/>
<c:if test="${renderContext.loggedIn and (empty cookie[cookieName])}">
<form action="<c:url value='${url.base}${currentNode.path}'/>.rate.do" method="post"
id="jahia-forum-post-vote-${currentNode.identifier}">
<input type="hidden" name="jcrRedirectTo" value="<c:url value='${url.base}${renderContext.mainResource.node.path}'/>"/>
<%-- Define the output format for the newly created node by default html or by redirectTo--%>
<input type="hidden" name="jcrNewNodeOutputFormat" value="html"/>
<input type="hidden" name="jcrMethodToCall" value="post"/>
<input type="hidden" name="j:lastVote" value="1"/>
<input type="hidden" name="jcrCookieValue" value="${currentNode.identifier}"/>
<input type="hidden" name="jcrCookieName" value="${cookieName}"/>
<input type="hidden" name="jcrReturnContentType" value="html"/>
</form>
</c:if>
FORM PARAMETERS AND FILE UPLOADING
In an Action, you will receive your form parameters inside the parameters map. Your form submission will pass through several components before reaching your Action. Those components are responsible for managing the security (captcha validation, permissions check) and the uploading of files. If your action needs to manage files, the file will be uploaded in temporary folder so you will only have to choose where to save it in the JCR or elsewhere.
An example of getting an uploaded file in your action and handling it in there :
final FileUpload fu = (FileUpload) req.getAttribute(FileUpload.FILEUPLOAD_ATTRIBUTE);
DiskFileItem inputFile = fu.getFileItems().get("fileField");
This example is from the DocumentConverterAction class.
CHAINING ACTIONS IN ONE REQUEST
Jahia provides a ChainAction class that allows to chain actions called. You will have to pass a chainOfAction parameter containing the list of action to execute - all actions will be executed sequentially with the same parameters, and the the result will be one of the last actions.
For example, the following form will call mail and redirect actions :
<form action="mynode.chain.do" method="post">
<input type="hidden" name="chainOfAction" value="mail,redirect"/>
...
</form>
INTERACTING WITH OTHER JAHIA SUBSYSTEMS
Your action can also interact or be called by other Jahia subsystems like rules or GWT UI; for this your action must also implement the <BackgroundAction> interface:
public class SendAsNewsletterAction extends Action implements BackgroundAction {
public void executeBackgroundAction(JCRNodeWrapper node) {
// do local post on node.getPath/sendAsNewsletter.do
try {
Map<String,String> headers = new HashMap<String,String>();
headers.put("jahiatoken",TokenAuthValveImpl.addToken(node.getSession().getUser()));
String out = httpClientService.executePost("http://localhost:8080"+
Jahia.getContextPath() + Render.getRenderServletPath() + "/live/"
+ node.getResolveSite().getDefaultLanguage() + node.getPath()
+ ".sendAsNewsletter.do", null, headers);
logger.info(out);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
Example of usage in rules:
rule "Schedule as a newsletter"
when
A property j:scheduled has been set on a node
- the node has the type jnt:newsletterIssue
then
Log "Scheduling newsletter " + node.getPath() + " at " + propertyValueAsString
Execute the action "sendAsNewsletter" at j:scheduled on the node
end
Actions: forms validation
Form validation works in two phases, first you have to do some javascript validation on client side. Then Jahia will validate the captcha for you if present, and then your Action could do any validation needed.
Here the code of a typical form, from the add comment example
<template:addResources type="javascript" resources="jquery.js,jquery.validate.js"/>
<template:addResources type="inlinejavascript">
<script type="text/javascript">
$(document).ready(function() {
$("#newCommentForm").validate({
rules: {
'jcr:title': "required",
<c:if test="${not renderContext.loggedIn}">
pseudo: "required",
captcha: "required"
</c:if>
}
});
});
</script>
</template:addResources>
<template:tokenizedForm>
<form action="<c:url value='${url.base}${boundComponent.path}.addComment.do'/>" method="post" id="newCommentForm">
<input type="hidden" name="jcrNodeType" value="jnt:post"/>
<input type="hidden" name="jcrRedirectTo" value="<c:url value='${url.base}${renderContext.mainResource.node.path}'/>"/>
<input type="hidden" name="jcrNewNodeOutputFormat" value="html"/>
<input type="hidden" name="jcrResourceID" value="${currentNode.identifier}"/>
<div id="formGenericComment">
<fieldset>
<c:if test="${not renderContext.loggedIn}">
<p class="field">
<label for="comment_pseudo"><fmt:message key="comment.pseudo"/></label>
<input value="${sessionScope.formDatas['pseudo'][0]}"
type="text" size="35" name="pseudo" id="comment_pseudo"
tabindex="1"/>
</p>
</c:if>
<p class="field">
<label class="left" for="comment-title"><fmt:message key="comment.title"/></label>
<input class="" value="${sessionScope.formDatas['jcr:title'][0]}"
type="text" size="35" id="comment-title" name="jcr:title"
tabindex="1"/>
</p>
<p class="field">
<label class="left" for="jahia-comment-${boundComponent.identifier}"><fmt:message
key="comment.body"/></label>
<textarea rows="7" cols="35" id="jahia-comment-${boundComponent.identifier}"
name="content"
tabindex="2"><c:if
test="${not empty sessionScope.formDatas['content']}">${fn:escapeXml(sessionScope.formDatas['content'][0])}</c:if></textarea>
</p>
<c:if test="${not renderContext.loggedIn}">
<p class="field">
<label class="left" for="captcha"><template:captcha/></label>
<input type="text" id="captcha" name="jcrCaptcha"/>
</p>
</c:if>
<p>
<input type="reset" value="<fmt:message key='label.reset'/>" class="button"
tabindex="3" ${disabled}/>
<input type="submit" value="<fmt:message key='label.submit'/>" class="button"
tabindex="4" ${disabled}/>
</p>
</fieldset>
</div>
</form>
</template:tokenizedForm>
First of all you design your form classically
<form action="<c:url value='${url.base}${boundComponent.path}.addComment.do'/>" method="post" id="newCommentForm">
<div id="formGenericComment">
<fieldset>
<c:if test="${not renderContext.loggedIn}">
<p class="field">
<label for="comment_pseudo"><fmt:message key="comment.pseudo"/></label>
<input value=""
type="text" size="35" name="pseudo" id="comment_pseudo"
tabindex="1"/>
</p>
</c:if>
<p class="field">
<label class="left" for="comment-title"><fmt:message key="comment.title"/></label>
<input class="" value=""
type="text" size="35" id="comment-title" name="jcr:title"
tabindex="1"/>
</p>
<p class="field">
<label class="left" for="jahia-comment-${boundComponent.identifier}"><fmt:message
key="comment.body"/></label>
<textarea rows="7" cols="35" id="jahia-comment-${boundComponent.identifier}"
name="content"
tabindex="2"></textarea>
</p>
<p>
<input type="reset" value="<fmt:message key='label.reset'/>" class="button"
tabindex="3"/>
<input type="submit" value="<fmt:message key='label.submit'/>" class="button"
tabindex="4"/>
</p>
</fieldset>
</div>
</form>
Then you add specific hidden fields for Jahia :
<input type="hidden" name="jcrNodeType" value="jnt:post"/>
<input type="hidden" name="jcrRedirectTo" value="<c:url value='${url.base}${renderContext.mainResource.node.path}'/>"/>
<input type="hidden" name="jcrNewNodeOutputFormat" value="html"/>
<input type="hidden" name="jcrResourceID" value="${currentNode.identifier}"/>
Here the AddComment action expects some specific control parameters :
<nodeType>
defines the type of node this form will create on submission<redirectTo>
defines to which node we want to redirect (you can also specify the template here), here we redirect to the current page<newNodeOutputFormat>
specify in which format the results will be displayed here <html> (can be ommited as it is the default value)<resourceID>
specify the identifier of the currently displayed node to allow the cache system to correctly redisplay the form in case of errors.
ALLOWING CONTENT CREATION IN LIVE WORKSPACE BY AUTHENTICATED OR NON AUTHENTICATED USERS
If your form is designed to be used in live mode (render/live) to allow your users to create content, then you must use the token system of Jahia for your forms and enforce the usage of captcha for at least your non authenticated users.
When Jahia receive data from a tokenized form, it will execute the action with system user. This ensure that anybody seeing the form will be able to execute the action, even if it requires write access. It will also bypass any action restrictions that were set on the action, such as requiredWorkspace, requiredPermission, requiredAuthenticatedUser. If you need to restrict the form display or submission to a specific workspace, you will have to add <c:if> statements around the tokenized form rendering that test the current workspace and also implement checks inside your actions implementation (make sure you do both because a malicious user could always try to submit a HTTP POST request directly to your action, bypassing the HTML form entirely).
First you will need to tokenize your form for that simply wrap your form inside the <template:tokenizedForm> tag
<template:tokenizedForm>
<form>
~~~~~~form code~~~~~~
</form>
</template:tokenizedForm>
If you look at your html source code in your browser, you will see that the tokenizedForm tag has added an hidden input field in your form containing a uuid that identifies this instance of the form. The uuid is generated by a macro so that the uuid is regenerated each time the form is displayed even if delivered from the cache system.
You also need to add a captcha for at least the non authenticated users to avoid spamming of fake form submission.
To add a captcha, you need to have an input field named <captcha> and display the captcha itself using the <template:captcha> tag.
<c:if test="${not renderContext.loggedIn}">
<p class="field">
<label class="left" for="captcha"><template:captcha/></label>
<input type="text" id="captcha" name="jcrCaptcha"/>
</p>
</c:if>
VALIDATION OF DATAS BEFORE AND AFTER SUBMISSION
To validate your form on the client side we advise you to use the JQuery Validate plugin. Here an example of configuring it for the previous form:
<template:addResources type="javascript" resources="jquery.js,jquery.validate.js"/>
<template:addResources type="inlinejavascript">
<script type="text/javascript">
$(document).ready(function() {
$("#newCommentForm").validate({
rules: {
'jcr:title': "required",
<c:if test="${not renderContext.loggedIn}">
pseudo: "required",
captcha: "required"
</c:if>
}
});
});
</script>
</template:addResources>
To be noticed that this plugin refer to the field by their <name> attribute and not their <id> attribute.
On token or captcha error Jahia will send you back the submitted datas so you can refill the form with previously submitted datas, so that your users do not have to retype everything.
All submitted datas are store under map in the session of the user. Here an example on a simple filed to display the previously submitted data if needed.
<input class="" value="${sessionScope.formDatas['jcr:title'][0]}"
type="text" size="35" id="comment-title" name="jcr:title"
tabindex="1"/>
We see here the formDatas
map under the session of the user <$sessionScope.formDatas['jcr:title'][0]>
Actions provided by the default module
PERMISSIONS
In order to use some actions, you might need to setup permissions if you intend to perform them with users other than the root user.
GENERAL ACTIONS
addMemberToGroup
chain
This action is used to execute many actions one after the other.
Class: ChainAction.java
URL: /path/to/the/node.chain.do
Parameters:
Name | Value | Example |
---|---|---|
chainOfAction | comma separated list of action names | default,startWorkflow |
Example:
<!--
Submitting this form :
1. creates a main content into the home page of ACME-SPACE demo website
2. starts the simple publication workflow
-->
<form method="post" action="cms/render/default/en/sites/ACME-SPACE/home.chain.do" />
<input type="hidden" name="chainOfAction" value="default,startWorkflow"/>
<!-- Parameters for the default action (content creation) -->
<input type="hidden" name="jcrNodeType" value="jnt:mainContent"/>
<input type="hidden" name="jcr:title" value="Title of my new content"/>
<input type="hidden" name="body" value="Body of my new content"/>
<!-- Parameters for StartWorkflowAction -->
<input type="hidden" name="process" value="jBPM:1-step-publication" />
<input type="submit" value="Start the actions" />
</form>
lock
unlock
move
multiplePublish
publish
startWorkflow
This action starts a workflow on a node.
Class: StartWorkflowAction.java
URL: /path/to/the/node.startWorkflow.do
Parameters:
Name | Value | Example |
---|---|---|
process | <workflowProvider>:<workflowKey> | jBPM:1-step-publication |
Example:
<!-- Submitting this form starts the simple publication workflow on the homepage of ACME-SPACE demo website -->
<form method="post" action="cms/render/default/en/sites/ACME-SPACE/home.startWorkflow.do" />
<input type="hidden" name="process" value="jBPM:1-step-publication" />
<input type="submit" value="Start workflow" />
</form>
COMMENTS
addComment
CLIPBOARD
checkClipboard
cleanClipboard
multipleCopy
multipleCut
multipleDelete
multiplePaste
TASKS
commentTask
executeTask
Auto-splitting nodes
OVERVIEW
Automatic splitting feature allows for non-intrusive management of large amount of sub-nodes, 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, e.g.. 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, e.g. 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 2010, will land under:
files
|_sergiy
|_2010
|_07
|_report.pdf
Split folders sergiy, 2010
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. E.g. 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
RESTful JCR Access
Overview
REST, HATEOAS and HTTP
REST stands for REpresentational State Transfer. It is an architectural style defined by R. Fielding that underlies the modern web. On the web, anything of interest can become a resource and be identified using a Universal Resource Identifier (URI). Resources are interacted with using representations that are passed between clients and servers.
Hypermedia As The Engine Of Application State (HATEOAS) is a concept that states that all the information needed to interact with a resource should be contained in its representation. Consider a web site. You first access it by typing its URL (a special kind of URI) in your browser's location bar. The server sends you a representation (HTML page) of the web site's home page (the entry point resource). What you can do with that web page is contained in its representation: links, forms, etc. Of course, you could try to figure out what other resources might exist on that server by manually crafting URIs but it's much easier to follow the information contained in the HTML page. manipulated via representations that are passed between clients and servers. This is the essence of the HATEOAS concept.
HyperText Transfer Protocol (HTTP) is the protocol on which the web is build. It defines a uniform interface that both clients and servers agree to and with which they can manipulate resources. In particular, the protocol defines methods (or verbs) corresponding to operations that can be done on resources. The main methods are:
GET
: retrieve the identified resourcePUT
: add / update the identified resourcePOST
: should only be used for complex operations or to create resources from a factory resource (both if needed)DELETE
: delete the identified resource
For a good (and not overly complex) overview of REST, please see Jos Dirksen's REST: From GET to HATEOAS presentation.
API version history
- v1.0: initial release
- v1.1:
PUT
andDELETE
methods are now supported when operating on nodes via their path- added
includeFullChildren
,resolveReferences
andnoLinks
flags that can be passed as URI query parameters to control some aspects of the representations - added new
query
endpoint to performJCR-SQL2
queries on the repository and retrieve matching nodes, disabled by default for security reasons, following thejahia.find .disabled
property. - order of children is now properly maintained
- it is now possible to filter children to retrieve by providing a list of accepted child node types concatenated by commas
- v1.1.1:
- added support for prepared queries to the query endpoint. This is now the preferred way to use the query endpoint since prepared queries will still be available even when the query endpoint is disabled since they are considered "safe" by the administrator who deployed them.
- flags are now also supported on query endpoint
- v1.2:
- support for server-delegated naming of newly created instances via
POST
requests via both node identifiers or paths - added JSON version of the
/version
endpoint to have an easier to parse version information
- support for server-delegated naming of newly created instances via
Implementation version history
- v2.0.0: initial release
- v2.0.1:
- properly use
application/hal+json
content type on responses - improved links support
- minor improvements
- properly use
- v2.1.0:
- support for v1.1 of the API
- fixed an issue with Paths API where extra slashes in the URI could result in unexpected behavior
- v2.1.1:
- support for v1.1.1 of the API
- support for disabling query and types endpoint based on
jahia.find.disabled
setting
- v2.2.0:
- support for v1.2 of the API
- uses BOM from jaxrs-osgi-extender and json-generation modules to keep Jackson and Jersey versions in sync
Goals
The goals of this project are as follows:
- Provide a simple Create-Read-Update-Delete (CRUD) RESTful API to JCR content
- Leverage HATEOAS by providing all the required information to interact with a resource in its representation
- Support only JSON representations
- Optimize the API for Javascript client applications
- Provide full JCR access from the API with the associated benefits and risks
Special provision for PUT and POST methods
PUT and POST methods theoretically exchange full resource representations. However, nodes and properties representations can be quite complex with a potentially deep graph-like structure. Moreover, much of the exposed data is actually read-only (node type information, links...) or is or can be derived from the node type information. It would therefore be inefficient to require clients to pass all that information to the API during creation or update of resources. We adopt the convention that only the information that is either required or is being changed as a result of the operation is passed along to the API. This results in minimal effort on the client side and has the added benefit of reducing the amount of network chatter. The semantics we follow is therefore close to the PATCH method semantics in spirit, if not in implementation.
Resources identification
The natural match to map JCR data unto resources is to use JCR nodes as resources, identified either by their path or identifier, which is made rather easy since JCR data is stored mostly in tree form.
A node also defines sub-resources:
- children for a given node are accessed using the
children
child resource - properties for a given node are found under the
properties
child resource - mixins for a given node are accessed using the
mixins
child resource - versions for a given node are found under the
versions
child resource
Each of these sub-resources (which we also call sub-element type as they identify a type of node sub-element) provides named access to their respective sub-elements as well.
URI design
:
character is encoded by__
in property names since:
is a reserved character for URIs- indices of same name siblings are denoted using the
--
prefix
Examples
Node | Encoded URI |
---|---|
/foo/ns:bar/ns:child[2] |
/foo/ns__bar/ns__child--2 |
mix:title mixin of /foo |
/foo/mixins/mix__title |
jcr:uuid property of /a |
/a/properties/jcr__uuid |
Basic API workflow
Using the API is then a matter of:
- Identifying which resource to work with.
- Deciding which HTTP method to use to operate on the identified resource.
- Invoke the HTTP method on the resource passing it any necessary data.
Since client and server exchange representations to signify state changes, when you need to pass data to create or update a resource, the API server expects a representation it can understand. In this particular implementation, representations that are expected as input data of operations MUST have the exact same structure as the representation you would retrieve performing a GET
operation on that particular resource.
So, for example, if you're trying to add several properties to a given node, you could do it several different ways. Either you could PUT
each property individually using each property URI as target URI and passing the JSON representation of a property for each invocation. You could also use the properties
sub-resource for this node and invoke the PUT
method with a body corresponding to the JSON representation of a properties
resource. This second way would result in modifying several properties using a single call to the API.
Note regarding asynchronous calls to the RESTful API: While the API implementation can properly handle concurrent requests at any given time, it is not designed to handle re-entrant requests. This will result in an exception. This could happen if you're calling the API from Javascript and you're calling back to the API in your success callback in an asynchronous call. There normally shouldn't be a need for such calls which is why we are not currently supporting this use case. We might revisit this position if such need arises.
Resources representation
This version of the API will use the JSON representation format as specified by the RFC 4627 augmented by the HAL specification as explained below. The media type for our specific representations for node elements is therefore application/hal+json
.
Note that representations elements are not ordered so you shouldn't depend on elements of a given representation being in a specific order. In particular, if elements appear in a given order in examples in this document this doesn't mean that they will appear in the same order in representations you will retrieve from the API.
Note also that we focus only on salient parts in examples so the representations that are shown might actually be incomplete and missing some data that we didn't consider relevant for that specific example. In particular, links sections are often elided in examples for brevity's sake.
Linking to resources and keeping URIs opaque
It is important that clients do not rely on building URIs themselves as much as possible and, thus, we should strive to let clients treat them as opaque as possible. This does not mean that URIs need to be unreadable. This, however, means that URIs for resources should be easily discoverable automatically by clients so that they don't need to construct them. In order to do that, each resource that can be interacted with embeds a _links
child object. This child object contains links objects per the HAL specification.
This means that links are represented as objects containing at least an href
property identifying the URI associated with the link.
Per the HAL recommendations, we define a self
reference identifying the URI to use to interact with this specific element. self
is a relative URI so we also provide an absolute
link providing the absolute URI for the given resource. If a resource has a type that we can identify then another type
link will also be available so that clients can find out more about the resource's metadata. If a resource can be accessed via its path (in JCR parlance), then a path
link pointing to the URI allowing access to the resource by its path. When appropriate, another parent
link will also be available, pointing to the parent node of the resource. Specific objects might add more link types when appropriate.
We add a little bit of redundant information to make links easier to work with from a javascript client application: we add a rel
property to each link that repeats the name of the link from within it so that when iterating over the links collection, we can easily retrieve the name of the relation from an individual link object.
To sum up, the _links
section will look similarly to the following example:
"_links" : {
"self" : {
"rel" : "self",
"href" : "<relative URI (to the API base URI) identifying the associated resource>"
},
"absolute" : {
"rel" : "absolute",
"href" : "<absolute URI identifying the associated resource>"
},
"type" : {
"rel" : "type",
"href" : "<URI identifying the resource associated with the resource type>"
}
... other links as appropriate
}
As of version 1.1 of the API, we've added the option not to output links if your client application has no need for them. This is accomplished by providing a query parameter named noLinks
to any API URI. If this query parameter is present in the URI, its value is assumed to be true
unless its value is false
, which corresponds to the default behavior where links are output. Any other value will be understood as true
.
Node representation
A node is composed of several elements that need to be represented as efficiently and usefully as possible so that both humans and automated systems can make sense of the information conveyed by the representation. We've identified the following information to represent nodes:
- The unescaped node's name, represented by the
name
field, as detailed in the Names section. - The name of the node's type, represented by the
type
field. - The node's properties, which are gathered in a
properties
object, as detailed in the Properties section. - The node's children collection, which are gathered in a
children
object, as detailed in the Children section. - The node's attached mixins, which information is gathered in a
mixins
object, as detailed in the Mixins section. - The node's versions if appropriate, which are gathered in a
versions
object, as detailed in the Versions section. - Links to useful resources associated with the node, gathered in
_links
object. By default, only one level of depth in the hierarchy is retrieved for a given node. This means that reference properties are not resolved to objects but left as strings. This also means that the API makes extensive use of links between resources to discover and interact with associated resources. This is detailed in the Linking section.
The node representation adds URIs identifying each sub-element to the _links
section, each named with the name of the sub-element it is associated with. This way, a properties
object is added to the _links
section pointing to the properties
resource, a mixins
object points to the mixins
resource, etc.
A node's collection resources allow users to query the particular kind of resource it holds or add new resource to the set of existing ones. Collections should also considered as ordered despite not being modelled using JSON arrays. We made this particular design decision because we felt being able to access a child resource via its name was more important than explicitly modelling ordering.
Names, escaped and unescaped
Since JCR property and node names are usually namespaced and the :
character used as a namespace delimiter in the prefixed form is a reserved JSON character, we use escaped (according to the rules outlined in the URI design section) names to identify both properties and nodes in collections. This way, client code can refer to properties and nodes without having to first escape them. However, it can still be useful to know the original item name. We therefore provide an extra value in the item content named quite appropriately name
which contains the original, unescaped name of the item.
Node representation structure
"name" : "<the node's unescaped name>",
"type" : "<the node's node type name>",
"properties" : <properties representation>,
"mixins" : <mixins representation>,
"children" : <children representation>,
"versions" : <versions representation>,
"_links" : {
"absolute": {
"rel": "absolute",
"href": "<An absolute URL directly usable to retrieve this node's representation>"
},
"versions": {
"rel": "versions",
"href": "<URI identifying the resource associated with this node's versions>"
},
"mixins": {
"rel": "mixins",
"href": "<URI identifying the resource associated with this node's mixins>"
},
"path": {
"rel": "path",
"href": "<URI identifying the URI to access this node by path>"
},
"children": {
"rel": "children",
"href": "<URI identifying the resource associated with this node's children>"
},
"parent": {
"rel": "parent",
"href": "<URI identifying the resource associated with this node's parent or itself if the node is the root node>"
},
"self": {
"rel": "self",
"href": "<URI identifying the resource associated with this node>"
},
"properties": {
"rel": "properties",
"href": "<URI identifying the resource associated with this node's properties>"
},
"type": {
"rel": "type",
"href": "<URI identifying the resource associated with this node's type>"
}
}
Note that it is possible for an API client to only request a subset of the complete structure. For example, a client might only be interested in properties for a given call and not care about the rest of the structure.
Properties representation
A node's properties are gathered within a properties
object that has the following structure:
// other node elements...
"properties" : {
<for each property>
<escaped property name> : <property representation>,
</for each property>
"_links" : {
"absolute" : {
"rel" : "absolute",
"href" : "<An absolute URL directly usable to retrieve this properties representation>"
},
"parent" : {
"rel" : "parent",
"href" : "<URI of the resource associated with the parent node of this properties sub-resource>"
},
"self" : {
"rel" : "self",
"href" : "<URI of the resource associated with this properties resource>"
}
}
},
// other node elements...
Each property is represented by an object with the following structure:
"name" : "<unescaped name>",
"multiValued" : <boolean indicating whether the property is multi-valued or not>
"reference": <boolean indicating whether the property value(s) point(s) to a node or not>
"value" : "<value>",
"type" : "<type>",
"_links" : {
"path" : {
"rel" : "path",
"href" : "<URI identifying the URI to access this property by path>"
},
"absolute" : {
"rel" : "absolute",
"href" : "<An absolute URL directly usable to retrieve this property's representation>"
},
"parent" : {
"rel" : "parent",
"href" : "<URI identifying the resource associated with this property's parent node>"
},
"self" : {
"rel" : "self",
"href" : "<URI identifying the resource associated with this property>"
},
"type" : {
"rel" : "type",
"href" : "<URI identifying the resource associated with this property definition>"
}
}
type
is the case-insensitive name of the JCR property type, and is one of: STRING
, BINARY
, LONG
, DOUBLE
, DATE
, BOOLEAN
, NAME
, PATH
, REFERENCE
, WEAKREFERENCE
, URI
, and DECIMAL
.
multiValued
specifies whether the property is multi-valued or not. Having this field allows for easier processing of properties on the client side without having to examine the property's definition.
The reference
field specifies whether or not the property is of type PATH
, REFERENCE
or WEAKREFERENCE
; in essence whether or not the property's value is actually a pointer to a node. While this is merely a convenience since that information can be inferred from the type
field, this makes client processing easier.
If a property is a reference (its reference
field is set to true
), an additional target
link is added to the _links
subsection, providing the URI identifying the resource identified by the path or reference value of the property.
Additionally, if a property is a reference and the resolveReferences
flag is set in the URI (using a query parameter appended to the URI), another references
subsection will be added to the property's representation containing basic information about the node(s) being referenced by the property value. Each entry in the references
object is identified using the identifier of the node being referenced. Note also that the resolveReferences
flag works properly with the includeFullChildren
one. See the example below for more details.
Examples
An example of the jcr:uuid
property of a /sites/mySite
node. jcr:uuid
is defined by the JCR specification as being defined by the mix:referenceable
mixin:
"name" : "jcr:uuid",
"value" : "039cdef3-289a-4fee-b80e-54da0ad35195",
"type" : "string",
"multiValued" : false,
"reference" : false,
"_links" : {
...
"self" : { "href" : "<basecontext>/default/en/nodes/039cdef3-289a-4fee-b80e-54da0ad35195/properties/jcr__uuid" },
"type" : { "href" : "<basecontext>/default/en/paths/jcr__system/jcr__nodeTypes/mix__referenceable/jcr__propertyDefinition" }
...
}
An example of the jcr:mixinTypes
property on a /sites/mySite
node.
"name" : "jcr:mixinTypes",
"multiValued" : true,
"value" : ["jmix:accessControlled" , "jmix:robots"],
"type" : "string",
"reference" : false,
"_links" : {
...
"self" : { "href" : "<basecontext>/default/en/nodes/039cdef3-289a-4fee-b80e-54da0ad35195/properties/jcr__mixinTypes" },
"type" : { "href" : "<basecontext>/default/en/paths/jcr__system/jcr__nodeTypes/nt__base/jcr__propertyDefinition" }
...
}
An example showing how indexed, same name properties URIs are represented, here the node type associated with the property's definition is the second property defined on the nt:base
node type:
"name" : "jcr:primaryType",
"value" : "jnt:virtualsite",
"multiValued" : true,
"type" : "string",
"reference" : false,
"_links" : {
...
"type" : { "href" : "<basecontext>/default/en/paths/jcr__system/jcr__nodeTypes/nt__base/jcr__propertyDefinition--2" }
...
}
An example showing how a j:defaultSite
reference property pointing to a /sites/mySite
node on a /sites
node is represented, demonstrating the target
field in the _links
section:
"name" : "j:defaultSite",
"value" : "09100a94-0714-4fb6-98de-351ad63773b2",
"multiValued" : false,
"type" : "weakreference",
"reference" : true,
"_links" : {
...
"type" : { "rel" : "type", "href" : "<basecontext>/default/en/paths/jcr__system/jcr__nodeTypes/jnt__virtualsitesFolder/jcr__propertyDefinition--3" },
"target" : { "rel" : "target", "href" : "http://api.example.org/sites/mySite" }
...
}
An example showing how the node pointed at by a reference property j:node
is resolved in the references
subsection of the property's representation when the resolveReferences
flag is used in the URI:
"_links": { ... },
"references": {
"5c82bcdc-b837-4ee0-a15a-c8d8d48a0916": {
_links: { ... },
name: "banner-earth.png",
type: "jnt:file",
path: "/sites/ACMESPACE/files/Images/Banner-home-slider/banner-earth.png",
id: "5c82bcdc-b837-4ee0-a15a-c8d8d48a0916"
}
},
"name": "j:node",
"type": "WeakReference",
"path": "/sites/ACMESPACE/home/main/foo/j:node",
"multiValued": false,
"value": "5c82bcdc-b837-4ee0-a15a-c8d8d48a0916",
"reference": true
An example showing how the node pointed at by a reference property j:node
is resolved in the references
subsection of the property's representation when the resolveReferences
flag is used in the URI, in conjunction with the includeFullChildren
flag:
"_links": { ... },
"references": {
5c82bcdc-b837-4ee0-a15a-c8d8d48a0916: {
_links: { ... },
"name": "banner-earth.png",
"type": "jnt:file",
"path": "/sites/ACMESPACE/files/Images/Banner-home-slider/banner-earth.png",
"mixins": { ... },
"versions": { ... },
"properties": { ... },
"children": { ... },
"id": "5c82bcdc-b837-4ee0-a15a-c8d8d48a0916"
}
},
"name": "j:node",
"type": "WeakReference",
"path": "/sites/ACMESPACE/home/main/foo/j:node",
"multiValued": false,
"value": "5c82bcdc-b837-4ee0-a15a-c8d8d48a0916",
"reference": true
Mixins representation
A node's attached mixins information is gathered within a mixins
object on the node's representation, as follows:
// other node elements...
"mixins" : {
<for each mixin>
<escaped mixin name> : <mixin representation>,
</for each mixin>
"_links" : {
"absolute" : {
"rel" : "absolute",
"href" : "<An absolute URL directly usable to retrieve this mixins sub-resource representation>"
},
"parent" : {
"rel" : "parent",
"href" : "<URI of the resource associated with the parent node of this mixins sub-resource>"
},
"self" : {
"rel" : "self",
"href" : "<URI of the resource associated with this nmixins resource>"
}
}
},
// other node elements...
Here is the structure for a mixin representation:
"name" : <the mixin's unescaped name>,
"_links" : {
"absolute" : {
"rel" : "absolute",
"href" : "<An absolute URL directly usable to retrieve this mixin's representation>"
},
"self" : {
"rel" : "self",
"href" : "<URI identifying the resource associated with the mixin in the context of the enclosing node>"
},
"type" : {
"rel" : "type",
"href" : "<URI identifying the resource associated with the mixin's node type>"
}
}
Examples
Given the following mixin definition:
[jmix:robots] mixin
extends=jnt:virtualsite
- robots (string, textarea) = 'User-agent: *'
we would get, assuming it is attached to the 49bf6a13-96a8-480a-ae8a-2a82136d1c67
node, a representation similar to:
"_links" : {
"absolute" : {
"rel" : "absolute",
"href" : "http://localhost:8080/modules/api/jcr/v1/default/en/nodes/49bf6a13-96a8-480a-ae8a-2a82136d1c67/mixins/jmix__robots"
},
"self" : {
"rel" : "self",
"href" : "/api/jcr/v1/default/en/nodes/49bf6a13-96a8-480a-ae8a-2a82136d1c67/mixins/jmix__robots"
},
"type" : {
"rel" : "type",
"href" : "/api/jcr/v1/default/en/paths/jcr__system/jcr__nodeTypes/jmix__robots"
}
},
"name" : "jmix:robots",
"properties" : {
"j:robots" : "String"
},
"type" : "jmix:robots"
To attach this mixin to an existing 49bf6a13-96a8-480a-ae8a-2a82136d1c67
node, a client would perform the following, creating a new jmix__robots
resource in the mixins
collection resource, using a PUT
request:
PUT /api/jcr/v1/default/en/nodes/49bf6a13-96a8-480a-ae8a-2a82136d1c67/mixins/jmix__robots HTTP/1.1
Host: api.example.org
"properties" : {
"j__robots" : {
"value" : "User-agent: *"
}
}
Children representation
Children of a given node are gathered within a children
object, as follows:
// other node elements...
"children" : {
<for each child>
<escaped child name> : <child representation>,
</for each child>
"_links" : {
"absolute" : {
"rel" : "absolute",
"href" : "<An absolute URL directly usable to retrieve this children sub-resource representation>"
},
"parent" : {
"rel" : "parent",
"href" : "<URI identifying the resource associated with the parent node>"
},
"self" : {
"rel" : "self",
"href" : "<URI identifying the resource associated with the parent's node children resource>"
}
}
},
// other node elements...
Each child is represented by an object providing only minimal information about the child: its name, its primary node type and its associated URIs (for both associated node, node type and parent node resources):
"name" : <unescaped child name>,
"type" : <nodetype name>,
"id" : <identifier of the child node>,
"_links" : {
"absolute": {
"rel": "absolute",
"href": "<An absolute URL directly usable to retrieve this node's representation>"
},
"path": {
"rel": "path",
"href": "<URI identifying the URI to access this node by path>"
},
"parent": {
"rel": "parent",
"href": "<URI identifying the resource associated with this node's parent>"
},
"self": {
"rel": "self",
"href": "<URI identifying the resource associated with this node>"
},
"type": {
"rel": "type",
"href": "<URI identifying the resource associated with this node's type>"
}
}
As of version 1.1 of the API, we've added the option to include a full representation for each child instead of simply outputting minimal information as above. This means that now children representations can now be equivalent to that of nodes. This fuller representation is, however, currently limited to only one level of hierarchy, meaning that children of children will use the regular minimal representation. This feature is activated by providing the includeFullChildren
query parameter on any API URI. If present, this query parameter is assumed to have a true
value, unless a false
value is explicitly provided, which corresponds to the default behavior. Any other value is understood to be true
.
Also as of version 1.1 of the API, we've added the option to filter children that are returned when asking for a node's children. This is accomplished by providing the childrenNodeTypes
query parameter which value is a concatenation of accepted node type names, separated by a comma (','). If this query parameter is present and its value corresponds to a valid list of node type names, only children of the corresponding node type(s) will be output in the node's representation.
Example
Below is the representation of a tags
child element of a /sites/mySite
node, within the context of the enclosing node children
element:
// ...
"children" : {
"tags" : {
"_links" : {
"path" : {
"rel" : "path",
"href" : "/api/jcr/v1/default/en/paths/sites/mySite/tags"
},
"absolute" : {
"rel" : "absolute",
"href" : "http://localhost:8080/modules/api/jcr/v1/default/en/nodes/e3a6e425-0afa-490b-a319-514db66eea04"
},
"parent" : {
"rel" : "parent",
"href" : "/api/jcr/v1/default/en/nodes/49bf6a13-96a8-480a-ae8a-2a82136d1c67"
},
"self" : {
"rel" : "self",
"href" : "/api/jcr/v1/default/en/nodes/e3a6e425-0afa-490b-a319-514db66eea04"
},
"type" : {
"rel" : "type",
"href" : "/api/jcr/v1/default/en/paths/jcr__system/jcr__nodeTypes/jnt__tagList"
}
},
"name" : "tags",
"type" : "jnt:tagList",
"id" : "e3a6e425-0afa-490b-a319-514db66eea04"
},
// ...
"_links" : {
"absolute" : {
"rel" : "absolute",
"href" : "http://localhost:8080/modules/api/jcr/v1/default/en/nodes/49bf6a13-96a8-480a-ae8a-2a82136d1c67/children"
},
"parent" : {
"rel" : "parent",
"href" : "/api/jcr/v1/default/en/nodes/49bf6a13-96a8-480a-ae8a-2a82136d1c67"
},
"self" : {
"rel" : "self",
"href" : "/api/jcr/v1/default/en/nodes/49bf6a13-96a8-480a-ae8a-2a82136d1c67/children"
}
},
// ...
If the includeFullChildren
flag is provided on the URI, we would get the following representation for the tags
child:
// ...
"children" : {
"tags" : {
"_links" : { ... },
"path": "/sites/mySite/tags",
"name" : "tags",
"type" : "jnt:tagList",
"id" : "e3a6e425-0afa-490b-a319-514db66eea04",
"mixins": { ... },
"versions": { ... },
"properties": { ... },
"children": { ... } // children would only contain minimal information as "expanding" children is limited to one hierarchic level
},
// ...
"_links" : {
"absolute" : {
"rel" : "absolute",
"href" : "http://localhost:8080/modules/api/jcr/v1/default/en/nodes/49bf6a13-96a8-480a-ae8a-2a82136d1c67/children"
},
"parent" : {
"rel" : "parent",
"href" : "/api/jcr/v1/default/en/nodes/49bf6a13-96a8-480a-ae8a-2a82136d1c67"
},
"self" : {
"rel" : "self",
"href" : "/api/jcr/v1/default/en/nodes/49bf6a13-96a8-480a-ae8a-2a82136d1c67/children"
}
},
// ...
Assuming we have a node aNode
with children of type ns:foo
, ns:bar
and ns:baz
, adding ?childrenNodeTypes=ns:foo,ns:bar
to the node's URI will only return the ns:foo
and ns:bar
children, omitting any ns:baz
ones.
Special considerations for same-name siblings
It is possible for a node type to specify that instances of this node type allow multiple children with the same name. They are then identified using a 1-based index representing the relative position of the child compared to other instances of same-named children. They are created using the children
resource of the parent node and are appended at the end of the parent node's children collection. Their URIs and escaped names use the --<index>
suffix convention we discussed previously.
For example, assuming a foo
node allows for multiple bar
children:
# Adding a bar child
PUT <basecontext>/default/en/nodes/foo/children/bar HTTP/1.1
Host: api.example.org
// bar content
# Response
HTTP/1.1 200 OK
Content-Type: application/hal+json
"name" : "foo",
// ...
"children" : {
"bar" : {
"name" : "bar",
"type" : "bar:nodeType",
// ...
},
// ...
}
# Adding a bar child
PUT <basecontext>/default/en/nodes/foo/children/bar HTTP/1.1
Host: api.example.org
// another bar child content
# New response
HTTP/1.1 200 OK
Content-Type: application/json
"name" : "foo",
// ...
"children" : {
"bar" : {
"name" : "bar",
"type" : "bar:nodeType",
// ...
},
"bar--2" : {
"name" : "bar",
"type" : "bar:nodeType",
// ...
},
// ...
}
Versions representation
A node's versions are gathered within a versions
object as follows:
// other node elements...
"versions" : {
<for each version>
<escaped version name> : <version representation>,
</for each version>
"_links" : {
"absolute" : {
"rel" : "absolute",
"href" : "<An absolute URL directly usable to retrieve this versions sub-resource representation>"
},
"parent" : {
"rel" : "parent",
"href" : "<URI identifying the resource associated with this versions sub-resource's parent>"
},
"self" : {
"rel" : "self",
"href" : "<URI identifying the resource associated with this node>"
}
},
},
// other node elements...
Each version is represented as follows:
"_links" : {
"absolute" : {
"rel" : "absolute",
"href" : "<An absolute URL directly usable to retrieve this version's representation>"
},
"self" : {
"rel" : "self",
"href" : "<URI of the resource associated with this version>"
},
"nodeAtVersion" : {
"rel" : "nodeAtVersion",
"href" : "<URI corresponding to the node data (frozen node) associated with this version>"
},
"next" : {
"rel" : "next",
"href" : "<URI of the linear successor of this version if it exists (otherwise this link is skipped)>"
},
"previous" : {
"rel" : "previous",
"href" : "<URI of the linear predecessor of this version if it exists (otherwise this link is skipped)>"
}
},
"name" : "<unescaped name of this version>",
"created" : <creation time of this version>
API entry points
The goal of this API is that you should be able to operate on its data using links provided within the returned representations. However, you still need to be able to retrieve that first representation to work with in the first place.
Base context
Since the API implementation is deployed as a module, it is available on your Jahia instance under the /modules
context with the /api
specific context. Therefore, all URIs targeting the API will start with /modules/api
. Keep this in mind while looking at the examples below since we might not repeat the base context all the time.
We further qualify the base context by adding /jcr/v1
to specify that this particular API deals with the JCR domain and is currently in version 1. The scoping by domain allows us to potentially expand the API's reach to other aspects in the future while we also make it clear which version (if/when several versions are needed) of that particular domain API is being used.
<basecontext>
will henceforth refer to the /modules/api/jcr/v1
base context below.
Authentication
Access to the JCR content is protected and different users will be able to see different content. It is therefore required to authenticate properly before using the API. In fact, some errors (usually PathNotFoundException
s) can be the result of attempting to access a node for which you don't have proper access level. There are several options to log into Jahia from clients.
If you use a browser and log in, the API will use your existing session.
From a non-browser client, you have different options. You can programatically log in using the /cms/login
URL, POST
ing to it as follows: <your Jahia root context>/cms/login?doLogin=true&restMode=true&username=<user name>&password=<user password>&redirectActive=false
For example, if you're using cURL, you would do it as follows: curl -i -X POST --cookie-jar cookies.txt '<Jahia context>/cms/login?doLogin=true&restMode=true&username=<user name>&password=<user password>&redirectActive=false'
Note that we're using the cookie-jar
option. This is needed to record the session information sent back by the server as we then need to provide it for each subsequent request using this mode.
Alternatively, you can use the Authentication
HTTP header, using the Basic
authentication mode. You can generate a Basic
authentication token using your credentials and then provide it using the Authentication
header with each request to the API. See Client side Basic HTTP Authentication for more details on how to do this.
API version
You can access the version of the API implementation performing a GET
on the <basecontext>/version
URI. This returns plain text information about both the version of the API and of the currently running implementation. This can also serve as a quick check to see if the API is currently running or not.
As of version 1.2, if your client requests application/json
content, you can also retrieve a JSON representation of the version information from that same URI, as follows:
{
"api": <API version>,
"module": <API module implementation version>,
"commit": {
"id": <GIT commit identifier>,
"branch": <commit branch name>
}
}
Workspace and language
You can access all the different workspaces and languages available in the Jahia JCR repository. However, you must choose a combination of workspace and language at any one time to work with JCR data. Which workspace and language to use are specified in the URI path, using first, the escaped workspace name followed by the language code associated with the language you wish to retrieve data in.
Therefore, all URIs targeting JCR data will be prefixed as follows: <basecontext>/<workspace name>/<language code>/<rest of the URI>
In the following sections, we detail the different types of URIs the API responds to. We will use <placeholder>
or {placeholder}
indifferently to represent place holders in the different URIs. Each section will first present the URI template using the JAX-RS @Path
syntax for URIs which is quite self-explanatory for anyone with regular expression knowledge. We will then detail each part of the URI template, specify the expected result, define which options if any are available and, finally, which HTTP operations can be used on these URIs. Since we already talked about the workspace and language path elements, we won't address them in the following.
Error reporting
Should an error occur during the processing of an API call, a response using an appropriate HTTP error code should be returned with a JSON body providing some context and details about what went wrong in the form of a JSON object with the following format:
{
"exception": <type of the exception that occurred as a fully qualified Java exception name if available>,
"message": <associated message if any>,
"operation": <type of operation that triggered the error>,
"nodeAccess": <how the node on which the error was triggered was accessed: 'byId' if using the nodes entry point or 'byPath' if the paths entry point was used>,
"idOrPath": <identifier if nodeAccess is byId or path if nodeAccess is byPath of the node that caused the issue>,
"subElementType": <type of sub-element requested if any>,
"subElements": <list of requested sub-elements if any>,
"data": <JSON data that was provided in the request>
}
Currently the following types of operations exist: read
, createOrUpdate
, delete
which map to GET
, PUT
and DELETE
requests respectively and upload
which corresponds to the POST
-performed upload method.
Operating on nodes using their identifier
URI template
/{workspace}/{language}/nodes/{id: [^/]*}{subElementType: (/children|mixins|properties|versions)?}{subElement: .*}
URI elements
nodes
: path element marking access to JCR nodes from their identifier{id: [^/]*}
: the identifier of the node we want to operate on, which is defined as all characters up to the next/
character{subElementType: (/children|mixins|properties|versions)?}
: an optional sub-element type to operate on the identified node's sub-resources as defined in the URI Design section{subElement: .*}
: an optional sub-element escaped name to operate on a specific sub-resource of the identified node
If no subElementType
path element is provided then no subElement
path element can be provided either and the resource on which the API will operate is the node identified by the specified id
path element.
If a subElementType
path element is provided but no subElement
path element is provided, then the API will operate on the collection of specified type of child resources for the node identified by the specified id
path element.
If a subElementType
path element is provided and a subElement
path element is provided, then the API will operate on the child resource identified by the subElement
path element for the node identified by the specified id
path element.
Allowed HTTP operations
GET
: to retrieve the identified resourcePUT
: to create (if it doesn't already exist) or update the identified resourceDELETE
: to delete the identified resourcePOST
:- to create a new child without providing a name for it, leaving it up to the server to create an appropriate one (starting with v1.2 of the API)
- to rename a resource but leave it at the same spot in the hierarchy using the
moveto
sub-resource
Accepted data
PUT
operations accept JSON representations of the objects that the method intends to update or create, meaning a PUT
to create a property must provide a valid JSON representation of a property, a PUT
to update a node must provide in the body of the request a valid JSON representation of a node. As mentioned before, this representation only needs to be partial, containing only the information that is required to complete the operation.
DELETE
operations accept a JSON array of String identifiers of elements to be batch-deleted. This way several elements can be deleted in one single call.
Examples
GET <basecontext>/default/en/nodes/
will retrieve the root node of the default
workspace using its English version when internationalized exists.
GET <basecontext>/live/fr/nodes/children
will retrieve only the children of the root node in the live
workspace using the French version.
PUT <basecontext>/default/en/nodes/27d671f6-9c75-4604-8f81-0d1861c5e302/children/foo
with the {"type" : "jnt:bigText", "properties" : {"text" : {"value" : "FOO!"}}}
JSON body data will create a new node of type jnt:bigText
named foo
with a text
property set to FOO!
and add it to the children of the node identified with the 27d671f6-9c75-4604-8f81-0d1861c5e302
identifier. Note that this assumes that such a child can be added on that particular node. For example, sending the same request to a node that doesn't accept jnt:bigText
children will result in a 500
error response with a body similar to the following one:
{
"exception": "javax.jcr.nodetype.ConstraintViolationException",
"message": "No child node definition for foo found in node /sites/ACMESPACE/home/slider-1/acme-space-demo-carousel",
"operation": "createOrUpdate",
"nodeAccess": "byId",
"idOrPath": "9aa720a1-23d4-454e-ac9c-8bfdf83a8351",
"subElementType": "children",
"subElements": ["foo"],
"data": {
"type": "jnt:bigText",
"properties": {
"text": {
"multiValued": false,
"value": "FOO BAR!",
"reference": false
}
}
}
}
This body response provides some details as to why this particular operation failed.
PUT <basecontext>/default/en/nodes/27d671f6-9c75-4604-8f81-0d1861c5e302/properties/foo
with the {"value" : "bar"}
JSON body data will create (or update if it already exists) the foo
property of the node identified by the 27d671f6-9c75-4604-8f81-0d1861c5e302
identifier and sets its value to bar
.
PUT <basecontext>/default/en//nodes/eae598a3-8a41-4003-9c6b-f31018ee0e46/mixins/jmix__rating
with the {"properties" : {"j__lastVote": {"value": "-1"}, "j__nbOfVotes": {"value": "100"}, "j__sumOfVotes": {"value": "1000"}}}'
JSON body data will add the jmix:rating
mixin on the eae598a3-8a41-4003-9c6b-f31018ee0e46
node, initializing it with the following properties values: j:lastVote
set to -1
, j:nbOfVotes
set to 100
and j:sumOfVotes
set to 1000
. If the mixin already existed on this node then the properties would only be updated to the specified values. Once that jmix:rating
is added to the node, you can then modify its properties as you would do with "normal" properties. For example, PUT <basecontext>/default/en/nodes/eae598a3-8a41-4003-9c6b-f31018ee0e46/properties/j__sumOfVotes
with the {"value": "3000"}
JSON body data will update the j:sumOfVotes
property to 3000
.
DELETE <basecontext>/default/en/nodes/eae598a3-8a41-4003-9c6b-f31018ee0e46/mixins/jmix__rating
will remove the mixin from the node, while
DELETE <basecontext>/default/en/nodes/eae598a3-8a41-4003-9c6b-f31018ee0e46/properties/j__sumOfVotes
will just remove the j:sumOfVotes
property.
DELETE <basecontext>/default/en/nodes/eae598a3-8a41-4003-9c6b-f31018ee0e46/properties
with the ["j__sumOfVotes", "j__nbOfVotes"]
will delete both j:sumOfVotes
and j:nbOfVotes
properties.
POST <basecontext>/default/en/nodes/eae598a3-8a41-4003-9c6b-f31018ee0e46/moveto/newName
will rename the eae598a3-8a41-4003-9c6b-f31018ee0e46
node to newName
, leaving it at the same spot in the JCR tree.
As of version 1.2 of the API, POST <basecontext>/default/en/nodes/eae598a3-8a41-4003-9c6b-f31018ee0e46/children
with the appropriate JSON body (specifying at least the child's type and omitting its name) will add a new child to eae598a3-8a41-4003-9c6b-f31018ee0e46
, automatically generating an appropriate name for it (using its jcr:title
property if provided or using its type if not)
Operating on nodes using their path
URI template
/{workspace}/{language}/paths{path: /.*}
URI elements
paths
: path element marking access to JCR nodes from their path{path: /.*}
: the path of the resource to operate one
The path
path element should contain the absolute path to a given JCR node with optional sub-element resolution if one of the child resource names defined in the URI Design section is found. Note that once a sub-element is found, the node resolution will occur up to that sub-element and the resolution of the sub-element will happen using the next path element, all others being discarded.
Allowed HTTP operations
GET
: to retrieve the identified resourcePUT
: to create (if it doesn't already exist) or update the identified resource (starting from v1.1 of the API)DELETE
: to delete the identified resource (starting from v1.1 of the API)POST
:- to create a new child without providing a name for it, leaving it up to the server to create an appropriate one (starting with v1.2 of the API)
- to upload a file as a child node of the identified resource using
multipart/form-data
content type and specifying the file to upload using thefile
parameter
Examples
<basecontext>/default/en/paths/users/root/profile
resolves to the /users/root/profile
node in the default
workspace using the en
language.
<basecontext>/live/fr/paths/sites/foo/properties/bar
resolves to the French (fr
language) version of the bar
property of the /sites/foo
node in the live
workspace.
<basecontext>/live/fr/paths/sites/foo/properties/bar/baz/foo
also resolves to the French version of the bar
property of the /sites/foo
node since only the next path element is considered when a sub-element type if found in one of the path elements of the considered URI.
POST <basecontext>default/en/paths/users/root/files
using multipart/form-data
content type and specifying the file to upload using the file
parameter will upload the specified file to the /users/root/files
content directory. Using cURL: curl -i -H "Authorization:Basic cm9vdDpyb290MTIzNA==" -F file=@file.png <basecontext>/default/en/paths/users/root/files
. The API will respond with the new node's representation, including its links and identifier so that you can further work with it.
Retrieving nodes using their type
Version 1.1.1 of the API restricts the types endpoint to limit security exposure. It is therefore disabled by default. Its activation is controlled by the value of the jahia.find.disabled
property that can be set in the digital-factory-config/jahia/jahia.properties
properties file of your Jahia install. Please refer to the Jahia documentation for more details.
URI template
/{workspace}/{language}/types/{type}
URI elements
types
: path element marking access to JCR nodes from their type{type}
: the escaped name of the type of JCR nodes to retrieve
Options
Options are specified using query parameters in the URI, further refining the request. Here is the list of available query parameters:
nameContains
: a possibly multi-valued String (by passing the query parameter several time in the URI) specifying which String(s) the retrieved nodes must contain in their name. This is anAND
constraint so further value of this parameter further limit the possible names.orderBy
: a String specifying whether returned nodes should be ordered by ascending order (the default) or by descending order if thedesc
value is passed.limit
: an integer specifying how many nodes should be returned at mostoffset
: an integer specifying how many nodes are skipped so that paging can be implementeddepth
: an integer specifying whether the returned nodes hierarchy is expanded to include sub-elements or not (default is0
so no sub-elements included)
Allowed HTTP operations
GET
: to retrieve the identified nodes
Examples
GET <basecontext>/default/en/types/genericnt__event?nameContains=jahia
will retrieve all the genericnt:event
nodes which name contains jahia
.
GET <basecontext>/default/en/types/genericnt__event?nameContains=rest&offset=5&limit=10
will retrieve at most 10 genericnt:event
nodes starting with the 6th one and which name contains rest
.
Querying nodes
Version 1.1.1 of the API restricts the query endpoint introduced in version 1.1 to limit security exposure. It is therefore disabled by default. Its activation is controlled by the value of the jahia.find.disabled
property that can be set in the digital-factory-config/jahia/jahia.properties
properties file of your Jahia install. Please refer to the Jahia documentation for more details.
URI template
/{workspace}/{language}/query
URI elements
query
: path element marking access to the query endpoint
Allowed HTTP operations
POST
: to query the JCR repository
Accepted data
The query endpoint accepts JSON data consisting of the following object structure:
{
"query" : <A String representing a valid JCR-SQL2 query>,
"limit" : <An optional Integer specifying the maximum number of to retrieve>,
"offset": <An optional Integer specifying the starting index of the elements to retrieve to allow for pagination>
}
As of v1.1.1 of the API, the query endpoint is disabled by default and the preferred way to query nodes is to register prepared queries that are checked by administrators of the server and are deemed "safe". These prepared queries are still available even when the default query endpoint is de-activated since they have been vetted. The accepted JSON input has therefore evolved to support these prepared queries as follows:
{
"query" : <An optional String representing a valid JCR-SQL2 query>,
"queryName": <An optional String identifying a registered prepared query>,
"parameters": <An optional array of Strings providing values for parameter placeholders (the '?' character) in the prepared query>,
"namedParameters": <An optional dictionary of String -> Object providing values for named parameters in the prepared query>,
"limit" : <An optional Integer specifying the maximum number of to retrieve>,
"offset": <An optional Integer specifying the starting index of the elements to retrieve to allow for pagination>
}
The query
value is still supported as previously. However, it will only be taken into account if and only if the query endpoint is activated and no queryName
value is provided. Otherwise, the API implementation will look for a prepared query registered under the provided queryName
value. A prepared query can specify placeholders to dynamically provide values when the query is run. This is accomplished using two different means. First, for simple cases, you can use the ?
character as a placeholder for values to be provided later. In that case, you will need to provide a parameters
array of values to replace these placeholders. Order is significant since placeholders are replaced by the provided values in order, the first placeholder being replaced by the first provided value, etc. The other, and preferred, option for complex queries, is to use named parameters with placeholders in the form of a parameter name prefixed by a column (:
) and preceded by a space. Parameter names can contain only alpha-numerical and underscore (_
) characters. In this case, you will need to provide a namedParameters
dictionary providing a mapping between a parameter name and its associated value.
The prepared query will thus be interpolated using the provided values for the parameters and then limited and offset if needed.
Prepared queries are registered using your module Spring context by defining PreparedQuery
beans. You will therefore need your module to depend on the jcrestapi
module. You need to provide values for the name
and source
properties, name
being the name of the prepared query with which you can access it from the query endpoint and the source
property being the query itself, using the appropriate placeholders (either ?
or named parameters as you see fit, but you cannot mix both in the same query).
Examples
POST <basecontext>/default/en/query
providing the following body {query: "SELECT * FROM [nt:base]", limit: 10, offset: 1}
will result in retrieving 10 nodes starting with the second one (i.e. bypassing the first one since the offset is not 0). Note that this query will only work if the query endpoint has been activated (it is disabled by default).
Assuming you've registered the following prepared query in your module's Spring context:
<bean id="foo" class="org.jahia.modules.jcrestapi.api.PreparedQuery">
<property name="name" value="foo"/>
<property name="source" value="select * from [nt:nodeType] where [jcr:nodeTypeName] like ?"/>
</bean>
You can execute the query using POST <basecontext>/default/en/query
with the following body {"queryName": "foo", "parameters": [ "nt:%" ] }
to execute the following interpolated query on the English version of the default
workspace: select * from [nt:nodeType] where [jcr:nodeTypeName] like 'nt:%'
.
If, on the other hand, you used the named parameter option:
<bean id="foo" class="org.jahia.modules.jcrestapi.api.PreparedQuery">
<property name="name" value="foo"/>
<property name="source" value="select * from [nt:nodeType] where [jcr:nodeTypeName] like :nodeType"/>
</bean>
You could run the same query using the same request providing the following body this time: {"queryName": "foo", "namedParameters": { "nodeType": "nt:%" } }
JAX-RS endpoints
Developing JAX-RS endpoints for Jahia
Jahia provides a mechanism to publish and register JAX-RS endpoints as OSGi bundles so that Jahia can treat them as regular Jahia modules. This capability provides yet another extension to the Jahia platform by allowing you to create custom, RESTful access to your Jahia content should you find yourself not able to accomplish your task using the other Jahia capabilities.
Architecture overview
Jahia comes bundled with Jersey, which is the JAX-RS reference implementation. Jahia also developed an OSGi extender specifically for JAX-RS endpoints. We based our implementation on Neil Bartlett's JAX-RS Extender Bundle for OSGi that we updated to be able to use JAX-RS 2 and Jersey, along with modifications to make it work seamlessly with Jahia's idiosyncracies. All this means is that you can simply create a JAX-RS endpoint and declaratively register it in Jahia so that it is available to the rest of the platform and any client applications you might have.
Creating your endpoint
Your JAX-RS endpoint needs to be packaged as an OSGi bundle. For more details on OSGi bundles and Digital factory modules, please refer to the Jahia modules documentation.
Since the endpoint is bundled as a JAR file, it has some consequences on what can be done in terms of configuring the application since the web.xml
file will not be processed as would be the case for a regular web application. We therefore recommend that you configure your endpoint using the JAX-RS Application
mechanism by providing an instance of an Application
implementation providing the adequate configuration for your endpoint. Fortunately, Jersey makes this process rather easy thanks to its ResourceConfig
class that your Application
implementation can extend. Please refer to the Application Deployment and Runtime Environments section of the Jersey documentation for more details.
You then need to tell the JAX-RS extender about your Application
implementation so that it can be processed. You also need to specify under which context you want your endpoint to be registered. Note that since your endpoint will be implemented as a Jahia module, the context that you specify actually refers to a sub-context of the Jahia /modules
context under which all modules are deployed. All this is accomplished declaratively using specific instructions passed to the maven-bundle-plugin
: you specify the fully qualified named of your JAX-RS Application
implementation using the JAX-RS -Application
directive while the context you want your endpoint to be registered under is specified using the JAX-RS-Alias
directive. You can see an example of how this is done with maven below:
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<instructions>
<Jahia-Depends>default,...</Jahia-Depends>
<Private-Package>
...
</Private-Package>
<Import-Package>
...
</Import-Package>
<JAX-RS-Alias>{the context under which you wish your endpoint to be registered}</JAX-RS-Alias>
<JAX-RS-Application>{your Application implementation fully qualified class name}</JAX-RS-Application>
</instructions>
</configuration>
</plugin>
If you are not using maven, you can accomplish the same result by modifying the MANIFEST
file for your bundle to add these directives. When the JAX-RS OSGi extender processes your bundle, it looks for these directives to perform the necessary operations to register your endpoint with Jahia.
Accessing your endpoint
Assuming you registered your endpoint under the /endpoint
context using the JAX-RS-Alias
directive, it will accessible under the /modules/endpoint
context of your Jahia installation. It will also benefit from all the standard functionality provided by Jahia to its modules, in particular, you can authenticate to Jahia and your credentials will be propagated to your endpoint.