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}