001/* 002 * (C) Copyright 2010 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 * Contributors: 016 * Nuxeo - initial API and implementation 017 */ 018 019package org.nuxeo.ecm.platform.rendition.service; 020 021import static org.nuxeo.ecm.platform.rendition.Constants.RENDITION_SOURCE_ID_PROPERTY; 022import static org.nuxeo.ecm.platform.rendition.Constants.RENDITION_SOURCE_VERSIONABLE_ID_PROPERTY; 023 024import java.io.Serializable; 025import java.util.ArrayList; 026import java.util.Deque; 027import java.util.HashMap; 028import java.util.LinkedList; 029import java.util.List; 030import java.util.Map; 031import java.util.stream.Collectors; 032 033import org.apache.commons.logging.Log; 034import org.apache.commons.logging.LogFactory; 035import org.nuxeo.ecm.automation.AutomationService; 036import org.nuxeo.ecm.core.api.Blob; 037import org.nuxeo.ecm.core.api.CoreSession; 038import org.nuxeo.ecm.core.api.DocumentModel; 039import org.nuxeo.ecm.core.api.DocumentRef; 040import org.nuxeo.ecm.core.api.IdRef; 041import org.nuxeo.ecm.core.api.IterableQueryResult; 042import org.nuxeo.ecm.core.api.NuxeoException; 043import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner; 044import org.nuxeo.ecm.core.api.VersioningOption; 045import org.nuxeo.ecm.core.query.sql.NXQL; 046import org.nuxeo.ecm.platform.query.nxql.NXQLQueryBuilder; 047import org.nuxeo.ecm.platform.rendition.Rendition; 048import org.nuxeo.ecm.platform.rendition.extension.DefaultAutomationRenditionProvider; 049import org.nuxeo.ecm.platform.rendition.extension.RenditionProvider; 050import org.nuxeo.ecm.platform.rendition.impl.LazyRendition; 051import org.nuxeo.ecm.platform.rendition.impl.LiveRendition; 052import org.nuxeo.ecm.platform.rendition.impl.StoredRendition; 053import org.nuxeo.runtime.api.Framework; 054import org.nuxeo.runtime.model.ComponentContext; 055import org.nuxeo.runtime.model.ComponentInstance; 056import org.nuxeo.runtime.model.DefaultComponent; 057import org.nuxeo.runtime.transaction.TransactionHelper; 058 059/** 060 * Default implementation of {@link RenditionService}. 061 * 062 * @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a> 063 * @since 5.4.1 064 */ 065public class RenditionServiceImpl extends DefaultComponent implements RenditionService { 066 067 public static final String RENDITION_DEFINITIONS_EP = "renditionDefinitions"; 068 069 public static final String RENDITON_DEFINION_PROVIDERS_EP = "renditionDefinitionProviders"; 070 071 /** 072 * @since 8.1 073 */ 074 public static final String STORED_RENDITION_MANAGERS_EP = "storedRenditionManagers"; 075 076 private static final Log log = LogFactory.getLog(RenditionServiceImpl.class); 077 078 /** 079 * @deprecated since 7.2. Not used. 080 */ 081 @Deprecated 082 protected AutomationService automationService; 083 084 /** 085 * @deprecated since 7.3. 086 */ 087 @Deprecated 088 protected Map<String, RenditionDefinition> renditionDefinitions; 089 090 /** 091 * @since 7.3. RenditionDefinitions are store in {@link #renditionDefinitionRegistry}. 092 */ 093 protected RenditionDefinitionRegistry renditionDefinitionRegistry; 094 095 protected RenditionDefinitionProviderRegistry renditionDefinitionProviderRegistry; 096 097 protected static final StoredRenditionManager DEFAULT_STORED_RENDITION_MANAGER = new DefaultStoredRenditionManager(); 098 099 /** 100 * @since 8.1 101 */ 102 protected Deque<StoredRenditionManagerDescriptor> storedRenditionManagerDescriptors = new LinkedList<>(); 103 104 /** 105 * @since 8.1 106 */ 107 public StoredRenditionManager getStoredRenditionManager() { 108 StoredRenditionManagerDescriptor descr = storedRenditionManagerDescriptors.peekLast(); 109 return descr == null ? DEFAULT_STORED_RENDITION_MANAGER : descr.getStoredRenditionManager(); 110 } 111 112 @Override 113 public void activate(ComponentContext context) { 114 renditionDefinitions = new HashMap<>(); 115 renditionDefinitionRegistry = new RenditionDefinitionRegistry(); 116 renditionDefinitionProviderRegistry = new RenditionDefinitionProviderRegistry(); 117 super.activate(context); 118 } 119 120 @Override 121 public void deactivate(ComponentContext context) { 122 renditionDefinitions = null; 123 renditionDefinitionRegistry = null; 124 renditionDefinitionProviderRegistry = null; 125 super.deactivate(context); 126 } 127 128 public RenditionDefinition getRenditionDefinition(String name) { 129 return renditionDefinitionRegistry.getRenditionDefinition(name); 130 } 131 132 @Override 133 @Deprecated 134 public List<RenditionDefinition> getDeclaredRenditionDefinitions() { 135 return new ArrayList<>(renditionDefinitionRegistry.descriptors.values()); 136 } 137 138 @Override 139 @Deprecated 140 public List<RenditionDefinition> getDeclaredRenditionDefinitionsForProviderType(String providerType) { 141 List<RenditionDefinition> defs = new ArrayList<>(); 142 for (RenditionDefinition def : getDeclaredRenditionDefinitions()) { 143 if (def.getProviderType().equals(providerType)) { 144 defs.add(def); 145 } 146 } 147 return defs; 148 } 149 150 @Override 151 public List<RenditionDefinition> getAvailableRenditionDefinitions(DocumentModel doc) { 152 153 List<RenditionDefinition> defs = new ArrayList<>(); 154 defs.addAll(renditionDefinitionRegistry.getRenditionDefinitions(doc)); 155 defs.addAll(renditionDefinitionProviderRegistry.getRenditionDefinitions(doc)); 156 157 // XXX what about "lost renditions" ? 158 return defs; 159 } 160 161 @Override 162 public DocumentRef storeRendition(DocumentModel source, String renditionDefinitionName) { 163 Rendition rendition = getRendition(source, renditionDefinitionName, true); 164 return rendition == null ? null : rendition.getHostDocument().getRef(); 165 } 166 167 /** 168 * @deprecated since 8.1 169 */ 170 @Deprecated 171 protected DocumentModel storeRendition(DocumentModel sourceDocument, Rendition rendition, String name) { 172 StoredRendition storedRendition = storeRendition(sourceDocument, rendition); 173 return storedRendition == null ? null : storedRendition.getHostDocument(); 174 } 175 176 /** 177 * @since 8.1 178 */ 179 protected StoredRendition storeRendition(DocumentModel sourceDocument, Rendition rendition) { 180 if (!rendition.isCompleted()) { 181 return null; 182 } 183 List<Blob> renderedBlobs = rendition.getBlobs(); 184 Blob renderedBlob = renderedBlobs.get(0); 185 String mimeType = renderedBlob.getMimeType(); 186 if (mimeType != null && mimeType.contains(LazyRendition.ERROR_MARKER)) { 187 return null; 188 } 189 190 CoreSession session = sourceDocument.getCoreSession(); 191 DocumentModel version = null; 192 boolean isVersionable = sourceDocument.isVersionable(); 193 if (isVersionable) { 194 DocumentRef versionRef = createVersionIfNeeded(sourceDocument, session); 195 version = session.getDocument(versionRef); 196 } 197 198 RenditionDefinition renditionDefinition = getRenditionDefinition(rendition.getName()); 199 StoredRendition storedRendition = getStoredRenditionManager().createStoredRendition(sourceDocument, version, 200 renderedBlob, renditionDefinition); 201 return storedRendition; 202 } 203 204 protected DocumentRef createVersionIfNeeded(DocumentModel source, CoreSession session) { 205 if (source.isVersionable()) { 206 if (source.isVersion()) { 207 return source.getRef(); 208 } else if (source.isCheckedOut()) { 209 DocumentRef versionRef = session.checkIn(source.getRef(), VersioningOption.MINOR, null); 210 source.refresh(DocumentModel.REFRESH_STATE, null); 211 return versionRef; 212 } else { 213 return session.getLastDocumentVersionRef(source.getRef()); 214 } 215 } 216 return null; 217 } 218 219 /** 220 * @deprecated since 7.2. Not used. 221 */ 222 @Deprecated 223 protected AutomationService getAutomationService() { 224 return Framework.getService(AutomationService.class); 225 } 226 227 @Override 228 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 229 if (RENDITION_DEFINITIONS_EP.equals(extensionPoint)) { 230 RenditionDefinition renditionDefinition = (RenditionDefinition) contribution; 231 renditionDefinitionRegistry.addContribution(renditionDefinition); 232 } else if (RENDITON_DEFINION_PROVIDERS_EP.equals(extensionPoint)) { 233 renditionDefinitionProviderRegistry.addContribution((RenditionDefinitionProviderDescriptor) contribution); 234 } else if (STORED_RENDITION_MANAGERS_EP.equals(extensionPoint)) { 235 storedRenditionManagerDescriptors.add(((StoredRenditionManagerDescriptor) contribution)); 236 } 237 } 238 239 /** 240 * @deprecated since 7.3. RenditionDefinitions are store in {@link #renditionDefinitionRegistry}. 241 */ 242 @Deprecated 243 protected void registerRendition(RenditionDefinition renditionDefinition) { 244 String name = renditionDefinition.getName(); 245 if (name == null) { 246 log.error("Cannot register rendition without a name"); 247 return; 248 } 249 boolean enabled = renditionDefinition.isEnabled(); 250 if (renditionDefinitions.containsKey(name)) { 251 log.info("Overriding rendition with name: " + name); 252 if (enabled) { 253 renditionDefinition = mergeRenditions(renditionDefinitions.get(name), renditionDefinition); 254 } else { 255 log.info("Disabled rendition with name " + name); 256 renditionDefinitions.remove(name); 257 } 258 } 259 if (enabled) { 260 log.info("Registering rendition with name: " + name); 261 renditionDefinitions.put(name, renditionDefinition); 262 } 263 264 // setup the Provider 265 setupProvider(renditionDefinition); 266 } 267 268 /** 269 * @deprecated since 7.3. RenditionDefinitions are store in {@link #renditionDefinitionRegistry}. 270 */ 271 @Deprecated 272 protected void setupProvider(RenditionDefinition definition) { 273 if (definition.getProviderClass() == null) { 274 definition.setProvider(new DefaultAutomationRenditionProvider()); 275 } else { 276 try { 277 RenditionProvider provider = definition.getProviderClass().newInstance(); 278 definition.setProvider(provider); 279 } catch (Exception e) { 280 log.error("Unable to create RenditionProvider", e); 281 } 282 } 283 } 284 285 protected RenditionDefinition mergeRenditions(RenditionDefinition oldRenditionDefinition, 286 RenditionDefinition newRenditionDefinition) { 287 String label = newRenditionDefinition.getLabel(); 288 if (label != null) { 289 oldRenditionDefinition.label = label; 290 } 291 292 String operationChain = newRenditionDefinition.getOperationChain(); 293 if (operationChain != null) { 294 oldRenditionDefinition.operationChain = operationChain; 295 } 296 297 return oldRenditionDefinition; 298 } 299 300 @Override 301 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 302 if (RENDITION_DEFINITIONS_EP.equals(extensionPoint)) { 303 renditionDefinitionRegistry.removeContribution((RenditionDefinition) contribution); 304 } else if (RENDITON_DEFINION_PROVIDERS_EP.equals(extensionPoint)) { 305 renditionDefinitionProviderRegistry.removeContribution( 306 (RenditionDefinitionProviderDescriptor) contribution); 307 } else if (STORED_RENDITION_MANAGERS_EP.equals(extensionPoint)) { 308 storedRenditionManagerDescriptors.remove(((StoredRenditionManagerDescriptor) contribution)); 309 } 310 } 311 312 /** 313 * @deprecated since 7.3. RenditionDefinitions are store in {@link #renditionDefinitionRegistry}. 314 */ 315 @Deprecated 316 protected void unregisterRendition(RenditionDefinition renditionDefinition) { 317 String name = renditionDefinition.getName(); 318 renditionDefinitions.remove(name); 319 log.info("Unregistering rendition with name: " + name); 320 } 321 322 @Override 323 public Rendition getRendition(DocumentModel doc, String renditionName) { 324 RenditionDefinition renditionDefinition = getAvailableRenditionDefinition(doc, renditionName); 325 return getRendition(doc, renditionDefinition, renditionDefinition.isStoreByDefault()); 326 } 327 328 @Override 329 public Rendition getRendition(DocumentModel doc, String renditionName, boolean store) { 330 RenditionDefinition renditionDefinition = getAvailableRenditionDefinition(doc, renditionName); 331 return getRendition(doc, renditionDefinition, store); 332 } 333 334 protected Rendition getRendition(DocumentModel doc, RenditionDefinition renditionDefinition, boolean store) { 335 336 Rendition rendition = null; 337 boolean isVersionable = doc.isVersionable(); 338 if (!isVersionable || !doc.isCheckedOut()) { 339 // stored renditions are only done against a non-versionable doc 340 // or a versionable doc that is not checkedout 341 rendition = getStoredRenditionManager().findStoredRendition(doc, renditionDefinition); 342 if (rendition != null) { 343 return rendition; 344 } 345 } 346 347 rendition = new LiveRendition(doc, renditionDefinition); 348 349 if (store) { 350 StoredRendition storedRendition = storeRendition(doc, rendition); 351 if (storedRendition != null) { 352 return storedRendition; 353 } 354 } 355 return rendition; 356 } 357 358 protected RenditionDefinition getAvailableRenditionDefinition(DocumentModel doc, String renditionName) { 359 RenditionDefinition renditionDefinition = renditionDefinitionRegistry.getRenditionDefinition(renditionName); 360 if (renditionDefinition == null) { 361 renditionDefinition = renditionDefinitionProviderRegistry.getRenditionDefinition(renditionName, doc); 362 if (renditionDefinition == null) { 363 String message = "The rendition definition '%s' is not registered"; 364 throw new NuxeoException(String.format(message, renditionName)); 365 } 366 } else { 367 // we have a rendition definition but we must check that it can be used for this doc 368 if (!renditionDefinitionRegistry.canUseRenditionDefinition(renditionDefinition, doc)) { 369 throw new NuxeoException("Rendition " + renditionName + " cannot be used for this doc " + doc.getId()); 370 } 371 } 372 if (!renditionDefinition.getProvider().isAvailable(doc, renditionDefinition)) { 373 throw new NuxeoException("Rendition " + renditionName + " not available for this doc " + doc.getId()); 374 } 375 return renditionDefinition; 376 } 377 378 @Override 379 public List<Rendition> getAvailableRenditions(DocumentModel doc) { 380 return getAvailableRenditions(doc, false); 381 } 382 383 @Override 384 public List<Rendition> getAvailableRenditions(DocumentModel doc, boolean onlyVisible) { 385 List<Rendition> renditions = new ArrayList<>(); 386 387 if (doc.isProxy()) { 388 return renditions; 389 } 390 391 List<RenditionDefinition> defs = getAvailableRenditionDefinitions(doc); 392 if (defs != null) { 393 for (RenditionDefinition def : defs) { 394 if (!onlyVisible || onlyVisible && def.isVisible()) { 395 Rendition rendition = getRendition(doc, def.getName(), false); 396 if (rendition != null) { 397 renditions.add(rendition); 398 } 399 } 400 } 401 } 402 403 return renditions; 404 } 405 406 @Override 407 public void deleteStoredRenditions(String repositoryName) { 408 StoredRenditionsCleaner cleaner = new StoredRenditionsCleaner(repositoryName); 409 cleaner.runUnrestricted(); 410 } 411 412 private final class StoredRenditionsCleaner extends UnrestrictedSessionRunner { 413 414 private static final int BATCH_SIZE = 100; 415 416 private StoredRenditionsCleaner(String repositoryName) { 417 super(repositoryName); 418 } 419 420 @Override 421 public void run() { 422 Map<String, List<String>> sourceIdToRenditionRefs = computeLiveDocumentRefsToRenditionRefs(); 423 removeStoredRenditions(sourceIdToRenditionRefs); 424 } 425 426 /** 427 * Computes only live documents renditions, the related versions will be deleted by Nuxeo. 428 */ 429 private Map<String, List<String>> computeLiveDocumentRefsToRenditionRefs() { 430 Map<String, List<String>> liveDocumentRefsToRenditionRefs = new HashMap<>(); 431 String query = String.format("SELECT %s, %s, %s FROM Document WHERE %s IS NOT NULL AND ecm:isVersion = 0", 432 NXQL.ECM_UUID, RENDITION_SOURCE_ID_PROPERTY, RENDITION_SOURCE_VERSIONABLE_ID_PROPERTY, 433 RENDITION_SOURCE_ID_PROPERTY); 434 try (IterableQueryResult result = session.queryAndFetch(query, NXQL.NXQL)) { 435 for (Map<String, Serializable> res : result) { 436 String renditionRef = res.get(NXQL.ECM_UUID).toString(); 437 String sourceId = res.get(RENDITION_SOURCE_ID_PROPERTY).toString(); 438 Serializable sourceVersionableId = res.get(RENDITION_SOURCE_VERSIONABLE_ID_PROPERTY); 439 440 String key = sourceVersionableId != null ? sourceVersionableId.toString() : sourceId; 441 liveDocumentRefsToRenditionRefs.computeIfAbsent(key, k -> new ArrayList<>()).add(renditionRef); 442 } 443 } 444 return liveDocumentRefsToRenditionRefs; 445 } 446 447 private void removeStoredRenditions(Map<String, List<String>> liveDocumentRefsToRenditionRefs) { 448 List<String> liveDocumentRefs = new ArrayList<>(liveDocumentRefsToRenditionRefs.keySet()); 449 if (liveDocumentRefs.isEmpty()) { 450 // no more document to check 451 return; 452 } 453 454 int processedSourceIds = 0; 455 while (processedSourceIds < liveDocumentRefs.size()) { 456 // compute the batch of source ids to check for existence 457 int limit = processedSourceIds + BATCH_SIZE > liveDocumentRefs.size() ? liveDocumentRefs.size() 458 : processedSourceIds + BATCH_SIZE; 459 List<String> batchSourceIds = liveDocumentRefs.subList(processedSourceIds, limit); 460 461 // retrieve still existing documents 462 List<String> existingSourceIds = new ArrayList<>(); 463 String query = NXQLQueryBuilder.getQuery("SELECT ecm:uuid FROM Document WHERE ecm:uuid IN ?", 464 new Object[] { batchSourceIds }, true, true, null); 465 try (IterableQueryResult result = session.queryAndFetch(query, NXQL.NXQL)) { 466 result.forEach(res -> existingSourceIds.add(res.get(NXQL.ECM_UUID).toString())); 467 } 468 batchSourceIds.removeAll(existingSourceIds); 469 470 List<String> renditionRefsToDelete = batchSourceIds.stream() 471 .map(liveDocumentRefsToRenditionRefs::get) 472 .reduce(new ArrayList<>(), (allRefs, refs) -> { 473 allRefs.addAll(refs); 474 return allRefs; 475 }); 476 477 if (!renditionRefsToDelete.isEmpty()) { 478 session.removeDocuments( 479 renditionRefsToDelete.stream().map(IdRef::new).collect(Collectors.toList()).toArray( 480 new DocumentRef[renditionRefsToDelete.size()])); 481 } 482 483 if (TransactionHelper.isTransactionActiveOrMarkedRollback()) { 484 TransactionHelper.commitOrRollbackTransaction(); 485 TransactionHelper.startTransaction(); 486 } 487 488 // next batch 489 processedSourceIds += BATCH_SIZE; 490 } 491 } 492 } 493 494}