001/*
002 * (C) Copyright 2017 Nuxeo (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 *     Guillaume Renard <grenard@nuxeo.com>
018 */
019package org.nuxeo.ecm.automation.core.operations.blob;
020
021import java.io.IOException;
022import java.util.Calendar;
023import java.util.Collections;
024import java.util.List;
025import java.util.stream.Collectors;
026
027import org.apache.commons.codec.digest.DigestUtils;
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030import org.nuxeo.ecm.automation.core.Constants;
031import org.nuxeo.ecm.automation.core.annotations.Context;
032import org.nuxeo.ecm.automation.core.annotations.Operation;
033import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
034import org.nuxeo.ecm.automation.core.annotations.Param;
035import org.nuxeo.ecm.automation.core.work.BlobListZipWork;
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.DocumentModelList;
040import org.nuxeo.ecm.core.api.NuxeoException;
041import org.nuxeo.ecm.core.api.blobholder.BlobHolderAdapterService;
042import org.nuxeo.ecm.core.api.impl.blob.AsyncBlob;
043import org.nuxeo.ecm.core.io.download.DownloadService;
044import org.nuxeo.ecm.core.transientstore.api.TransientStore;
045import org.nuxeo.ecm.core.transientstore.api.TransientStoreService;
046import org.nuxeo.ecm.core.work.api.Work;
047import org.nuxeo.ecm.core.work.api.WorkManager;
048import org.nuxeo.ecm.core.work.api.WorkManager.Scheduling;
049import org.nuxeo.runtime.api.Framework;
050
051/**
052 * Asynchronous Bulk Download Operation.
053 *
054 * @since 9.3
055 */
056@Operation(id = BulkDownload.ID, category = Constants.CAT_BLOB, label = "Bulk Downlaod", description = "Prepare a Zip of a list of documents which is build asynchrously. Produced Zip will be available in the TransientStore with the key returned by the JSON.")
057public class BulkDownload {
058
059    private static final Log log = LogFactory.getLog(BulkDownload.class);
060
061    public static final String ID = "Blob.BulkDownload";
062
063    public static final String WORKERID_KEY = "workerid";
064
065    @Context
066    protected CoreSession session;
067
068    @Context
069    BlobHolderAdapterService blobHolderAdapterService;
070
071    @Param(name = "filename", required = false)
072    protected String fileName;
073
074    protected String buildTransientStoreKey(DocumentModelList docs) {
075        StringBuffer sb = new StringBuffer();
076        for (DocumentModel doc : docs) {
077            sb.append(doc.getId());
078            sb.append("::");
079            Calendar modif = (Calendar) doc.getPropertyValue("dc:modified");
080            if (modif != null) {
081                long millis = modif.getTimeInMillis();
082                // the date may have been rounded by the storage layer, normalize it to the second
083                millis -= millis % 1000;
084                sb.append(millis);
085                sb.append("::");
086            }
087        }
088        // Rendered documents might differ according to current user
089        sb.append(session.getPrincipal().getName());
090        return DigestUtils.md5Hex(sb.toString());
091    }
092
093    @OperationMethod
094    public Blob run(DocumentModelList docs) throws IOException {
095        // build the key
096        String key = buildTransientStoreKey(docs);
097        TransientStoreService tss = Framework.getService(TransientStoreService.class);
098
099        TransientStore ts = tss.getStore(DownloadService.TRANSIENT_STORE_STORE_NAME);
100        if (ts == null) {
101            throw new NuxeoException("Unable to find download Transient Store");
102        }
103        List<Blob> blobs = null;
104        if (!ts.exists(key)) {
105            log.trace("No async download already initialized");
106            Work work = new BlobListZipWork(key, session.getPrincipal().getName(), this.fileName,
107                    docs.stream().map(DocumentModel::getId).collect(Collectors.toList()),
108                    DownloadService.TRANSIENT_STORE_STORE_NAME);
109            ts.setCompleted(key, false);
110            ts.putParameter(key, WORKERID_KEY, work.getId());
111            blobs = Collections.singletonList(new AsyncBlob(key));
112            ts.putBlobs(key, blobs);
113            Framework.getService(WorkManager.class).schedule(work, Scheduling.IF_NOT_SCHEDULED);
114            return blobs.get(0);
115        } else {
116            log.trace("Async download already initialized");
117            blobs = ts.getBlobs(key);
118            if (ts.isCompleted(key)) {
119                if (blobs != null && blobs.size() == 1) {
120                    Blob blob = blobs.get(0);
121                    ts.release(key);
122                    return blob;
123                } else {
124                    ts.release(key);
125                    throw new NuxeoException("Cannot retrieve blob");
126                }
127
128            } else {
129                Work work = new BlobListZipWork(key, session.getPrincipal().getName(), this.fileName,
130                        docs.stream().map(DocumentModel::getId).collect(Collectors.toList()),
131                        DownloadService.TRANSIENT_STORE_STORE_NAME);
132                WorkManager wm = Framework.getService(WorkManager.class);
133                wm.schedule(work, Scheduling.IF_NOT_SCHEDULED);
134                return new AsyncBlob(key);
135            }
136        }
137    }
138
139}