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}