001/*
002 * (C) Copyright 2015 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 *     Thierry Delprat <tdelprat@nuxeo.com>
018 *     Antoine Taillefer <ataillefer@nuxeo.com>
019 *     Gabriel Barata <gbarata@nuxeo.com>
020 *
021 */
022package org.nuxeo.ecm.automation.server.jaxrs.batch;
023
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.Serializable;
027import java.util.ArrayList;
028import java.util.Collections;
029import java.util.Comparator;
030import java.util.HashMap;
031import java.util.List;
032import java.util.Map;
033
034import org.apache.commons.io.FileUtils;
035import org.apache.commons.logging.Log;
036import org.apache.commons.logging.LogFactory;
037import org.nuxeo.ecm.core.api.Blob;
038import org.nuxeo.ecm.core.api.Blobs;
039import org.nuxeo.ecm.core.transientstore.api.TransientStore;
040import org.nuxeo.runtime.api.Framework;
041
042/**
043 * Batch Object to encapsulate all data related to a batch, especially the temporary files used for Blobs.
044 * <p>
045 * Since 7.4 a batch is backed by the {@link TransientStore}.
046 *
047 * @since 5.4.2
048 */
049public class Batch {
050
051    protected static final Log log = LogFactory.getLog(Batch.class);
052
053    public static final String CHUNKED_PARAM_NAME = "chunked";
054
055    protected String key;
056
057    protected Map<String, Serializable> fileEntries;
058
059    public Batch(String key) {
060        this(key, new HashMap<>());
061    }
062
063    public Batch(String key, Map<String, Serializable> fileEntries) {
064        this.key = key;
065        this.fileEntries = fileEntries;
066    }
067
068    public String getKey() {
069        return key;
070    }
071
072    /**
073     * Returns the uploaded blobs in the order the user chose to upload them.
074     */
075    public List<Blob> getBlobs() {
076        List<Blob> blobs = new ArrayList<Blob>();
077        List<String> sortedFileIndexes = getOrderedFileIndexes();
078        log.debug(String.format("Retrieving blobs for batch %s: %s", key, sortedFileIndexes));
079        for (String index : sortedFileIndexes) {
080            Blob blob = retrieveBlob(index);
081            if (blob != null) {
082                blobs.add(blob);
083            }
084        }
085        return blobs;
086    }
087
088    public Blob getBlob(String index) {
089        log.debug(String.format("Retrieving blob %s for batch %s", index, key));
090        return retrieveBlob(index);
091    }
092
093    protected List<String> getOrderedFileIndexes() {
094        List<String> sortedFileIndexes = new ArrayList<String>(fileEntries.keySet());
095        Collections.sort(sortedFileIndexes, new Comparator<String>() {
096            @Override
097            public int compare(String o1, String o2) {
098                return Integer.valueOf(o1).compareTo(Integer.valueOf(o2));
099            }
100        });
101        return sortedFileIndexes;
102    }
103
104    protected Blob retrieveBlob(String index) {
105        Blob blob = null;
106        BatchFileEntry fileEntry = getFileEntry(index);
107        if (fileEntry != null) {
108            blob = fileEntry.getBlob();
109        }
110        return blob;
111    }
112
113    public List<BatchFileEntry> getFileEntries() {
114        List<BatchFileEntry> batchFileEntries = new ArrayList<BatchFileEntry>();
115        List<String> sortedFileIndexes = getOrderedFileIndexes();
116        for (String index : sortedFileIndexes) {
117            BatchFileEntry fileEntry = getFileEntry(index);
118            if (fileEntry != null) {
119                batchFileEntries.add(fileEntry);
120            }
121        }
122        return batchFileEntries;
123    }
124
125    public BatchFileEntry getFileEntry(String index) {
126        return getFileEntry(index, true);
127    }
128
129    public BatchFileEntry getFileEntry(String index, boolean fetchBlobs) {
130        BatchManager bm = Framework.getService(BatchManager.class);
131        String fileEntryKey = (String) fileEntries.get(index);
132        if (fileEntryKey == null) {
133            return null;
134        }
135        TransientStore ts = bm.getTransientStore();
136        Map<String, Serializable> fileEntryParams = ts.getParameters(fileEntryKey);
137        if (fileEntryParams == null) {
138            return null;
139        }
140        boolean chunked = Boolean.parseBoolean((String) fileEntryParams.get(CHUNKED_PARAM_NAME));
141        if (chunked) {
142            return new BatchFileEntry(fileEntryKey, fileEntryParams);
143        } else {
144            Blob blob = null;
145            if (fetchBlobs) {
146                List<Blob> fileEntryBlobs = ts.getBlobs(fileEntryKey);
147                if (fileEntryBlobs == null) {
148                    return null;
149                }
150                if (!fileEntryBlobs.isEmpty()) {
151                    blob = fileEntryBlobs.get(0);
152                }
153            }
154            return new BatchFileEntry(fileEntryKey, blob);
155        }
156    }
157
158    /**
159     * Adds a file with the given {@code index} to the batch.
160     *
161     * @return The key of the new {@link BatchFileEntry}.
162     */
163    public String addFile(String index, InputStream is, String name, String mime) throws IOException {
164        String mimeType = mime;
165        if (mimeType == null) {
166            mimeType = "application/octet-stream";
167        }
168        Blob blob = Blobs.createBlob(is, mime);
169        blob.setFilename(name);
170
171        String fileEntryKey = key + "_" + index;
172        BatchManager bm = Framework.getService(BatchManager.class);
173        TransientStore ts = bm.getTransientStore();
174        ts.putBlobs(fileEntryKey, Collections.singletonList(blob));
175        ts.putParameter(fileEntryKey, CHUNKED_PARAM_NAME, String.valueOf(false));
176        ts.putParameter(key, index, fileEntryKey);
177
178        return fileEntryKey;
179    }
180
181    /**
182     * Adds a chunk with the given {@code chunkIndex} to the batch file with the given {@code index}.
183     *
184     * @return The key of the {@link BatchFileEntry}.
185     * @since 7.4
186     */
187    public String addChunk(String index, InputStream is, int chunkCount, int chunkIndex, String fileName,
188            String mimeType, long fileSize) throws IOException {
189        BatchManager bm = Framework.getService(BatchManager.class);
190        Blob blob = Blobs.createBlob(is);
191
192        String fileEntryKey = key + "_" + index;
193        BatchFileEntry fileEntry = getFileEntry(index);
194        if (fileEntry == null) {
195            fileEntry = new BatchFileEntry(fileEntryKey, chunkCount, fileName, mimeType, fileSize);
196            TransientStore ts = bm.getTransientStore();
197            ts.putParameters(fileEntryKey, fileEntry.getParams());
198            ts.putParameter(key, index, fileEntryKey);
199        }
200        fileEntry.addChunk(chunkIndex, blob);
201
202        return fileEntryKey;
203    }
204
205    /**
206     * @since 7.4
207     */
208    public void clean() {
209        // Remove batch and all related storage entries from transient store, GC will clean up the files
210        log.debug(String.format("Cleaning batch %s", key));
211        BatchManager bm = Framework.getService(BatchManager.class);
212        TransientStore ts = bm.getTransientStore();
213        for (String fileIndex : fileEntries.keySet()) {
214            removeFileEntry(fileIndex, ts);
215        }
216        // Remove batch entry
217        ts.remove(key);
218    }
219
220    /**
221     * @since 8.4
222     */
223    public boolean removeFileEntry(String index, TransientStore ts) {
224        // Check for chunk entries to remove
225        BatchFileEntry fileEntry = getFileEntry(index, false);
226        if (fileEntry == null) {
227            return false;
228        }
229        if (fileEntry.isChunked()) {
230            for (String chunkEntryKey : fileEntry.getChunkEntryKeys()) {
231                // Remove chunk entry from the store and delete blobs from the file system
232                List<Blob> chunkBlobs = ts.getBlobs(chunkEntryKey);
233                if (chunkBlobs != null) {
234                    for (Blob blob : chunkBlobs) {
235                        FileUtils.deleteQuietly(blob.getFile().getParentFile());
236                    }
237                }
238                ts.remove(chunkEntryKey);
239            }
240            fileEntry.beforeRemove();
241        }
242        // Remove file entry from the store and delete blobs from the file system
243        String fileEntryKey = fileEntry.getKey();
244        List<Blob> fileBlobs = ts.getBlobs(fileEntryKey);
245        if (fileBlobs != null) {
246            for (Blob blob : fileBlobs) {
247                FileUtils.deleteQuietly(blob.getFile().getParentFile());
248            }
249        }
250        ts.remove(fileEntryKey);
251        return true;
252    }
253
254    /**
255     * @since 8.4
256     */
257    public boolean removeFileEntry(String index) {
258        BatchManager bm = Framework.getService(BatchManager.class);
259        TransientStore ts = bm.getTransientStore();
260        return removeFileEntry(index, ts);
261    }
262}