001/*
002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 * Nuxeo - initial API and implementation
018 */
019
020package org.nuxeo.ecm.platform.rendition.lazy;
021
022import java.security.MessageDigest;
023import java.security.NoSuchAlgorithmException;
024import java.util.ArrayList;
025import java.util.Calendar;
026import java.util.List;
027
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030import org.nuxeo.ecm.core.api.Blob;
031import org.nuxeo.ecm.core.api.DocumentModel;
032import org.nuxeo.ecm.core.api.NuxeoException;
033import org.nuxeo.ecm.core.api.impl.blob.StringBlob;
034import org.nuxeo.ecm.core.transientstore.api.TransientStore;
035import org.nuxeo.ecm.core.transientstore.api.TransientStoreService;
036import org.nuxeo.ecm.core.work.api.Work;
037import org.nuxeo.ecm.core.work.api.WorkManager;
038import org.nuxeo.ecm.core.work.api.WorkManager.Scheduling;
039import org.nuxeo.ecm.platform.rendition.Rendition;
040import org.nuxeo.ecm.platform.rendition.extension.AutomationRenderer;
041import org.nuxeo.ecm.platform.rendition.extension.RenditionProvider;
042import org.nuxeo.ecm.platform.rendition.impl.LazyRendition;
043import org.nuxeo.ecm.platform.rendition.service.RenditionDefinition;
044import org.nuxeo.runtime.api.Framework;
045
046/**
047 * Default implementation of an asynchronous {@link RenditionProvider}
048 *
049 * @author <a href="mailto:tdelprat@nuxeo.com">Tiry</a>
050 * @since 7.2
051 */
052public abstract class AbstractLazyCachableRenditionProvider implements RenditionProvider {
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    @Override
061    public List<Blob> render(DocumentModel doc, RenditionDefinition def) {
062
063        // build the key
064        String key = buildRenditionKey(doc, def);
065
066        // see if rendition is already in process
067
068        TransientStoreService tss = Framework.getService(TransientStoreService.class);
069
070        TransientStore ts = tss.getStore(CACHE_NAME);
071
072        if (ts == null) {
073            throw new NuxeoException("Unable to find Transient Store  " + CACHE_NAME);
074        }
075
076        List<Blob> blobs = null;
077        if (!ts.exists(key)) {
078            Work work = getRenditionWork(key, doc, def);
079            ts.putParameter(key, WORKERID_KEY, work.getId());
080            blobs = new ArrayList<>();
081            StringBlob emptyBlob = new StringBlob("");
082            emptyBlob.setFilename("inprogress");
083            emptyBlob.setMimeType("text/plain;" + LazyRendition.EMPTY_MARKER);
084            blobs.add(emptyBlob);
085            ts.putBlobs(key, blobs);
086            Framework.getService(WorkManager.class).schedule(work, Scheduling.IF_NOT_SCHEDULED);
087            blobs = ts.getBlobs(key);
088        } else {
089            blobs = ts.getBlobs(key);
090            if (ts.isCompleted(key)) {
091                if (blobs != null && blobs.size() == 1) {
092                    Blob blob = blobs.get(0);
093                    String mimeType = blob.getMimeType();
094                    if (mimeType != null && mimeType.contains(LazyRendition.ERROR_MARKER)) {
095                        ts.remove(key);
096                    } else {
097                        ts.release(key);
098                    }
099                } else {
100                    ts.release(key);
101                }
102            } else {
103                Work work = getRenditionWork(key, doc, def);
104                String workId = work.getId();
105                WorkManager wm = Framework.getService(WorkManager.class);
106                if (wm.find(workId, null) == null) {
107                    wm.schedule(work, Scheduling.IF_NOT_SCHEDULED);
108                }
109            }
110        }
111
112        return blobs;
113     }
114
115    @Override
116    public String getVariant(DocumentModel doc, RenditionDefinition definition) {
117        return AutomationRenderer.getVariant(doc, definition);
118    }
119
120    protected String buildRenditionKey(DocumentModel doc, RenditionDefinition def) {
121
122        StringBuffer sb = new StringBuffer(doc.getId());
123        sb.append("::");
124        String modificationDatePropertyName = def.getSourceDocumentModificationDatePropertyName();
125        Calendar modif = (Calendar) doc.getPropertyValue(modificationDatePropertyName);
126        if (modif != null) {
127            sb.append(modif.getTimeInMillis());
128            sb.append("::");
129        }
130        String variant = getVariant(doc, def);
131        if (variant != null) {
132            sb.append(variant);
133            sb.append("::");
134        }
135        sb.append(def.getName());
136
137        return getDigest(sb.toString());
138    }
139
140    protected String getDigest(String key) {
141        MessageDigest digest;
142        try {
143            digest = MessageDigest.getInstance("MD5");
144        } catch (NoSuchAlgorithmException e) {
145            return key;
146        }
147        byte[] buf = digest.digest(key.getBytes());
148        return toHexString(buf);
149    }
150
151    private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
152
153    protected String toHexString(byte[] data) {
154        StringBuilder buf = new StringBuilder(2 * data.length);
155        for (byte b : data) {
156            buf.append(HEX_DIGITS[(0xF0 & b) >> 4]);
157            buf.append(HEX_DIGITS[0x0F & b]);
158        }
159        return buf.toString();
160    }
161
162    /**
163     * Return the {@link Work} that will compute the {@link Rendition}. {@link AbstractRenditionBuilderWork} can be used
164     * as a base class
165     *
166     * @param key the key used to rendition
167     * @param doc the target {@link DocumentModel}
168     * @param def the {@link RenditionDefinition}
169     * @return
170     */
171    protected abstract Work getRenditionWork(final String key, final DocumentModel doc, final RenditionDefinition def);
172
173}