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