001/* 002 * (C) Copyright 2010-2016 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 */ 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 /** 133 * @deprecated since 7.2 because unused 134 */ 135 @Override 136 @Deprecated 137 public List<RenditionDefinition> getDeclaredRenditionDefinitions() { 138 return new ArrayList<>(renditionDefinitionRegistry.descriptors.values()); 139 } 140 141 /** 142 * @deprecated since 7.2 because unused 143 */ 144 @Override 145 @Deprecated 146 public List<RenditionDefinition> getDeclaredRenditionDefinitionsForProviderType(String providerType) { 147 List<RenditionDefinition> defs = new ArrayList<>(); 148 for (RenditionDefinition def : getDeclaredRenditionDefinitions()) { 149 if (def.getProviderType().equals(providerType)) { 150 defs.add(def); 151 } 152 } 153 return defs; 154 } 155 156 @Override 157 public List<RenditionDefinition> getAvailableRenditionDefinitions(DocumentModel doc) { 158 159 List<RenditionDefinition> defs = new ArrayList<>(); 160 defs.addAll(renditionDefinitionRegistry.getRenditionDefinitions(doc)); 161 defs.addAll(renditionDefinitionProviderRegistry.getRenditionDefinitions(doc)); 162 163 // XXX what about "lost renditions" ? 164 return defs; 165 } 166 167 @Override 168 public DocumentRef storeRendition(DocumentModel source, String renditionDefinitionName) { 169 Rendition rendition = getRendition(source, renditionDefinitionName, true); 170 return rendition == null ? null : rendition.getHostDocument().getRef(); 171 } 172 173 /** 174 * @deprecated since 8.1 175 */ 176 @Deprecated 177 protected DocumentModel storeRendition(DocumentModel sourceDocument, Rendition rendition, String name) { 178 StoredRendition storedRendition = storeRendition(sourceDocument, rendition); 179 return storedRendition == null ? null : storedRendition.getHostDocument(); 180 } 181 182 /** 183 * @since 8.1 184 */ 185 protected StoredRendition storeRendition(DocumentModel sourceDocument, Rendition rendition) { 186 if (!rendition.isCompleted()) { 187 return null; 188 } 189 List<Blob> renderedBlobs = rendition.getBlobs(); 190 Blob renderedBlob = renderedBlobs.get(0); 191 String mimeType = renderedBlob.getMimeType(); 192 if (mimeType != null && mimeType.contains(LazyRendition.ERROR_MARKER)) { 193 return null; 194 } 195 196 CoreSession session = sourceDocument.getCoreSession(); 197 DocumentModel version = null; 198 boolean isVersionable = sourceDocument.isVersionable(); 199 if (isVersionable) { 200 DocumentRef versionRef = createVersionIfNeeded(sourceDocument, session); 201 version = session.getDocument(versionRef); 202 } 203 204 RenditionDefinition renditionDefinition = getRenditionDefinition(rendition.getName()); 205 StoredRendition storedRendition = getStoredRenditionManager().createStoredRendition(sourceDocument, version, 206 renderedBlob, renditionDefinition); 207 return storedRendition; 208 } 209 210 protected DocumentRef createVersionIfNeeded(DocumentModel source, CoreSession session) { 211 if (source.isVersionable()) { 212 if (source.isVersion()) { 213 return source.getRef(); 214 } else if (source.isCheckedOut()) { 215 DocumentRef versionRef = session.checkIn(source.getRef(), VersioningOption.MINOR, null); 216 source.refresh(DocumentModel.REFRESH_STATE, null); 217 return versionRef; 218 } else { 219 return session.getLastDocumentVersionRef(source.getRef()); 220 } 221 } 222 return null; 223 } 224 225 /** 226 * @deprecated since 7.2. Not used. 227 */ 228 @Deprecated 229 protected AutomationService getAutomationService() { 230 return Framework.getService(AutomationService.class); 231 } 232 233 @Override 234 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 235 if (RENDITION_DEFINITIONS_EP.equals(extensionPoint)) { 236 RenditionDefinition renditionDefinition = (RenditionDefinition) contribution; 237 renditionDefinitionRegistry.addContribution(renditionDefinition); 238 } else if (RENDITON_DEFINION_PROVIDERS_EP.equals(extensionPoint)) { 239 renditionDefinitionProviderRegistry.addContribution((RenditionDefinitionProviderDescriptor) contribution); 240 } else if (STORED_RENDITION_MANAGERS_EP.equals(extensionPoint)) { 241 storedRenditionManagerDescriptors.add(((StoredRenditionManagerDescriptor) contribution)); 242 } 243 } 244 245 /** 246 * @deprecated since 7.3. RenditionDefinitions are store in {@link #renditionDefinitionRegistry}. 247 */ 248 @Deprecated 249 protected void registerRendition(RenditionDefinition renditionDefinition) { 250 String name = renditionDefinition.getName(); 251 if (name == null) { 252 log.error("Cannot register rendition without a name"); 253 return; 254 } 255 boolean enabled = renditionDefinition.isEnabled(); 256 if (renditionDefinitions.containsKey(name)) { 257 log.info("Overriding rendition with name: " + name); 258 if (enabled) { 259 renditionDefinition = mergeRenditions(renditionDefinitions.get(name), renditionDefinition); 260 } else { 261 log.info("Disabled rendition with name " + name); 262 renditionDefinitions.remove(name); 263 } 264 } 265 if (enabled) { 266 log.info("Registering rendition with name: " + name); 267 renditionDefinitions.put(name, renditionDefinition); 268 } 269 270 // setup the Provider 271 setupProvider(renditionDefinition); 272 } 273 274 /** 275 * @deprecated since 7.3. RenditionDefinitions are store in {@link #renditionDefinitionRegistry}. 276 */ 277 @Deprecated 278 protected void setupProvider(RenditionDefinition definition) { 279 if (definition.getProviderClass() == null) { 280 definition.setProvider(new DefaultAutomationRenditionProvider()); 281 } else { 282 try { 283 RenditionProvider provider = definition.getProviderClass().newInstance(); 284 definition.setProvider(provider); 285 } catch (Exception e) { 286 log.error("Unable to create RenditionProvider", e); 287 } 288 } 289 } 290 291 protected RenditionDefinition mergeRenditions(RenditionDefinition oldRenditionDefinition, 292 RenditionDefinition newRenditionDefinition) { 293 String label = newRenditionDefinition.getLabel(); 294 if (label != null) { 295 oldRenditionDefinition.label = label; 296 } 297 298 String operationChain = newRenditionDefinition.getOperationChain(); 299 if (operationChain != null) { 300 oldRenditionDefinition.operationChain = operationChain; 301 } 302 303 return oldRenditionDefinition; 304 } 305 306 @Override 307 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 308 if (RENDITION_DEFINITIONS_EP.equals(extensionPoint)) { 309 renditionDefinitionRegistry.removeContribution((RenditionDefinition) contribution); 310 } else if (RENDITON_DEFINION_PROVIDERS_EP.equals(extensionPoint)) { 311 renditionDefinitionProviderRegistry.removeContribution( 312 (RenditionDefinitionProviderDescriptor) contribution); 313 } else if (STORED_RENDITION_MANAGERS_EP.equals(extensionPoint)) { 314 storedRenditionManagerDescriptors.remove(((StoredRenditionManagerDescriptor) contribution)); 315 } 316 } 317 318 /** 319 * @deprecated since 7.3. RenditionDefinitions are store in {@link #renditionDefinitionRegistry}. 320 */ 321 @Deprecated 322 protected void unregisterRendition(RenditionDefinition renditionDefinition) { 323 String name = renditionDefinition.getName(); 324 renditionDefinitions.remove(name); 325 log.info("Unregistering rendition with name: " + name); 326 } 327 328 @Override 329 public Rendition getRendition(DocumentModel doc, String renditionName) { 330 RenditionDefinition renditionDefinition = getAvailableRenditionDefinition(doc, renditionName); 331 return getRendition(doc, renditionDefinition, renditionDefinition.isStoreByDefault()); 332 } 333 334 @Override 335 public Rendition getRendition(DocumentModel doc, String renditionName, boolean store) { 336 RenditionDefinition renditionDefinition = getAvailableRenditionDefinition(doc, renditionName); 337 return getRendition(doc, renditionDefinition, store); 338 } 339 340 protected Rendition getRendition(DocumentModel doc, RenditionDefinition renditionDefinition, boolean store) { 341 342 Rendition rendition = null; 343 boolean isVersionable = doc.isVersionable(); 344 if (!isVersionable || !doc.isCheckedOut()) { 345 // stored renditions are only done against a non-versionable doc 346 // or a versionable doc that is not checkedout 347 rendition = getStoredRenditionManager().findStoredRendition(doc, renditionDefinition); 348 if (rendition != null) { 349 return rendition; 350 } 351 } 352 353 rendition = new LiveRendition(doc, renditionDefinition); 354 355 if (store) { 356 StoredRendition storedRendition = storeRendition(doc, rendition); 357 if (storedRendition != null) { 358 return storedRendition; 359 } 360 } 361 return rendition; 362 } 363 364 protected RenditionDefinition getAvailableRenditionDefinition(DocumentModel doc, String renditionName) { 365 RenditionDefinition renditionDefinition = renditionDefinitionRegistry.getRenditionDefinition(renditionName); 366 if (renditionDefinition == null) { 367 renditionDefinition = renditionDefinitionProviderRegistry.getRenditionDefinition(renditionName, doc); 368 if (renditionDefinition == null) { 369 String message = "The rendition definition '%s' is not registered"; 370 throw new NuxeoException(String.format(message, renditionName)); 371 } 372 } else { 373 // we have a rendition definition but we must check that it can be used for this doc 374 if (!renditionDefinitionRegistry.canUseRenditionDefinition(renditionDefinition, doc)) { 375 throw new NuxeoException("Rendition " + renditionName + " cannot be used for this doc " + doc.getId()); 376 } 377 } 378 if (!renditionDefinition.getProvider().isAvailable(doc, renditionDefinition)) { 379 throw new NuxeoException("Rendition " + renditionName + " not available for this doc " + doc.getId()); 380 } 381 return renditionDefinition; 382 } 383 384 @Override 385 public List<Rendition> getAvailableRenditions(DocumentModel doc) { 386 return getAvailableRenditions(doc, false); 387 } 388 389 @Override 390 public List<Rendition> getAvailableRenditions(DocumentModel doc, boolean onlyVisible) { 391 List<Rendition> renditions = new ArrayList<>(); 392 393 if (doc.isProxy()) { 394 return renditions; 395 } 396 397 List<RenditionDefinition> defs = getAvailableRenditionDefinitions(doc); 398 if (defs != null) { 399 for (RenditionDefinition def : defs) { 400 if (!onlyVisible || onlyVisible && def.isVisible()) { 401 Rendition rendition = getRendition(doc, def.getName(), false); 402 if (rendition != null) { 403 renditions.add(rendition); 404 } 405 } 406 } 407 } 408 409 return renditions; 410 } 411 412 @Override 413 public void deleteStoredRenditions(String repositoryName) { 414 StoredRenditionsCleaner cleaner = new StoredRenditionsCleaner(repositoryName); 415 cleaner.runUnrestricted(); 416 } 417 418 private final class StoredRenditionsCleaner extends UnrestrictedSessionRunner { 419 420 private static final int BATCH_SIZE = 100; 421 422 private StoredRenditionsCleaner(String repositoryName) { 423 super(repositoryName); 424 } 425 426 @Override 427 public void run() { 428 Map<String, List<String>> sourceIdToRenditionRefs = computeLiveDocumentRefsToRenditionRefs(); 429 removeStoredRenditions(sourceIdToRenditionRefs); 430 } 431 432 /** 433 * Computes only live documents renditions, the related versions will be deleted by Nuxeo. 434 */ 435 private Map<String, List<String>> computeLiveDocumentRefsToRenditionRefs() { 436 Map<String, List<String>> liveDocumentRefsToRenditionRefs = new HashMap<>(); 437 String query = String.format("SELECT %s, %s, %s FROM Document WHERE %s IS NOT NULL AND ecm:isVersion = 0", 438 NXQL.ECM_UUID, RENDITION_SOURCE_ID_PROPERTY, RENDITION_SOURCE_VERSIONABLE_ID_PROPERTY, 439 RENDITION_SOURCE_ID_PROPERTY); 440 try (IterableQueryResult result = session.queryAndFetch(query, NXQL.NXQL)) { 441 for (Map<String, Serializable> res : result) { 442 String renditionRef = res.get(NXQL.ECM_UUID).toString(); 443 String sourceId = res.get(RENDITION_SOURCE_ID_PROPERTY).toString(); 444 Serializable sourceVersionableId = res.get(RENDITION_SOURCE_VERSIONABLE_ID_PROPERTY); 445 446 String key = sourceVersionableId != null ? sourceVersionableId.toString() : sourceId; 447 liveDocumentRefsToRenditionRefs.computeIfAbsent(key, k -> new ArrayList<>()).add(renditionRef); 448 } 449 } 450 return liveDocumentRefsToRenditionRefs; 451 } 452 453 private void removeStoredRenditions(Map<String, List<String>> liveDocumentRefsToRenditionRefs) { 454 List<String> liveDocumentRefs = new ArrayList<>(liveDocumentRefsToRenditionRefs.keySet()); 455 if (liveDocumentRefs.isEmpty()) { 456 // no more document to check 457 return; 458 } 459 460 int processedSourceIds = 0; 461 while (processedSourceIds < liveDocumentRefs.size()) { 462 // compute the batch of source ids to check for existence 463 int limit = processedSourceIds + BATCH_SIZE > liveDocumentRefs.size() ? liveDocumentRefs.size() 464 : processedSourceIds + BATCH_SIZE; 465 List<String> batchSourceIds = liveDocumentRefs.subList(processedSourceIds, limit); 466 467 // retrieve still existing documents 468 List<String> existingSourceIds = new ArrayList<>(); 469 String query = NXQLQueryBuilder.getQuery("SELECT ecm:uuid FROM Document WHERE ecm:uuid IN ?", 470 new Object[] { batchSourceIds }, true, true, null); 471 try (IterableQueryResult result = session.queryAndFetch(query, NXQL.NXQL)) { 472 result.forEach(res -> existingSourceIds.add(res.get(NXQL.ECM_UUID).toString())); 473 } 474 batchSourceIds.removeAll(existingSourceIds); 475 476 List<String> renditionRefsToDelete = batchSourceIds.stream() 477 .map(liveDocumentRefsToRenditionRefs::get) 478 .reduce(new ArrayList<>(), (allRefs, refs) -> { 479 allRefs.addAll(refs); 480 return allRefs; 481 }); 482 483 if (!renditionRefsToDelete.isEmpty()) { 484 session.removeDocuments( 485 renditionRefsToDelete.stream().map(IdRef::new).collect(Collectors.toList()).toArray( 486 new DocumentRef[renditionRefsToDelete.size()])); 487 } 488 489 if (TransactionHelper.isTransactionActiveOrMarkedRollback()) { 490 TransactionHelper.commitOrRollbackTransaction(); 491 TransactionHelper.startTransaction(); 492 } 493 494 // next batch 495 processedSourceIds += BATCH_SIZE; 496 } 497 } 498 } 499 500}