In my previous blog I explored some of the new data validation features of the Nuxeo Platform 7.2. We created a simple application called the “Collaborative Showroom”, where we were able to collect and validate data coming from Internet customers. Today, I will talk about how to publish that content by effectively using the existing JSON REST API. Let’s get started!
The “Collaborative Showroom” Application
The main purpose of this application is to collect content from the customers of an existing e-commerce website and publish it. Our example is an online kitchen reseller who wants to provide some kitchen installation examples to their customers.
Collaborative Showrooms for an E-commerce Website
The source code of this application is available on github.
$ git clone https://github.com/nuxeo-sandbox/nuxeo-demo-collaborative-showroom.git #Collaborative Showroom source code
$ cd nuxeo-demo-collaborative-showroom
$ mvn clean install
$ cp target/*.jar path/to/nuxeo/nxserver/bundles/
=> Start Nuxeo and go to http://localhost:8080/nuxeo/site/showroom/products
To understand how it works, start with the MANIFEST.MF file and the corresponding contribs
Let’s Display the Showrooms
Showroom - Showing pictures
“Nuxeo Fake Kitchen Reseller” is very famous and its website has a lot of visitors. So, performance is a primary factor. We also have to provide the easiest way to integrate the showrooms.
To get this, we’d like to make a single call to a JSON REST service. This call will be done after the page load, asynchronously. The result will contain the showrooms for the entire kitchen present on the current page. Each picture will be associated with its metadata: the categorization, information about the authors and information about the related kitchen.
Showroom - Logical Architecture - Show pictures
The data we’d like to get:
- Kitchen’s reference #1:
- Picture #1:
- File
- URL
- Size
- Category:
- Main category name
- Sub category name
- Author:
- Firstname
- Lastname
- Kitchen’s data (optional: already present in the website but if present here, it will simplify the JS integration):
- Reference
- Title
- File
- Picture #2:
- …
- Picture #1:
- Kitchen’s reference #2:
- …
Nuxeo Base Capabilities
Nuxeo provides an endpoint to get the documents as JSON. The generated JSON is widely configurable. For example, if we stored a Showroom document in /default-domain/worskspaces/showrooms/kitchen1picture1, we could make the following call:
- GET
http://localhost:8080/nuxeo/api/v1/path/default-domain/worskspaces/showrooms/kitchen1picture1
- Header “Accept” = “application/json”
This will return a JSON description of the document:
{
"entity-type": "document",
"repository": "default",
"uid": "3efaad50-e6cc-4c8e-ba86-3afe41a3ca78",
"path": "/default-domain/workspaces/showrooms/kitchen1picture1",
"title": "Picture #1 for Kitchen #1",
"type": "ShowroomEntry",
"state": "undefined",
"parentRef": "fe6e1bb0-d85f-4292-81f7-ece77ecebda0",
"isCheckedOut": true,
"changeToken": "1428524410600",
"lastModified": "2015-04-08T20:20:10.60Z",
"facets": [
"Versionable",
"Publishable",
"Commentable",
"HasRelatedText",
"Thumbnail",
"Downloadable"
]
}
We could have a parameter to get the information we need:
- GET http://localhost:8080/nuxeo/api/v1/path/default-domain/worskspaces/showrooms/kitchen1picture1
- Header “Accept” = “application/json”
- Parameter “properties” = “dublincore,file,productReference”
This loads the given schemas and provides the corresponding data:
{
// the same information
"entity-type":"document",
"title":"Feedback for product #10000",
...
// the loaded schemas
"properties":{
// data about the picture</span>
"file:content":{
"name":"kitchen.jpg",
"mime-type":"image/jpeg",
"encoding":null,
"digest":"060a35006bcc09215524273541c97418",
"length":"1364623",
"data":"http://localhost:8080/nuxeo/nxbigfile/default/3efaad50-e6cc-4c8e-ba86-3afe41a3ca78/file:content/kitchen.jpg"
},
// the kitchen's reference
"pdt:product":10000,
// dublincore datas
"dc:modified":"2015-04-08T20:20:10.60Z",
"dc:creator":"nchapurlat",
"dc:subjects":[
"art/photography"
],
...
}
}
We got the username ( “dc:creator”:”nchapurlat” ) and the category’s id (“dc:subject”:[“art/photography”] ). This information is not very useful for the user experience. We need the firstname and the lastname of the user and a “human readable” label for the category. We can complete our call with parameters to fetch the user’s data and the category’s information:
- GET http://localhost:8080/nuxeo/api/v1/path/default-domain/worskspaces/showrooms/kitchen1picture1
- Header “Accept” = “application/json”
- Parameter “properties” = “dublincore,file,productReference”
- Parameter “fetch.document” = “dc:creator,dc:subjects”
- Parameter “fetch.directoryEntry” = “parent”
- Parameter “depth” = “max”
{
"entity-type":"document",
"title":"Feedback for product #10000", "properties":{
"file:content":{ ... },
"pdt:product":10000,
"dc:created":"2015-04-08T20:20:10.60Z",
// thanks to fetch.document=dc:creator, the author is expanded
"dc:creator":{
"entity-type":"user",
"id":"nchapurlat",
"properties":{
"firstName":"Nicolas",
"lastName":"Chapurlat", ...
},
},
// thanks to fetch.document=dc:subjects, the category also
"dc:subjects":[
{
"entity-type":"directoryEntry",
"directoryName":"l10nsubjects",
"properties":{
"id":"photography",
"label_en":"Photography",
// fetch.directoryEntry=parent load the parent
"parent":{
"entity-type":"directoryEntry",
"directoryName":"l10nsubjects",
"properties":{
"id":"art",
"label_en":"Art", ...
}
}, ...
}
}
],
...
}
}
A property in a schema is loadable if it’s defined as a reference (I already talked about references in the previous post) and if a JSON converter is registered for the corresponding Java type. Nuxeo provides JSON converters for users and directory’s entries, and dc:creator and dc:subjects are defined as references to a Nuxeo user and “l10nsubjects” directory. That’s why we were able to fetch dublincore’s creator and subjects. We added a parameter to get the parent of the “dc:subject” (fetch.directoryEntry=parent) but it would not have appeared if we didn’t add the depth=max parameter. Indeed, the JSON Marshalling system prevents the recursive data loading. By default, you can only fetch a single level of data. The “max” value for the “depth” parameter allows you to fetch 2 levels.
The result is nice, with a call to an existing URL and 4 parameters! But it does not fulfill the contract: The kitchen’s data is missing and we wanted a single call to load all the pictures of several kitchens.
Load the Kitchen’s Data
The Showroom document embed a schema named “productReference” which contains the kitchen’s reference. We defined this field as a reference to an existing kitchen (Product Java class). As I said previously, we’ll be able to load it in the document’s JSON if we provide a JSON converter for the Product class. Nuxeo provides an ubiquitous JSON Marshalling system. It’s available for use not only in JAX-RS but also outside the web context (in services, schedulers, listeners, etc.). It contains registered marshaller classes which manage the marshalling of a Java type from or to a given mimetype (JSON in our case). We have to contribute to the MarshallingRegistry, a ProductJsonWriter class which converts a Product object as JSON.
To implement the marshaller class, we just need to use the abstract class AbstractJsonWriter. The Product class is a simple POJO. We can just delegate its marshalling to jackson.
@Setup(mode = Instanciations.SINGLETON, priority = Priorities.REFERENCE)
public class ProductJsonWriter extends AbstractJsonWriter<Product> {
@Override
public void write(Product product, JsonGenerator jg) {
jg.writeObject(product);
}
}
Our marshaller is a singleton which manages the conversion of the Product class to JSON. It’s the default marshaller for Product (Priorities.REFERENCE). Now, we just need to register it and we’ll be able to load the Product right inside the document’s JSON.
<extension
target="org.nuxeo.ecm.core.io.MarshallerRegistry"
point="marshallers">
<register
class="org.nuxeo.demo.ProductJsonWriter" />
</extension>
Let’s add a new field to load the “fetch.document” parameter.
- GET http://localhost:8080/nuxeo/api/v1/path/default-domain/worskspaces/showrooms/kitchen1picture1
- Header “Accept” = “application/json”
- Parameter “properties” = “dublincore,file,productReference”
- Parameter “fetch.document” = “dc:creator,dc:subjects,pdt:product“
- Parameter “fetch.directoryEntry” = “parent”
- Parameter “depth” = “max”
The response now contains the whole Product data:
{
"entity-type":"document",
"title":"Feedback for product #10000",
...
"properties":{
"pdt:product":{
"reference":10000,
"description":"A nice kitchen which id is 10000",
"title":"Nuxeo Kitchen 10000",
"url":"http://localhost:8080/nuxeo/site/showroom/products/10000"
},
...
}
}
Exposing our Custom Endpoint
We’d like to expose a service which provides the pictures for a given list of products. We’ll use Nuxeo WebEngine to get all the benefits of both Nuxeo and JAX-RS. Our service handles the product as GET parameters and returns a Map where the keys are the product’s references and the values are the lists of documents. A single NXQL request allows to get all the required documents.
@GET
@Path("getPicturesGroupedByProduct")
@Produces(APPLICATION_JSON)
public Map<Long, DocumentModelList> getPicturesByProducts(
@QueryParam("ref") List<Long> references) {
if (references.isEmpty()) {
return new HashMap<Long, DocumentModelList>();
}
String refs = StringUtils.join(references.toArray(), ',');
String query = "SELECT * FROM ShowroomEntry "
+ " WHERE pdt:product IN (" + refs + ") "
+ " ORDER BY dc:modified desc";
CoreSession session = ctx.getCoreSession();
DocumentModelList list = session.query(query);
// organize the pictures by product
Map<Long, DocumentModelList> result = new HashMap<>();
for (DocumentModel doc : list) {
Long product = (Long) doc.getPropertyValue("pdt:product");
DocumentModelList docs = result.get(product);
if (docs == null) {
docs = new DocumentModelListImpl();
result.put(product, docs);
}
docs.add(doc);
}
return result;
}
The returned Java Type : Map<Long, DocumentModelList> is not yet managed by the marshalling system. We have to provide a marshaller for it. It’s quite simple since we can delegate the marshalling of the DocumentModelList to Nuxeo. We can create the marshaller the same way we did it for the Product class. In this case, the write method becomes:
public void write(Map<Long, DocumentModelList> map, JsonGenerator jg) {
jg.writeStartObject();
for (Map.Entry<Integer, DocumentModelList> entry : map.entrySet()) {
jg.writeFieldName(entry.getKey().toString());
// delegate the marshalling to Nuxeo
writeEntity(entry.getValue(), jg);
}
jg.writeEndObject();
}
Done! Our service is now ready to use. Its result contains DocumentModel so it supports the same parameters as the default endpoint.
- GET
http://localhost:8080/nuxeo/site/ourApp/getPicturesGroupedByProduct
- Header “Accept” = “application/json”
- Parameter “ref” = [ 10001 , 10002 , 10003 ]
- Parameter “properties” = “dublincore,file,productReference”
- Parameter “fetch.document” = “dc:creator,dc:subjects,pdt:product“
- Parameter “fetch.directoryEntry” = “parent”
- Parameter “depth” = “max”
If we want to lock the marshalling parameters from the server side, we can add the following code at the end of our JAX-RS method implementation:
RenderingContextBuilder builder = CtxBuilder.builder();
builder.properties("dublincore", "file", "productReference");
builder.fetchInDoc("dc:creator", "dc:subjects", "pdt:product");
builder.fetch("directoryEntry", "parent");
builder.depth(DepthValues.max);
String CTX_KEY = "_STORED_GENERATED_RENDERING_CONTEXT";
request.setAttribute(CTX_KEY, builder.get());
return result;
This way, no need to add any rendering parameters.
- GET
http://localhost:8080/nuxeo/site/ourApp/getPicturesGroupedByProduct
- Header “Accept” = “application/json”
- Parameter “ref” = [ 10001 , 10002 , 10003 ]
{
"10001":{
"entity-type":"documents",
"entries":[
// each entry contains the required data
{ "entity-type":"document", ... },
{ "entity-type":"document", ... },
...
]
},
"10002":{
"entity-type":"documents",
"entries":[
{ "entity-type":"document", ... },
...
]
},
"10003": ...
}
The initial contract is now fulfilled!
The Nuxeo Platform 7.2 provides a new JSON marshalling system. It’s parametrized, extensible and ubiquitous. In this blog, I exposed the way you can get all benefits of the existing Nuxeo marshalling and extend it. With a reduced amount of source code, we were able to publish a lot of information: a complex data structure which contains pictures, metadata, and data coming from a remote system (Kitchen’s data). All of this aggregated in one single JSON object.
I’m currently working on Nuxeo Studio with Arnaud Kervern to make it possible to configure your own fields easily as references to existing documents, directory entries, users or groups. After that, we will be working on custom references to easily integrate Nuxeo with your own Information System. I’ll probably update the “Collaborative Showroom” application to get all the benefits of these facilities. So, look out for my new blogs about the new Nuxeo Studio features!
More Resources
- Blog post: The first post: Automatic Data Validation Example (and Code Too!)
- Source code: Collaborative Showroom Application source code
- Documentation: JSON Marshalling
- Documentation: REST API
- Documentation: WebEngine (JAX-RS)
- Documentation: Field Constraints and Validation
- Documentation: How to Customize Document Validation