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