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.Calendar;
025import java.util.Collections;
026import java.util.List;
027import java.util.Objects;
028import java.util.stream.Collectors;
029
030import org.apache.logging.log4j.LogManager;
031import org.apache.logging.log4j.Logger;
032import org.nuxeo.ecm.core.api.Blob;
033import org.nuxeo.ecm.core.api.DocumentModel;
034import org.nuxeo.ecm.core.api.NuxeoException;
035import org.nuxeo.ecm.core.api.impl.blob.StringBlob;
036import org.nuxeo.ecm.core.transientstore.api.TransientStore;
037import org.nuxeo.ecm.core.transientstore.api.TransientStoreService;
038import org.nuxeo.ecm.core.work.api.Work;
039import org.nuxeo.ecm.core.work.api.WorkManager;
040import org.nuxeo.ecm.platform.rendition.Rendition;
041import org.nuxeo.ecm.platform.rendition.extension.DefaultAutomationRenditionProvider;
042import org.nuxeo.ecm.platform.rendition.extension.RenditionProvider;
043import org.nuxeo.ecm.platform.rendition.impl.LazyRendition;
044import org.nuxeo.ecm.platform.rendition.service.RenditionDefinition;
045import org.nuxeo.runtime.api.Framework;
046
047/**
048 * Default implementation of an asynchronous {@link RenditionProvider}
049 *
050 * @author <a href="mailto:tdelprat@nuxeo.com">Tiry</a>
051 * @since 7.2
052 */
053public abstract class AbstractLazyCachableRenditionProvider extends DefaultAutomationRenditionProvider {
054
055    public static final String SOURCE_DOCUMENT_MODIFICATION_DATE_KEY = "sourceDocumentModificationDate";
056
057    public static final String CACHE_NAME = "LazyRenditionCache";
058
059    protected static Logger log = LogManager.getLogger(AbstractLazyCachableRenditionProvider.class);
060
061    @Override
062    public List<Blob> render(DocumentModel doc, RenditionDefinition definition) {
063        log.debug("Asking \"{}\" rendition lazy rendering for document {} (id={}).", definition::getName,
064                doc::getPathAsString, doc::getId);
065
066        // Build the rendition key and get the current source document modification date
067        String key = buildRenditionKey(doc, definition);
068        String sourceDocumentModificationDate = getSourceDocumentModificationDate(doc, definition);
069
070        // If rendition is not already in progress schedule it
071        List<Blob> blobs;
072        TransientStore ts = getTransientStore();
073        if (!ts.exists(key)) {
074            blobs = handleNewRendition(key, doc, definition, sourceDocumentModificationDate);
075        } else {
076            String tsSourceDocumentModificationDate = (String) ts.getParameter(key,
077                    SOURCE_DOCUMENT_MODIFICATION_DATE_KEY);
078            blobs = ts.getBlobs(key);
079            if (ts.isCompleted(key)) {
080                handleCompletedRendition(key, doc, definition, sourceDocumentModificationDate,
081                        tsSourceDocumentModificationDate, blobs);
082            } else {
083                handleIncompleteRendition(key, doc, definition, sourceDocumentModificationDate,
084                        tsSourceDocumentModificationDate);
085            }
086        }
087        final List<Blob> finalBlobs = blobs;
088        log.debug("Returning blobs: {}.", () -> getBlobInfo(finalBlobs));
089        return blobs;
090    }
091
092    /**
093     * @since 11.1
094     */
095    protected String getBlobInfo(List<Blob> blobs) {
096        if (blobs == null) {
097            return null;
098        }
099        return blobs.stream()
100                    .map(blob -> String.format("{filename=%s, MIME type=%s}", blob.getFilename(), blob.getMimeType()))
101                    .collect(Collectors.joining(",", "[", "]"));
102    }
103
104    public String buildRenditionKey(DocumentModel doc, RenditionDefinition def) {
105        StringBuilder sb = new StringBuilder(doc.getId());
106        sb.append("::");
107        String variant = getVariant(doc, def);
108        if (variant != null) {
109            sb.append(variant);
110            sb.append("::");
111        }
112        sb.append(def.getName());
113
114        String key = getDigest(sb.toString());
115        log.debug("Built rendition key for document {} (id={}): {}.", doc::getPathAsString, doc::getId, () -> key);
116        return key;
117    }
118
119    public String getSourceDocumentModificationDate(DocumentModel doc, RenditionDefinition definition) {
120        String modificationDatePropertyName = definition.getSourceDocumentModificationDatePropertyName();
121        Calendar modificationDate = (Calendar) doc.getPropertyValue(modificationDatePropertyName);
122        if (modificationDate == null) {
123            return null;
124        }
125        long millis = modificationDate.getTimeInMillis();
126        // the date may have been rounded by the storage layer, normalize it to the second
127        millis -= millis % 1000;
128        return String.valueOf(millis);
129    }
130
131    protected String getDigest(String key) {
132        MessageDigest digest;
133        try {
134            digest = MessageDigest.getInstance("MD5");
135        } catch (NoSuchAlgorithmException e) {
136            return key;
137        }
138        byte[] buf = digest.digest(key.getBytes());
139        return toHexString(buf);
140    }
141
142    private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
143
144    protected String toHexString(byte[] data) {
145        StringBuilder sb = new StringBuilder(2 * data.length);
146        for (byte b : data) {
147            sb.append(HEX_DIGITS[(0xF0 & b) >> 4]);
148            sb.append(HEX_DIGITS[0x0F & b]);
149        }
150        return sb.toString();
151    }
152
153    protected TransientStore getTransientStore() {
154        TransientStoreService tss = Framework.getService(TransientStoreService.class);
155        TransientStore ts = tss.getStore(CACHE_NAME);
156        if (ts == null) {
157            throw new NuxeoException("Unable to find Transient Store  " + CACHE_NAME);
158        }
159        return ts;
160    }
161
162    protected List<Blob> handleNewRendition(String key, DocumentModel doc, RenditionDefinition definition,
163            String sourceDocumentModificationDate) {
164        Work work = getRenditionWork(key, doc, definition);
165        log.debug(
166                "No entry found for key {} in the {} transient store, scheduling rendition work with id {} and storing"
167                        + " an empty blob for now.",
168                () -> key, () -> CACHE_NAME, work::getId);
169        if (sourceDocumentModificationDate != null) {
170            getTransientStore().putParameter(key, SOURCE_DOCUMENT_MODIFICATION_DATE_KEY,
171                    sourceDocumentModificationDate);
172        }
173        StringBlob emptyBlob = new StringBlob("");
174        emptyBlob.setFilename(LazyRendition.IN_PROGRESS_MARKER);
175        emptyBlob.setMimeType("text/plain;" + LazyRendition.EMPTY_MARKER);
176        getTransientStore().putBlobs(key, Collections.singletonList(emptyBlob));
177        Framework.getService(WorkManager.class).schedule(work);
178        return Collections.singletonList(emptyBlob);
179    }
180
181    protected void handleCompletedRendition(String key, DocumentModel doc, RenditionDefinition definition,
182            String sourceDocumentModificationDate, String tsSourceDocumentModificationDate, List<Blob> blobs) {
183        log.debug("Completed entry found for key {} in the {} transient store.", key, CACHE_NAME);
184
185        // No or more than one blob
186        if (blobs == null || blobs.size() != 1) {
187            log.debug("No (or more than one) rendition blob for key {}, releasing entry from the transient store.",
188                    key);
189            getTransientStore().release(key);
190            return;
191        }
192
193        // Blob in error
194        Blob blob = blobs.get(0);
195        String mimeType = blob.getMimeType();
196        if (mimeType != null && mimeType.contains(LazyRendition.ERROR_MARKER)) {
197            log.debug("Rendition blob is in error for key {}.", key);
198            // Check if rendition is up-to-date
199            if (Objects.equals(tsSourceDocumentModificationDate, sourceDocumentModificationDate)) {
200                log.debug("Removing entry from the transient store.");
201                getTransientStore().remove(key);
202                return;
203            }
204            Work work = getRenditionWork(key, doc, definition);
205            log.debug(
206                    "Source document modification date {} is different from the corresponding transient store parameter"
207                            + " {}, scheduling rendition work with id {} and returning an error/stale rendition.",
208                    () -> sourceDocumentModificationDate, () -> tsSourceDocumentModificationDate, work::getId);
209            Framework.getService(WorkManager.class).schedule(work);
210            blob.setMimeType(blob.getMimeType() + ";" + LazyRendition.STALE_MARKER);
211            return;
212        }
213
214        // Check if rendition is up-to-date
215        if (Objects.equals(tsSourceDocumentModificationDate, sourceDocumentModificationDate)) {
216            log.debug("Rendition blob is up-to-date for key {}, returning it and releasing entry from the transient"
217                    + " store.", key);
218            getTransientStore().release(key);
219            return;
220        }
221
222        // Stale rendition
223        Work work = getRenditionWork(key, doc, definition);
224        log.debug(
225                "Source document modification date {} is different from the corresponding transient store parameter {},"
226                        + " scheduling rendition work with id {} and returning a stale rendition.",
227                () -> sourceDocumentModificationDate, () -> tsSourceDocumentModificationDate, work::getId);
228        Framework.getService(WorkManager.class).schedule(work);
229        blob.setMimeType(blob.getMimeType() + ";" + LazyRendition.STALE_MARKER);
230    }
231
232    protected void handleIncompleteRendition(String key, DocumentModel doc, RenditionDefinition definition,
233            String sourceDocumentModificationDate, String tsSourceDocumentModificationDate) {
234        log.debug("Incomplete entry found for key {} in the {} transient store.", key, CACHE_NAME);
235        WorkManager workManager = Framework.getService(WorkManager.class);
236        Work work = getRenditionWork(key, doc, definition);
237        String workId = work.getId();
238        boolean scheduleWork = false;
239        if (Objects.equals(tsSourceDocumentModificationDate, sourceDocumentModificationDate)) {
240            Work.State workState = workManager.getWorkState(workId);
241            if (workState == null) {
242                log.debug("Found no existing work with id {}.", workId);
243                scheduleWork = true;
244            } else {
245                log.debug("Found an existing work with id {} in sate {}.", workId, workState);
246            }
247        } else {
248            log.debug("Source document modification date {} is different from the corresponding transient store"
249                    + " parameter {}.", sourceDocumentModificationDate, tsSourceDocumentModificationDate);
250            scheduleWork = true;
251        }
252        if (scheduleWork) {
253            log.debug("Scheduling rendition work with id {}.", workId);
254            workManager.schedule(work);
255        }
256    }
257
258    /**
259     * Return the {@link Work} that will compute the {@link Rendition}. {@link AbstractRenditionBuilderWork} can be used
260     * as a base class
261     *
262     * @param key the key used to rendition
263     * @param doc the target {@link DocumentModel}
264     * @param def the {@link RenditionDefinition}
265     */
266    protected abstract Work getRenditionWork(final String key, final DocumentModel doc, final RenditionDefinition def);
267
268}