/* * (C) Copyright 2010-2016 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Nuxeo - initial API and implementation */ package org.nuxeo.ecm.platform.rendition.service; import static org.nuxeo.ecm.platform.rendition.Constants.RENDITION_SOURCE_ID_PROPERTY; import static org.nuxeo.ecm.platform.rendition.Constants.RENDITION_SOURCE_VERSIONABLE_ID_PROPERTY; import java.io.Serializable; import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.automation.AutomationService; import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentRef; import org.nuxeo.ecm.core.api.IdRef; import org.nuxeo.ecm.core.api.IterableQueryResult; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner; import org.nuxeo.ecm.core.api.VersioningOption; import org.nuxeo.ecm.core.query.sql.NXQL; import org.nuxeo.ecm.platform.query.nxql.NXQLQueryBuilder; import org.nuxeo.ecm.platform.rendition.Rendition; import org.nuxeo.ecm.platform.rendition.extension.DefaultAutomationRenditionProvider; import org.nuxeo.ecm.platform.rendition.extension.RenditionProvider; import org.nuxeo.ecm.platform.rendition.impl.LazyRendition; import org.nuxeo.ecm.platform.rendition.impl.LiveRendition; import org.nuxeo.ecm.platform.rendition.impl.StoredRendition; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.model.ComponentContext; import org.nuxeo.runtime.model.ComponentInstance; import org.nuxeo.runtime.model.DefaultComponent; import org.nuxeo.runtime.transaction.TransactionHelper; /** * Default implementation of {@link RenditionService}. * * @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a> * @since 5.4.1 */ public class RenditionServiceImpl extends DefaultComponent implements RenditionService { public static final String RENDITION_DEFINITIONS_EP = "renditionDefinitions"; public static final String RENDITON_DEFINION_PROVIDERS_EP = "renditionDefinitionProviders"; /** * @since 8.1 */ public static final String STORED_RENDITION_MANAGERS_EP = "storedRenditionManagers"; private static final Log log = LogFactory.getLog(RenditionServiceImpl.class); /** * @deprecated since 7.2. Not used. */ @Deprecated protected AutomationService automationService; /** * @deprecated since 7.3. */ @Deprecated protected Map<String, RenditionDefinition> renditionDefinitions; /** * @since 7.3. RenditionDefinitions are store in {@link #renditionDefinitionRegistry}. */ protected RenditionDefinitionRegistry renditionDefinitionRegistry; protected RenditionDefinitionProviderRegistry renditionDefinitionProviderRegistry; protected static final StoredRenditionManager DEFAULT_STORED_RENDITION_MANAGER = new DefaultStoredRenditionManager(); /** * @since 8.1 */ protected Deque<StoredRenditionManagerDescriptor> storedRenditionManagerDescriptors = new LinkedList<>(); /** * @since 8.1 */ public StoredRenditionManager getStoredRenditionManager() { StoredRenditionManagerDescriptor descr = storedRenditionManagerDescriptors.peekLast(); return descr == null ? DEFAULT_STORED_RENDITION_MANAGER : descr.getStoredRenditionManager(); } @Override public void activate(ComponentContext context) { renditionDefinitions = new HashMap<>(); renditionDefinitionRegistry = new RenditionDefinitionRegistry(); renditionDefinitionProviderRegistry = new RenditionDefinitionProviderRegistry(); super.activate(context); } @Override public void deactivate(ComponentContext context) { renditionDefinitions = null; renditionDefinitionRegistry = null; renditionDefinitionProviderRegistry = null; super.deactivate(context); } public RenditionDefinition getRenditionDefinition(String name) { return renditionDefinitionRegistry.getRenditionDefinition(name); } /** * @deprecated since 7.2 because unused */ @Override @Deprecated public List<RenditionDefinition> getDeclaredRenditionDefinitions() { return new ArrayList<>(renditionDefinitionRegistry.descriptors.values()); } /** * @deprecated since 7.2 because unused */ @Override @Deprecated public List<RenditionDefinition> getDeclaredRenditionDefinitionsForProviderType(String providerType) { List<RenditionDefinition> defs = new ArrayList<>(); for (RenditionDefinition def : getDeclaredRenditionDefinitions()) { if (def.getProviderType().equals(providerType)) { defs.add(def); } } return defs; } @Override public List<RenditionDefinition> getAvailableRenditionDefinitions(DocumentModel doc) { List<RenditionDefinition> defs = new ArrayList<>(); defs.addAll(renditionDefinitionRegistry.getRenditionDefinitions(doc)); defs.addAll(renditionDefinitionProviderRegistry.getRenditionDefinitions(doc)); // XXX what about "lost renditions" ? return defs; } @Override public DocumentRef storeRendition(DocumentModel source, String renditionDefinitionName) { Rendition rendition = getRendition(source, renditionDefinitionName, true); return rendition == null ? null : rendition.getHostDocument().getRef(); } /** * @deprecated since 8.1 */ @Deprecated protected DocumentModel storeRendition(DocumentModel sourceDocument, Rendition rendition, String name) { StoredRendition storedRendition = storeRendition(sourceDocument, rendition); return storedRendition == null ? null : storedRendition.getHostDocument(); } /** * @since 8.1 */ protected StoredRendition storeRendition(DocumentModel sourceDocument, Rendition rendition) { if (!rendition.isCompleted()) { return null; } List<Blob> renderedBlobs = rendition.getBlobs(); Blob renderedBlob = renderedBlobs.get(0); String mimeType = renderedBlob.getMimeType(); if (mimeType != null && mimeType.contains(LazyRendition.ERROR_MARKER)) { return null; } CoreSession session = sourceDocument.getCoreSession(); DocumentModel version = null; boolean isVersionable = sourceDocument.isVersionable(); if (isVersionable) { DocumentRef versionRef = createVersionIfNeeded(sourceDocument, session); version = session.getDocument(versionRef); } RenditionDefinition renditionDefinition = getRenditionDefinition(rendition.getName()); StoredRendition storedRendition = getStoredRenditionManager().createStoredRendition(sourceDocument, version, renderedBlob, renditionDefinition); return storedRendition; } protected DocumentRef createVersionIfNeeded(DocumentModel source, CoreSession session) { if (source.isVersionable()) { if (source.isVersion()) { return source.getRef(); } else if (source.isCheckedOut()) { DocumentRef versionRef = session.checkIn(source.getRef(), VersioningOption.MINOR, null); source.refresh(DocumentModel.REFRESH_STATE, null); return versionRef; } else { return session.getLastDocumentVersionRef(source.getRef()); } } return null; } /** * @deprecated since 7.2. Not used. */ @Deprecated protected AutomationService getAutomationService() { return Framework.getService(AutomationService.class); } @Override public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { if (RENDITION_DEFINITIONS_EP.equals(extensionPoint)) { RenditionDefinition renditionDefinition = (RenditionDefinition) contribution; renditionDefinitionRegistry.addContribution(renditionDefinition); } else if (RENDITON_DEFINION_PROVIDERS_EP.equals(extensionPoint)) { renditionDefinitionProviderRegistry.addContribution((RenditionDefinitionProviderDescriptor) contribution); } else if (STORED_RENDITION_MANAGERS_EP.equals(extensionPoint)) { storedRenditionManagerDescriptors.add(((StoredRenditionManagerDescriptor) contribution)); } } /** * @deprecated since 7.3. RenditionDefinitions are store in {@link #renditionDefinitionRegistry}. */ @Deprecated protected void registerRendition(RenditionDefinition renditionDefinition) { String name = renditionDefinition.getName(); if (name == null) { log.error("Cannot register rendition without a name"); return; } boolean enabled = renditionDefinition.isEnabled(); if (renditionDefinitions.containsKey(name)) { log.info("Overriding rendition with name: " + name); if (enabled) { renditionDefinition = mergeRenditions(renditionDefinitions.get(name), renditionDefinition); } else { log.info("Disabled rendition with name " + name); renditionDefinitions.remove(name); } } if (enabled) { log.info("Registering rendition with name: " + name); renditionDefinitions.put(name, renditionDefinition); } // setup the Provider setupProvider(renditionDefinition); } /** * @deprecated since 7.3. RenditionDefinitions are store in {@link #renditionDefinitionRegistry}. */ @Deprecated protected void setupProvider(RenditionDefinition definition) { if (definition.getProviderClass() == null) { definition.setProvider(new DefaultAutomationRenditionProvider()); } else { try { RenditionProvider provider = definition.getProviderClass().newInstance(); definition.setProvider(provider); } catch (Exception e) { log.error("Unable to create RenditionProvider", e); } } } protected RenditionDefinition mergeRenditions(RenditionDefinition oldRenditionDefinition, RenditionDefinition newRenditionDefinition) { String label = newRenditionDefinition.getLabel(); if (label != null) { oldRenditionDefinition.label = label; } String operationChain = newRenditionDefinition.getOperationChain(); if (operationChain != null) { oldRenditionDefinition.operationChain = operationChain; } return oldRenditionDefinition; } @Override public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { if (RENDITION_DEFINITIONS_EP.equals(extensionPoint)) { renditionDefinitionRegistry.removeContribution((RenditionDefinition) contribution); } else if (RENDITON_DEFINION_PROVIDERS_EP.equals(extensionPoint)) { renditionDefinitionProviderRegistry.removeContribution( (RenditionDefinitionProviderDescriptor) contribution); } else if (STORED_RENDITION_MANAGERS_EP.equals(extensionPoint)) { storedRenditionManagerDescriptors.remove(((StoredRenditionManagerDescriptor) contribution)); } } /** * @deprecated since 7.3. RenditionDefinitions are store in {@link #renditionDefinitionRegistry}. */ @Deprecated protected void unregisterRendition(RenditionDefinition renditionDefinition) { String name = renditionDefinition.getName(); renditionDefinitions.remove(name); log.info("Unregistering rendition with name: " + name); } @Override public Rendition getRendition(DocumentModel doc, String renditionName) { RenditionDefinition renditionDefinition = getAvailableRenditionDefinition(doc, renditionName); return getRendition(doc, renditionDefinition, renditionDefinition.isStoreByDefault()); } @Override public Rendition getRendition(DocumentModel doc, String renditionName, boolean store) { RenditionDefinition renditionDefinition = getAvailableRenditionDefinition(doc, renditionName); return getRendition(doc, renditionDefinition, store); } protected Rendition getRendition(DocumentModel doc, RenditionDefinition renditionDefinition, boolean store) { Rendition rendition = null; boolean isVersionable = doc.isVersionable(); if (!isVersionable || !doc.isCheckedOut()) { // stored renditions are only done against a non-versionable doc // or a versionable doc that is not checkedout rendition = getStoredRenditionManager().findStoredRendition(doc, renditionDefinition); if (rendition != null) { return rendition; } } rendition = new LiveRendition(doc, renditionDefinition); if (store) { StoredRendition storedRendition = storeRendition(doc, rendition); if (storedRendition != null) { return storedRendition; } } return rendition; } protected RenditionDefinition getAvailableRenditionDefinition(DocumentModel doc, String renditionName) { RenditionDefinition renditionDefinition = renditionDefinitionRegistry.getRenditionDefinition(renditionName); if (renditionDefinition == null) { renditionDefinition = renditionDefinitionProviderRegistry.getRenditionDefinition(renditionName, doc); if (renditionDefinition == null) { String message = "The rendition definition '%s' is not registered"; throw new NuxeoException(String.format(message, renditionName)); } } else { // we have a rendition definition but we must check that it can be used for this doc if (!renditionDefinitionRegistry.canUseRenditionDefinition(renditionDefinition, doc)) { throw new NuxeoException("Rendition " + renditionName + " cannot be used for this doc " + doc.getId()); } } if (!renditionDefinition.getProvider().isAvailable(doc, renditionDefinition)) { throw new NuxeoException("Rendition " + renditionName + " not available for this doc " + doc.getId()); } return renditionDefinition; } @Override public List<Rendition> getAvailableRenditions(DocumentModel doc) { return getAvailableRenditions(doc, false); } @Override public List<Rendition> getAvailableRenditions(DocumentModel doc, boolean onlyVisible) { List<Rendition> renditions = new ArrayList<>(); if (doc.isProxy()) { return renditions; } List<RenditionDefinition> defs = getAvailableRenditionDefinitions(doc); if (defs != null) { for (RenditionDefinition def : defs) { if (!onlyVisible || onlyVisible && def.isVisible()) { Rendition rendition = getRendition(doc, def.getName(), false); if (rendition != null) { renditions.add(rendition); } } } } return renditions; } @Override public void deleteStoredRenditions(String repositoryName) { StoredRenditionsCleaner cleaner = new StoredRenditionsCleaner(repositoryName); cleaner.runUnrestricted(); } private final class StoredRenditionsCleaner extends UnrestrictedSessionRunner { private static final int BATCH_SIZE = 100; private StoredRenditionsCleaner(String repositoryName) { super(repositoryName); } @Override public void run() { Map<String, List<String>> sourceIdToRenditionRefs = computeLiveDocumentRefsToRenditionRefs(); removeStoredRenditions(sourceIdToRenditionRefs); } /** * Computes only live documents renditions, the related versions will be deleted by Nuxeo. */ private Map<String, List<String>> computeLiveDocumentRefsToRenditionRefs() { Map<String, List<String>> liveDocumentRefsToRenditionRefs = new HashMap<>(); String query = String.format("SELECT %s, %s, %s FROM Document WHERE %s IS NOT NULL AND ecm:isVersion = 0", NXQL.ECM_UUID, RENDITION_SOURCE_ID_PROPERTY, RENDITION_SOURCE_VERSIONABLE_ID_PROPERTY, RENDITION_SOURCE_ID_PROPERTY); try (IterableQueryResult result = session.queryAndFetch(query, NXQL.NXQL)) { for (Map<String, Serializable> res : result) { String renditionRef = res.get(NXQL.ECM_UUID).toString(); String sourceId = res.get(RENDITION_SOURCE_ID_PROPERTY).toString(); Serializable sourceVersionableId = res.get(RENDITION_SOURCE_VERSIONABLE_ID_PROPERTY); String key = sourceVersionableId != null ? sourceVersionableId.toString() : sourceId; liveDocumentRefsToRenditionRefs.computeIfAbsent(key, k -> new ArrayList<>()).add(renditionRef); } } return liveDocumentRefsToRenditionRefs; } private void removeStoredRenditions(Map<String, List<String>> liveDocumentRefsToRenditionRefs) { List<String> liveDocumentRefs = new ArrayList<>(liveDocumentRefsToRenditionRefs.keySet()); if (liveDocumentRefs.isEmpty()) { // no more document to check return; } int processedSourceIds = 0; while (processedSourceIds < liveDocumentRefs.size()) { // compute the batch of source ids to check for existence int limit = processedSourceIds + BATCH_SIZE > liveDocumentRefs.size() ? liveDocumentRefs.size() : processedSourceIds + BATCH_SIZE; List<String> batchSourceIds = liveDocumentRefs.subList(processedSourceIds, limit); // retrieve still existing documents List<String> existingSourceIds = new ArrayList<>(); String query = NXQLQueryBuilder.getQuery("SELECT ecm:uuid FROM Document WHERE ecm:uuid IN ?", new Object[] { batchSourceIds }, true, true, null); try (IterableQueryResult result = session.queryAndFetch(query, NXQL.NXQL)) { result.forEach(res -> existingSourceIds.add(res.get(NXQL.ECM_UUID).toString())); } batchSourceIds.removeAll(existingSourceIds); List<String> renditionRefsToDelete = batchSourceIds.stream() .map(liveDocumentRefsToRenditionRefs::get) .reduce(new ArrayList<>(), (allRefs, refs) -> { allRefs.addAll(refs); return allRefs; }); if (!renditionRefsToDelete.isEmpty()) { session.removeDocuments( renditionRefsToDelete.stream().map(IdRef::new).collect(Collectors.toList()).toArray( new DocumentRef[renditionRefsToDelete.size()])); } if (TransactionHelper.isTransactionActiveOrMarkedRollback()) { TransactionHelper.commitOrRollbackTransaction(); TransactionHelper.startTransaction(); } // next batch processedSourceIds += BATCH_SIZE; } } } }