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.commons.logging.Log; 031import org.apache.commons.logging.LogFactory; 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:[email protected]">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 Log log = LogFactory.getLog(AbstractLazyCachableRenditionProvider.class); 060 061 @Override 062 public List<Blob> render(DocumentModel doc, RenditionDefinition definition) { 063 if (log.isDebugEnabled()) { 064 log.debug(String.format("Asking \"%s\" rendition lazy rendering for document %s (id=%s).", 065 definition.getName(), doc.getPathAsString(), doc.getId())); 066 } 067 068 // Build the rendition key and get the current source document modification date 069 String key = buildRenditionKey(doc, definition); 070 String sourceDocumentModificationDate = getSourceDocumentModificationDate(doc, definition); 071 072 // If rendition is not already in progress schedule it 073 List<Blob> blobs = null; 074 TransientStore ts = getTransientStore(); 075 if (!ts.exists(key)) { 076 blobs = handleNewRendition(key, doc, definition, sourceDocumentModificationDate); 077 } else { 078 String storedSourceDocumentModificationDate = (String) ts.getParameter(key, 079 SOURCE_DOCUMENT_MODIFICATION_DATE_KEY); 080 blobs = ts.getBlobs(key); 081 if (ts.isCompleted(key)) { 082 handleCompletedRendition(key, doc, definition, sourceDocumentModificationDate, 083 storedSourceDocumentModificationDate, blobs); 084 } else { 085 handleIncompleteRendition(key, doc, definition, sourceDocumentModificationDate, 086 storedSourceDocumentModificationDate); 087 } 088 } 089 090 if (log.isDebugEnabled()) { 091 String blobInfo = null; 092 if (blobs != null) { 093 blobInfo = blobs.stream() 094 .map(blob -> String.format("{filename=%s, MIME type=%s}", blob.getFilename(), 095 blob.getMimeType())) 096 .collect(Collectors.joining(",", "[", "]")); 097 } 098 log.debug(String.format("Returning blobs: %s.", blobInfo)); 099 } 100 return blobs; 101 } 102 103 public String buildRenditionKey(DocumentModel doc, RenditionDefinition def) { 104 StringBuilder sb = new StringBuilder(doc.getId()); 105 sb.append("::"); 106 String variant = getVariant(doc, def); 107 if (variant != null) { 108 sb.append(variant); 109 sb.append("::"); 110 } 111 sb.append(def.getName()); 112 113 String key = getDigest(sb.toString()); 114 if (log.isDebugEnabled()) { 115 log.debug(String.format("Built rendition key for document %s (id=%s): %s.", doc.getPathAsString(), 116 doc.getId(), key)); 117 } 118 return key; 119 } 120 121 public String getSourceDocumentModificationDate(DocumentModel doc, RenditionDefinition definition) { 122 String modificationDatePropertyName = definition.getSourceDocumentModificationDatePropertyName(); 123 Calendar modificationDate = (Calendar) doc.getPropertyValue(modificationDatePropertyName); 124 if (modificationDate == null) { 125 return null; 126 } 127 long millis = modificationDate.getTimeInMillis(); 128 // the date may have been rounded by the storage layer, normalize it to the second 129 millis -= millis % 1000; 130 return String.valueOf(millis); 131 } 132 133 protected String getDigest(String key) { 134 MessageDigest digest; 135 try { 136 digest = MessageDigest.getInstance("MD5"); 137 } catch (NoSuchAlgorithmException e) { 138 return key; 139 } 140 byte[] buf = digest.digest(key.getBytes()); 141 return toHexString(buf); 142 } 143 144 private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); 145 146 protected String toHexString(byte[] data) { 147 StringBuilder buf = new StringBuilder(2 * data.length); 148 for (byte b : data) { 149 buf.append(HEX_DIGITS[(0xF0 & b) >> 4]); 150 buf.append(HEX_DIGITS[0x0F & b]); 151 } 152 return buf.toString(); 153 } 154 155 protected TransientStore getTransientStore() { 156 TransientStoreService tss = Framework.getService(TransientStoreService.class); 157 TransientStore ts = tss.getStore(CACHE_NAME); 158 if (ts == null) { 159 throw new NuxeoException("Unable to find Transient Store " + CACHE_NAME); 160 } 161 return ts; 162 } 163 164 protected List<Blob> handleNewRendition(String key, DocumentModel doc, RenditionDefinition definition, 165 String sourceDocumentModificationDate) { 166 Work work = getRenditionWork(key, doc, definition); 167 if (log.isDebugEnabled()) { 168 log.debug(String.format( 169 "No entry found for key %s in the %s transient store, scheduling rendition work with id %s and storing an empty blob for now.", 170 key, CACHE_NAME, work.getId())); 171 } 172 if (sourceDocumentModificationDate != null) { 173 getTransientStore().putParameter(key, SOURCE_DOCUMENT_MODIFICATION_DATE_KEY, 174 sourceDocumentModificationDate); 175 } 176 StringBlob emptyBlob = new StringBlob(""); 177 emptyBlob.setFilename(LazyRendition.IN_PROGRESS_MARKER); 178 emptyBlob.setMimeType("text/plain;" + LazyRendition.EMPTY_MARKER); 179 getTransientStore().putBlobs(key, Collections.singletonList(emptyBlob)); 180 Framework.getService(WorkManager.class).schedule(work); 181 return Collections.singletonList(emptyBlob); 182 } 183 184 protected void handleCompletedRendition(String key, DocumentModel doc, RenditionDefinition definition, 185 String sourceDocumentModificationDate, String storedSourceDocumentModificationDate, List<Blob> blobs) { 186 if (log.isDebugEnabled()) { 187 log.debug(String.format("Completed entry found for key %s in the %s transient store.", key, CACHE_NAME)); 188 } 189 190 // No or more than one blob 191 if (blobs == null || blobs.size() != 1) { 192 if (log.isDebugEnabled()) { 193 log.debug(String.format( 194 "No (or more than one) rendition blob for key %s, releasing entry from the transient store.", 195 key)); 196 } 197 getTransientStore().release(key); 198 return; 199 } 200 201 // Blob in error 202 Blob blob = blobs.get(0); 203 String mimeType = blob.getMimeType(); 204 if (mimeType != null && mimeType.contains(LazyRendition.ERROR_MARKER)) { 205 if (log.isDebugEnabled()) { 206 log.debug(String.format("Rendition blob is in error for key %s.", key)); 207 } 208 // Check if rendition is up-to-date 209 if (Objects.equals(storedSourceDocumentModificationDate, sourceDocumentModificationDate)) { 210 log.debug("Removing entry from the transient store."); 211 getTransientStore().remove(key); 212 return; 213 } 214 Work work = getRenditionWork(key, doc, definition); 215 if (log.isDebugEnabled()) { 216 log.debug(String.format( 217 "Source document modification date %s is different from the stored one %s, scheduling rendition work with id %s and returning an error/stale rendition.", 218 sourceDocumentModificationDate, storedSourceDocumentModificationDate, work.getId())); 219 } 220 if (sourceDocumentModificationDate != null) { 221 getTransientStore().putParameter(key, SOURCE_DOCUMENT_MODIFICATION_DATE_KEY, 222 sourceDocumentModificationDate); 223 } 224 Framework.getService(WorkManager.class).schedule(work); 225 blob.setMimeType(blob.getMimeType() + ";" + LazyRendition.STALE_MARKER); 226 return; 227 } 228 229 // Check if rendition is up-to-date 230 if (Objects.equals(storedSourceDocumentModificationDate, sourceDocumentModificationDate)) { 231 if (log.isDebugEnabled()) { 232 log.debug(String.format( 233 "Rendition blob is up-to-date for key %s, returning it and releasing entry from the transient store.", 234 key)); 235 } 236 getTransientStore().release(key); 237 return; 238 } 239 240 // Stale rendition 241 Work work = getRenditionWork(key, doc, definition); 242 if (log.isDebugEnabled()) { 243 log.debug(String.format( 244 "Source document modification date %s is different from the stored one %s, scheduling rendition work with id %s and returning a stale rendition.", 245 sourceDocumentModificationDate, storedSourceDocumentModificationDate, work.getId())); 246 } 247 if (sourceDocumentModificationDate != null) { 248 getTransientStore().putParameter(key, SOURCE_DOCUMENT_MODIFICATION_DATE_KEY, 249 sourceDocumentModificationDate); 250 } 251 Framework.getService(WorkManager.class).schedule(work); 252 blob.setMimeType(blob.getMimeType() + ";" + LazyRendition.STALE_MARKER); 253 } 254 255 protected void handleIncompleteRendition(String key, DocumentModel doc, RenditionDefinition definition, 256 String sourceDocumentModificationDate, String storedSourceDocumentModificationDate) { 257 if (log.isDebugEnabled()) { 258 log.debug(String.format("Incomplete entry found for key %s in the %s transient store.", key, CACHE_NAME)); 259 } 260 WorkManager workManager = Framework.getService(WorkManager.class); 261 Work work = getRenditionWork(key, doc, definition); 262 String workId = work.getId(); 263 boolean scheduleWork = false; 264 if (Objects.equals(storedSourceDocumentModificationDate, sourceDocumentModificationDate)) { 265 Work.State workState = workManager.getWorkState(workId); 266 if (workState == null) { 267 if (log.isDebugEnabled()) { 268 log.debug(String.format("Found no existing work with id %s.", workId)); 269 } 270 scheduleWork = true; 271 } else { 272 if (log.isDebugEnabled()) { 273 log.debug(String.format("Found an existing work with id %s in sate %s.", workId, workState)); 274 } 275 } 276 } else { 277 if (log.isDebugEnabled()) { 278 log.debug(String.format("Source document modification date %s is different from the stored one %s.", 279 sourceDocumentModificationDate, storedSourceDocumentModificationDate)); 280 } 281 if (sourceDocumentModificationDate != null) { 282 getTransientStore().putParameter(key, SOURCE_DOCUMENT_MODIFICATION_DATE_KEY, 283 sourceDocumentModificationDate); 284 } 285 scheduleWork = true; 286 } 287 if (scheduleWork) { 288 if (log.isDebugEnabled()) { 289 log.debug(String.format("Scheduling rendition work with id %s.", workId)); 290 } 291 workManager.schedule(work); 292 } 293 } 294 295 /** 296 * Return the {@link Work} that will compute the {@link Rendition}. {@link AbstractRenditionBuilderWork} can be used 297 * as a base class 298 * 299 * @param key the key used to rendition 300 * @param doc the target {@link DocumentModel} 301 * @param def the {@link RenditionDefinition} 302 * @return 303 */ 304 protected abstract Work getRenditionWork(final String key, final DocumentModel doc, final RenditionDefinition def); 305 306}