Creating a Thumbnail Browsing Module with AngularJS and the REST API - Part 1


Thu 31 October 2013 By Laurent Doguin

We recently received the many photos taken during Nuxeo World 2013. And I needed a way to see all the photos easily. Meaning I did not care about the metadata, I just wanted to see the crazy faces of my fellow co-workers. So I decided to do a small browsing module. Which is the perfect opportunity to try AngularJS along with our REST API.

My goal is to be able to see all the pictures having a particular tag. The pictures have to be small and have to be viewable full size when clicking on them. And since we have a lot of pictures, I'll add infinite scrolling.

This first post will focus on how to use the REST API and the search and Business Object (BO) adapters. I'll write about the AngularJS integration in the second one.

How to Retrieve Documents with the API


So how do I get my documents given a facet (Picture) and a tag? Nuxeo has a built in search adapter. It lets you run an NXQL query or a fulltext search. This adapter has to be applied on a document. This might not sound particularly logical. Why would you need a document when you're actually trying to get one? The answer is because a document has an associated repository. And when you run a query, you do it against a particular repository. So it's just a way to choose against which repository your query will be run.

The simplest way to get a document with the new API is:

http://localhost:8080/nuxeo/api/v1/path/

This retrieves the root document of the default repository using its path '/'. The result should look like this:

[javascript]
{ "changeToken" : null,
"contextParameters" : { },
"entity-type" : "document",
"facets" : [ "Folderish" ],
"isCheckedOut" : true,
"path" : "/",
"repository" : "default",
"state" : null,
"title" : "7eaa8a0c-a0d8-4120-9aaa-c6cf80088d6f",
"type" : "Root",
"uid" : "7eaa8a0c-a0d8-4120-9aaa-c6cf80088d6f",
"versionLabel" : ""
}
[/javascript]

We have a document, now we can use the @search adapter. Using an adapter on a resource is easy. This is a resource:

http://localhost:8080/nuxeo/api/v1/path/

To use the search adapter, simply add @search to you URL:

http://localhost:8080/nuxeo/api/v1/path/@search

This adapter takes several query parameters. We can split them in two categories: those related to the search and those related to pagination.

About the Search

  • query: Any valid NXQL query. If you don't specify this parameter, Nuxeo will try to run a fulltext search using the fullText query parameter.
  • fullText: The search term for your fulltext search.
  • orderBy: An optional Order By clause will be append to your fulltext search.

This will run a the SELECT * FROM Document query against the default repository:

http://localhost:[email protected]?query=SELECT * FROM Document

This will run a fulltext search for the word searchTerm. Results will be ordered by modification date.

http://localhost:[email protected]?fullText=searchTerm&orderBy=dc:modified

The NXQL query will actually be SELECT * FROM Document WHERE ecm:fulltext = "searchterm" AND ecm:isCheckedInVersion = 0 AND ecm:path STARTSWITH "/" ORDER BY dc:modified.

If your document is the default domain instead of the root:

http://localhost:[email protected]?fullText=searchTerm&orderBy=dc:modified

Your query will look like SELECT * FROM Document WHERE ecm:fulltext = "searchterm" AND ecm:isCheckedInVersion = 0 AND ecm:path STARTSWITH "/default-domain" ORDER BY dc:modified.

About the Pagination


When the API returns a DocumentList, this list can be paginated. Here are the query parameters available to control the pagination:

  • currentPageIndex: Defaults to 0, the index of the page you want.
  • pageSize: Defaults to 50, the number of entries on a page.

Here's an example result:

[javascript]
{ "currentPageIndex" : 0,
"currentPageSize" : 20,
"errorMessage" : null,
"hasError" : false,
"isLasPageAvailable" : true,
"isNextPageAvailable" : true,
"isPaginable" : true,
"isPreviousPageAvailable" : false,
"isSortable" : true,
"maxPageSize" : 100,
"numberOfPages" : 7,
"pageCount" : 7,
"pageIndex" : 0,
"pageSize" : 20,
"resultsCount" : 128,
"totalSize" : 128,
"entity-type" : "documents",
"entries" : [ { "changeToken" : "1382970456847",
"contextParameters" : { "documentURL" : "/nuxeo/site/api/v1/id/e9e1035f-0535-4aed-bb47-f5fa429bfc65" },
"entity-type" : "document",
"facets" : [ "Picture",
"Commentable",
"Asset",
"MultiviewPicture",
"Versionable",
"Publishable",
"HasRelatedText"
],
"isCheckedOut" : true,
"lastModified" : "2013-10-28T14:27:36.84Z",
"path" : "/asset-library/Screenshot from 2012-10-.1382970297489",
"repository" : "default",
"state" : "project",
"title" : "Screenshot from 2012-10-29 12:26:10.png",
"type" : "Picture",
"uid" : "e9e1035f-0535-4aed-bb47-f5fa429bfc65",
"versionLabel" : "0.0"
},
...
{ "changeToken" : "1382970364262",
"contextParameters" : { "documentURL" : "/nuxeo/site/api/v1/id/7f5af434-91ae-4d9c-b7f2-5bcd7a6427ce" },
"entity-type" : "document",
"facets" : [ "Picture",
"Commentable",
"Asset",
"MultiviewPicture",
"Versionable",
"Publishable",
"HasRelatedText"
],
"isCheckedOut" : true,
"lastModified" : "2013-10-28T14:26:04.26Z",
"path" : "/asset-library/Screenshot from 2012-07-.1382970292974",
"repository" : "default",
"state" : "project",
"title" : "Screenshot from 2012-07-07 17:09:39.png",
"type" : "Picture",
"uid" : "7f5af434-91ae-4d9c-b7f2-5bcd7a6427ce",
"versionLabel" : "0.0"
}
]
}
[/javascript]

So we know how to query documents. But they have a lot of information that we don't need and some that we need are missing. To have exactly what we want, we need to use the Business Object adapter(@bo).

Create a Slideshow Element Business Object


So let's talk about business objects. It's a way to adapt a document. When you are building a new API, you don't want your users to have to deal with low level stuff. You want to keep it simple, to abstract the underlying plumbing of the Nuxeo repository as much as possible. This is why we have document adapters and business objects, to wrap a low level document into an object that is easy to use.

For my slideshow elements I will need the URLs of the thumbnail and the original image. I previously wrote a blog post on how you could do something similar using the RestContributorService extension point. While this is useful when you need something once, it's not as good for re-usability. Using Business Object will fix this. Let's write one.

A BO is a Java class that extends the BusinessAdapter class. It's also a regular document adapter so you have to declare it as such, with its factory and XML contribution. It's easy to do when you have Nuxeo IDE because we have a wizard for that. All the getters you declare here will be used by the JSONWriter when the response of your request is being built.

[java]
package org.nuxeo.slideshow;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.automation.core.operations.business.adapter.BusinessAdapter;
import org.nuxeo.ecm.core.api.ClientException;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.platform.picture.api.adapters.PictureResourceAdapter;
import org.nuxeo.runtime.api.Framework;

/**

  • @author ldoguin
    */
    public class SlideShowElement extends BusinessAdapter {

    private static final Log log = LogFactory.getLog(SlideShowElement.class);

    protected final DocumentModel doc;

    public SlideShowElement(DocumentModel doc) {

     this.doc = doc;
    

    }

    public String getId() {

     return doc.getId();
    

    }

    public String getType() {

     return doc.getType();
    

    }

    public String getTitle() throws ClientException {

     return doc.getTitle();
    

    }

    public String getDescription() throws ClientException {

     return doc.getProperty("dc:description").getValue(String.class);
    

    }

    public String getThumbnailURL() {

     PictureResourceAdapter picture = doc.getAdapter(PictureResourceAdapter.class);
     String blobPropertyName = picture.getViewXPath("Thumbnail") + "content";
     return getBigFileUrl(doc, blobPropertyName, doc.getName()
             + "_thumbnail" + ".jpg");
    

    }

    public String getOriginalURL() {

     PictureResourceAdapter picture = doc.getAdapter(PictureResourceAdapter.class);
     String blobPropertyName = picture.getViewXPath("OriginalJpeg")
             + "content";
     return getBigFileUrl(doc, blobPropertyName, doc.getName() + ".jpg");
    

    }

    private String getBigFileUrl(DocumentModel doc, String blobPropertyName,

         String filename) {
     String blobUrl = Framework.getProperty("nuxeo.url");
     blobUrl += "/nxbigfile/";
     blobUrl += doc.getRepositoryName() + "/";
     blobUrl += doc.getId() + "/";
     blobUrl += blobPropertyName + "/";
     blobUrl += filename;
     return blobUrl;
    

    }

}
[/java]

 

[xml]
<component name="org.nuxeo.slideshow.SlideShowElement">
<extension target="org.nuxeo.ecm.core.api.DocumentAdapterService" point="adapters">
<adapter class="org.nuxeo.slideshow.SlideShowElement"
factory="org.nuxeo.slideshow.SlideShowElementFactory"/>
</extension>
</component>
[/xml]

Once you deploy this, try the same query as before, but with the new adapter:

http://localhost:[email protected][email protected]/SlideShowElement?query=Select * From Document WHERE ecm:mixinType = 'Picture'

The resulting JSON object will be simpler and contains the URLs we need:

[javascript]
{ "currentPageIndex" : 0,
"currentPageSize" : 20,
"errorMessage" : null,
"hasError" : false,
"isLasPageAvailable" : true,
"isNextPageAvailable" : true,
"isPaginable" : true,
"isPreviousPageAvailable" : false,
"isSortable" : true,
"maxPageSize" : 100,
"numberOfPages" : 7,
"pageSize" : 20,
"resultsCount" : 128,
"entity-type" : "adapters",
"entries" : [ { "entity-type" : "SlideShowElement",
"value" : { "id" : "e9e1035f-0535-4aed-bb47-f5fa429bfc65",
"originalURL" : "http://localhost:8080/nuxeo/nxbigfile/default/e9e1035f-0535-4aed-bb47-f5fa429bfc65/picture:views/item[4]/content/Screenshot from 2012-10-.1382970297489.jpg",
"thumbnailURL" : "http://localhost:8080/nuxeo/nxbigfile/default/e9e1035f-0535-4aed-bb47-f5fa429bfc65/picture:views/item[3]/content/Screenshot from 2012-10-.1382970297489_thumbnail.jpg",
"title" : "Screenshot from 2012-10-29 12:26:10.png",
"type" : "Picture"
}
},
....
{ "entity-type" : "SlideShowElement",
"value" : { "id" : "ff1eb80e-0fab-43e8-a886-38f739b78bef",
"originalURL" : "http://localhost:8080/nuxeo/nxbigfile/default/ff1eb80e-0fab-43e8-a886-38f739b78bef/picture:views/item[4]/content/Screenshot from 2012-06-.1382970291187.jpg",
"thumbnailURL" : "http://localhost:8080/nuxeo/nxbigfile/default/ff1eb80e-0fab-43e8-a886-38f739b78bef/picture:views/item[3]/content/Screenshot from 2012-06-.1382970291187_thumbnail.jpg",
"title" : "Screenshot from 2012-06-13 14:56:41.png",
"type" : "Picture"
}
}
]
}
[/javascript]

Now we can retrieve all the information we want with a simple query. In the next blog post I'll explain how I used this in an AngularJS app.


Category: Product & Development
Tagged: API