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}