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