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>();
094        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            // Check for chunk entries to remove
215            BatchFileEntry fileEntry = (BatchFileEntry) getFileEntry(fileIndex, false);
216            if (fileEntry != null) {
217                if (fileEntry.isChunked()) {
218                    for (String chunkEntryKey : fileEntry.getChunkEntryKeys()) {
219                        // Remove chunk entry from the store and delete blobs from the file system
220                        List<Blob> chunkBlobs = ts.getBlobs(chunkEntryKey);
221                        if (chunkBlobs != null) {
222                            for (Blob blob : chunkBlobs) {
223                                try {
224                                    FileUtils.deleteDirectory(blob.getFile().getParentFile());
225                                } catch (IOException e) {
226                                    log.error("Error while deleting chunk parent directory", e);
227                                }
228                            }
229                        }
230                        ts.remove(chunkEntryKey);
231                    }
232                    fileEntry.beforeRemove();
233                }
234                // Remove file entry from the store and delete blobs from the file system
235                String fileEntryKey = fileEntry.getKey();
236                List<Blob> fileBlobs = ts.getBlobs(fileEntryKey);
237                if (fileBlobs != null) {
238                    for (Blob blob : fileBlobs) {
239                        try {
240                            FileUtils.deleteDirectory(blob.getFile().getParentFile());
241                        } catch (IOException e) {
242                            log.error("Error while deleting file parent directory", e);
243                        }
244                    }
245                }
246                ts.remove(fileEntryKey);
247            }
248        }
249        // Remove batch entry
250        ts.remove(key);
251    }
252
253}