001/*
002 * (C) Copyright 2015 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 * Nuxeo - initial API and implementation
016 */
017
018package org.nuxeo.ecm.platform.rendition.lazy;
019
020import java.io.IOException;
021import java.security.MessageDigest;
022import java.security.NoSuchAlgorithmException;
023import java.util.ArrayList;
024import java.util.Calendar;
025import java.util.List;
026
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029import org.nuxeo.ecm.core.api.Blob;
030import org.nuxeo.ecm.core.api.DocumentModel;
031import org.nuxeo.ecm.core.api.NuxeoException;
032import org.nuxeo.ecm.core.api.impl.blob.StringBlob;
033import org.nuxeo.ecm.core.transientstore.StorageEntryImpl;
034import org.nuxeo.ecm.core.transientstore.api.StorageEntry;
035import org.nuxeo.ecm.core.transientstore.api.TransientStore;
036import org.nuxeo.ecm.core.transientstore.api.TransientStoreService;
037import org.nuxeo.ecm.core.work.api.Work;
038import org.nuxeo.ecm.core.work.api.WorkManager;
039import org.nuxeo.ecm.platform.rendition.Rendition;
040import org.nuxeo.ecm.platform.rendition.extension.RenditionProvider;
041import org.nuxeo.ecm.platform.rendition.service.RenditionDefinition;
042import org.nuxeo.runtime.api.Framework;
043
044/**
045 * Default implementation of an asynchronous {@link RenditionProvider}
046 *
047 * @author <a href="mailto:tdelprat@nuxeo.com">Tiry</a>
048 * @since 7.2
049 */
050public abstract class AbstractLazyCachableRenditionProvider implements RenditionProvider {
051
052    public static final String COMPLETED_KEY = "completed";
053
054    public static final String WORKERID_KEY = "workerid";
055
056    public static final String CACHE_NAME = "LazyRenditionCache";
057
058    protected static Log log = LogFactory.getLog(AbstractLazyCachableRenditionProvider.class);
059
060    /**
061     * Define if rendition caching key should include the user login
062     *
063     * @return
064     */
065    protected abstract boolean perUserRendition();
066
067    @Override
068    public List<Blob> render(DocumentModel doc, RenditionDefinition def) {
069
070        // build the key
071        String key = buildRenditionKey(doc, def);
072
073        // see if rendition is already in process
074
075        TransientStoreService tss = Framework.getService(TransientStoreService.class);
076
077        TransientStore ts = tss.getStore(CACHE_NAME);
078
079        if (ts == null) {
080            throw new NuxeoException("Unable to find Transient Store  " + CACHE_NAME);
081        }
082
083        StorageEntry entry = ts.get(key);
084
085        if (entry == null) {
086            Work work = getRenditionWork(key, doc, def);
087            WorkManager wm = Framework.getService(WorkManager.class);
088            entry = new StorageEntryImpl(key);
089            entry.put(WORKERID_KEY, work.getId());
090            ts.put(entry);
091            wm.schedule(work);
092        } else {
093            if ((Boolean.TRUE).equals(entry.get(COMPLETED_KEY))) {
094                ts.release(key);
095                return entry.getBlobs();
096            }
097        }
098        // return an empty Blob
099        List<Blob> blobs = new ArrayList<Blob>();
100        StringBlob emptyBlob = new StringBlob("");
101        emptyBlob.setFilename("inprogress");
102        emptyBlob.setMimeType("text/plain;empty=true");
103        blobs.add(emptyBlob);
104        return blobs;
105    }
106
107    protected String buildRenditionKey(DocumentModel doc, RenditionDefinition def) {
108
109        StringBuffer sb = new StringBuffer(doc.getId());
110        sb.append("::");
111        Calendar modif = (Calendar) doc.getPropertyValue("dc:modified");
112        if (modif != null) {
113            sb.append(modif.getTimeInMillis());
114            sb.append("::");
115        }
116        if (perUserRendition()) {
117            sb.append(doc.getCoreSession().getPrincipal().getName());
118            sb.append("::");
119        }
120        sb.append(def.getName());
121
122        return getDigest(sb.toString());
123    }
124
125    protected String getDigest(String key) {
126        MessageDigest digest;
127        try {
128            digest = MessageDigest.getInstance("MD5");
129        } catch (NoSuchAlgorithmException e) {
130            return key;
131        }
132        byte[] buf = digest.digest(key.getBytes());
133        return toHexString(buf);
134    }
135
136    private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
137
138    protected String toHexString(byte[] data) {
139        StringBuilder buf = new StringBuilder(2 * data.length);
140        for (byte b : data) {
141            buf.append(HEX_DIGITS[(0xF0 & b) >> 4]);
142            buf.append(HEX_DIGITS[0x0F & b]);
143        }
144        return buf.toString();
145    }
146
147    /**
148     * Return the {@link Work} that will compute the {@link Rendition}. {@link AbstractRenditionBuilderWork} can be used
149     * as a base class
150     *
151     * @param key the key used to rendition
152     * @param doc the target {@link DocumentModel}
153     * @param def the {@link RenditionDefinition}
154     * @return
155     */
156    protected abstract Work getRenditionWork(final String key, final DocumentModel doc, final RenditionDefinition def);
157
158}