Last Friday’s post left us with three questions. Let’s jump right into it - the remaining questions are:

When will I generate the thumbnail and add it to the document?
What if the main Blob of the document changes?
And where will I display that brand new thumbnail?

When to generate the thumbnail?

I want to generate a thumbnail each time the main file of a document is updated. This is the perfect opportunity to use an event listener. There are two events that I need to listen to. The first one is obviously documentCreation. Each time a document is created I need to look for its main file and if it exists, try to generate the thumbnail. The other one is beforeDocumentModification. It’s better than documentModified because this way I can know if the document has dirty fields or not. Which is to say I can know if a property has indeed been modified.

Here’s the configuration of my listener:

<extension target="org.nuxeo.ecm.core.event.EventServiceComponent" point="listener"><listener name="updatethumblistener" async="true" postcommit="false" class="org.nuxeo.thumb.listener.UpdateThumbListener" priority="999"><event>documentCreated</event> <event>beforeDocumentModification</event></listener></extension>

Also be careful about the priority of the listeners. If it's too low, I will enter in _UpdateThumbListener_ before entering the _[DublinCoreListener]( 'DublinCoreListener Javadoc')_. What does that mean? Well _UpdateThumbListener_ relies on a property set by _DublinCoreListener_. You'll get a [NPE]( "What's a NPE?") if you pass before it.

About my Listener, it first checks if the document has been modified or just created. If so, it uses the _BlobHolder_ to retrieve the main blob. If we have a blob, we try to generate the thumbnail and go through the AddThumbnailUnrestricted runner. If there is no main blob, but the document has been modified, we remove the Thumbnail facet, if it exists. This way we can remove the thumbnail when, for instance, someone deletes the main file of a document.

java import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.ClientException; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.blobholder.BlobHolder; import org.nuxeo.ecm.core.api.event.DocumentEventTypes; import org.nuxeo.ecm.core.event.Event; import org.nuxeo.ecm.core.event.EventContext; import org.nuxeo.ecm.core.event.EventListener; import org.nuxeo.ecm.core.event.impl.DocumentEventContext; import org.nuxeo.thumb.AddThumbnailUnrestricted; import org.nuxeo.thumb.ThumbnailConstants;

/** * @author ldoguin */ public class UpdateThumbListener implements EventListener {

public void handleEvent(Event event) throws ClientException { EventContext ec = event.getContext(); if (ec instanceof DocumentEventContext) { DocumentEventContext context = (DocumentEventContext) ec; DocumentModel doc = context.getSourceDocument(); if (doc.isDirty() || DocumentEventTypes.DOCUMENT_CREATED.equals(event.getName())) { BlobHolder blobHolder = doc.getAdapter(BlobHolder.class); if (blobHolder != null) { Blob blob = blobHolder.getBlob(); if (blob != null) { try { AddThumbnailUnrestricted runner = new AddThumbnailUnrestricted( context.getCoreSession(), doc, blobHolder); runner.runUnrestricted(); return; } catch (Exception e) { throw new RuntimeException(e); } } } // No Blob anymore, remove the facet if (doc.hasFacet(ThumbnailConstants.THUMBNAIL_FACET)) { doc.removeFacet(ThumbnailConstants.THUMBNAIL_FACET); context.getCoreSession().saveDocument(doc); } } } } 

Now we jump to the runner. Its job is to compute the digest of the main file. We store it on the thumbnail schema. This way we know if the blob has changed and if we need to generate a new thumbnail. This is currently mandatory for 5.5\. In 5.6 the method Blob.getDigest always returns VCS digest so we won't have to compute it anymore.

Then if we can convert the blob to a thumbnail, we add the _Thumbnail_ facet (if it has not already been done), set the new thumb blob and the new digest to the document.

java package org.nuxeo.thumb;


import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.ClientException; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner; import org.nuxeo.ecm.core.api.blobholder.BlobHolder; import org.nuxeo.ecm.core.convert.api.ConversionService; import org.nuxeo.ecm.platform.filemanager.api.FileManager; import org.nuxeo.runtime.api.Framework;

/** * @author ldoguin */ public class AddThumbnailUnrestricted extends UnrestrictedSessionRunner {

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

protected ConversionService cs;

protected DocumentModel doc;

protected BlobHolder blobHolder;

protected Thumbnail thumbnail = null;

public AddThumbnailUnrestricted(CoreSession coreSession, DocumentModel doc, BlobHolder blobHolder) { super(coreSession); this.doc = doc; this.blobHolder = blobHolder; }

@Override public void run() throws ClientException { try { Blob blob = blobHolder.getBlob(); if (blob != null) { // computing the digest will be unnecessary in 5.6, // we use the one from VCS FileManager fm = Framework.getService(FileManager.class); String digest = fm.computeDigest(blob); if (doc.hasFacet(ThumbnailConstants.THUMBNAIL_FACET)) { Thumbnail thumb = doc.getAdapter(Thumbnail.class); // document already has Thumbnail Facet String previousDigest = thumb.getDigest(); if (previousDigest.equals(digest)) { // blob has not changed, no need for thumbnail update return; } } cs = Framework.getService(ConversionService.class); BlobHolder thumbnailBlob = cs.convert( ThumbnailConstants.THUMBNAIL_CONVERTER_NAME, blobHolder, null); if (thumbnailBlob != null) { // we can compute a thumbnail, add it to the document. if (!doc.hasFacet(ThumbnailConstants.THUMBNAIL_FACET)) { doc.addFacet(ThumbnailConstants.THUMBNAIL_FACET); } doc.setPropertyValue( ThumbnailConstants.THUMBNAIL_PROPERTY_NAME, (Serializable) thumbnailBlob.getBlob()); doc.setPropertyValue( ThumbnailConstants.THUMBNAIL_DIGEST_PROPERTY_NAME, digest); doc = session.saveDocument(doc); thumbnail = new Thumbnail(doc); } } } catch (Exception e) { log.warn("Error while adding thumbnail", e); } }

public Thumbnail getAdapter() { return thumbnail; }


With this listener, we are now sure that the thumbnail will be updated at document creation or whenever someone changes the main blob of a document, or removes it.

Where to display the thumbnail?

With Nuxeo, there are many ways to extend the default UI. The one I’ve chosen is to override the big icon listing layout. I will display the thumbnail instead of the icon when available. A listing layout is made of different widgets of a certain type. If I redefine the widget type _listing_big_icon_type_link_ to use a custom template, it will be used in any widget using the _listing_big_icon_type_link_ widgetType. So as usual I contribute to an extension point to override the widget type:

<extension target="org.nuxeo.ecm.platform.forms.layout.WebLayoutManager" point="widgettypes"><widgettype name="listing_big_icon_type_link"><handler-class>org.nuxeo.ecm.platform.forms.layout.facelets.plugins.TemplateWidgetTypeHandler</handler-class> <property name="template">/widgets/listing/listing_thumbnail_widget_template.xhtml</property></widgettype></extension>

Merge is currently not implemented in widgets, so I’ve overriden everything. I only need to specify the handler class for the template and my custom template _listing_thumbnail_widget_template.xhtml_:

<f:subview id="#{}" xmlns:c="" xmlns:f="" xmlns:nxu="" xmlns:nxd="" xmlns:nxl="" xmlns:h="" xmlns:nxp=""><c:if test="#{nxl:isLikePlainMode(widget.mode)}">#{field_2}</c:if> <c:if test="#{nxl:isLikeViewMode(widget.mode)}"><c:if test="#{field_0.hasFacet('Thumbnail')}"><nxu:set var="image_url" value="#{nxd:fileUrl('downloadFile', field_0, 'thumb:content', field_0.dublincore.modified)}" cache="true">![#{field_2}](#{image_url} "#{field_2}")</nxu:set></c:if>

 <c:if test="#{not field_0.hasFacet('Thumbnail')}"><div id="docRefTarget:#{field_1}" class="#{nxu:test(field_3, 'nxDropTarget', '')}">

<div id="docRef:#{field_1}" class="cell popupTarget dropout nxDraggable" docref="#{field_1}"><nxu:graphicimage value="#{nxd:bigIconPath(field_0)}" alt="#{field_2}" title="#{field_2}" rendered="#{!empty nxd:bigIconPath(field_0)}" styleclass="bigIcon"></nxu:graphicimage></div>

</div></c:if></c:if> <c:if test="#{widget.mode == 'pdf'}"><nxp:image value="#{nxd:iconPath(field_0)}"></nxp:image></c:if></f:subview>

What this template does is check if the given document has the Thumbnail facet and if so, displays the generated thumbnail. If there is no facet, we display the usual icon. Now every time I’ll be displaying the big icon document listing, I’ll be able to see documents thumbnails. Here’s the difference between the big icon listing with and without thumbnail :-)

Sources are on GitHub if you’re curious or if you want to fork it. On the improvement lists, there are still some things I would like to do. The first thing that comes to my mind is enhance the converter so that we can get thumbnails for office files likes odt, doc, etc… or other formats. It would also be very nice to display this in the DM summary tab or DAM. If you have other ideas, feel free to post them in the comments section. If you have questions, I’ll be glad to answer them on Nuxeo Answers.