RESTful JCR access
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.
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:%" } }
Security
Access to the REST API is restricted by default and cannot be used directly by a non authenticated user in Live mode. The exception is the nodes of types jnt:folder, jnt:page, jnt:portlet, jnt:navMenuText (pages, folders, portlets and menu labels) located under sites (/sites/.*). If you need to open the API on some nodes, you'll have to provide a configuration file for the security filter module. The configuration file should define precisely what you want to open - you can specify the node types and path patterns that you want to allow.
permission.myApiAccess.api=jcrestapi
permission.myApiAccess.nodeType=nt:myNodeType1,nt:myNodeType2
permission.myApiAccess.pathPattern=/sites/mysites/nodes/.*
Nodes must match to be returned by the API.
The security filter module can add restrictions based on user permissions or the presence of token. In the following example, the user will have access to the API only if it has the jcr:write
permission.
permission.myApiAccess.api=jcrestapi
permission.myApiAccess.nodeType=nt:myNodeType1,nt:myNodeType2
permission.myApiAccess.pathPattern=/sites/mysites/nodes/.*
permission.myApiAccess.requiredPermission=jcr:write
Here access to the API is granted if the user provides a token with the myApp scope:
permission.myApiAccess.api=jcrestapi
permission.myApiAccess.nodeType=nt:myNodeType1,nt:myNodeType2
permission.myApiAccess.pathPattern=/sites/mysites/nodes/.*
permission.myApiAccess.scope=myApp
For more details on security filter configuration, see API Security service and filter on GitHub .
Example
Given the following mixin definition:
[jmix:robots] mixin
extends=jnt:virtualsite
- robots (string, textarea) = 'User-agent: *'
You could attach this mixin to an existing /sites/mySite
node, whose identifier is foo
, by creating a new jmix__robots
resource in the mixins
collection resource using a PUT request.
PUT /default/en/nodes/foo/mixins/jmix__robots HTTP/1.1
Host: api.example.org
"properties" : {
"robots" : {
"value" : "User-agent: Mozilla"
}
}
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.