001package org.nuxeo.ecm.webapp.clipboard; 002 003import java.io.BufferedInputStream; 004import java.io.File; 005import java.io.FileOutputStream; 006import java.io.IOException; 007import java.text.SimpleDateFormat; 008import java.util.ArrayList; 009import java.util.Calendar; 010import java.util.List; 011import java.util.zip.ZipEntry; 012import java.util.zip.ZipException; 013import java.util.zip.ZipOutputStream; 014 015import org.nuxeo.common.utils.StringUtils; 016import org.nuxeo.ecm.core.api.Blob; 017import org.nuxeo.ecm.core.api.Blobs; 018import org.nuxeo.ecm.core.api.CoreSession; 019import org.nuxeo.ecm.core.api.DocumentModel; 020import org.nuxeo.ecm.core.api.LifeCycleConstants; 021import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 022import org.nuxeo.runtime.api.Framework; 023 024public class DocumentListZipExporter { 025 026 public static final String ZIP_ENTRY_ENCODING_PROPERTY = "zip.entry.encoding"; 027 028 public static enum ZIP_ENTRY_ENCODING_OPTIONS { 029 ascii 030 } 031 032 private static final int BUFFER = 2048; 033 034 private static final String SUMMARY_FILENAME = "INDEX.txt"; 035 036 public File exportWorklistAsZip(List<DocumentModel> documents, CoreSession documentManager, boolean exportAllBlobs) 037 throws IOException { 038 StringBuilder blobList = new StringBuilder(); 039 040 File tmpFile = File.createTempFile("NX-BigZipFile-", ".zip"); 041 tmpFile.deleteOnExit(); // file is deleted after being downloaded in 042 // DownloadServlet 043 FileOutputStream fout = new FileOutputStream(tmpFile); 044 ZipOutputStream out = new ZipOutputStream(fout); 045 out.setMethod(ZipOutputStream.DEFLATED); 046 out.setLevel(9); 047 byte[] data = new byte[BUFFER]; 048 049 for (DocumentModel doc : documents) { 050 051 // first check if DM is attached to the core 052 if (doc.getSessionId() == null) { 053 // refetch the doc from the core 054 doc = documentManager.getDocument(doc.getRef()); 055 } 056 057 // NXP-2334 : skip deleted docs 058 if (LifeCycleConstants.DELETED_STATE.equals(doc.getCurrentLifeCycleState())) { 059 continue; 060 } 061 062 BlobHolder bh = doc.getAdapter(BlobHolder.class); 063 if (doc.isFolder() && !isEmptyFolder(doc, documentManager)) { 064 addFolderToZip("", out, doc, data, documentManager, blobList, exportAllBlobs); 065 } else if (bh != null) { 066 addBlobHolderToZip("", out, doc, data, blobList, bh, exportAllBlobs); 067 } 068 } 069 if (blobList.length() > 1) { 070 addSummaryToZip(out, data, blobList); 071 } 072 try { 073 out.close(); 074 fout.close(); 075 } catch (ZipException e) { 076 return null; 077 } 078 return tmpFile; 079 } 080 081 private void addFolderToZip(String path, ZipOutputStream out, DocumentModel doc, byte[] data, 082 CoreSession documentManager, StringBuilder blobList, boolean exportAllBlobs) throws 083 IOException { 084 085 String title = doc.getTitle(); 086 List<DocumentModel> docList = documentManager.getChildren(doc.getRef()); 087 for (DocumentModel docChild : docList) { 088 // NXP-2334 : skip deleted docs 089 if (LifeCycleConstants.DELETED_STATE.equals(docChild.getCurrentLifeCycleState())) { 090 continue; 091 } 092 BlobHolder bh = docChild.getAdapter(BlobHolder.class); 093 String newPath = null; 094 if (path.length() == 0) { 095 newPath = title; 096 } else { 097 newPath = path + "/" + title; 098 } 099 if (docChild.isFolder() && !isEmptyFolder(docChild, documentManager)) { 100 addFolderToZip(newPath, out, docChild, data, documentManager, blobList, exportAllBlobs); 101 } else if (bh != null) { 102 addBlobHolderToZip(newPath, out, docChild, data, blobList, bh, exportAllBlobs); 103 } 104 } 105 } 106 107 private boolean isEmptyFolder(DocumentModel doc, CoreSession documentManager) { 108 109 List<DocumentModel> docList = documentManager.getChildren(doc.getRef()); 110 for (DocumentModel docChild : docList) { 111 // If there is a blob or a folder, it is not empty. 112 if (docChild.getAdapter(BlobHolder.class) != null || docChild.isFolder()) { 113 return false; 114 } 115 } 116 return true; 117 } 118 119 /** 120 * Writes a summary file and puts it in the archive. 121 */ 122 private void addSummaryToZip(ZipOutputStream out, byte[] data, StringBuilder sb) throws IOException { 123 124 Blob content = Blobs.createBlob(sb.toString()); 125 126 BufferedInputStream buffi = new BufferedInputStream(content.getStream(), BUFFER); 127 128 ZipEntry entry = new ZipEntry(SUMMARY_FILENAME); 129 out.putNextEntry(entry); 130 int count = buffi.read(data, 0, BUFFER); 131 132 while (count != -1) { 133 out.write(data, 0, count); 134 count = buffi.read(data, 0, BUFFER); 135 } 136 out.closeEntry(); 137 buffi.close(); 138 } 139 140 private void addBlobHolderToZip(String path, ZipOutputStream out, DocumentModel doc, byte[] data, 141 StringBuilder blobList, BlobHolder bh, boolean exportAllBlobs) throws IOException { 142 List<Blob> blobs = new ArrayList<Blob>(); 143 144 if (exportAllBlobs) { 145 if (bh.getBlobs() != null) { 146 blobs = bh.getBlobs(); 147 } 148 } else { 149 Blob mainBlob = bh.getBlob(); 150 if (mainBlob != null) { 151 blobs.add(mainBlob); 152 } 153 } 154 155 if (blobs.size() > 0) { // add document info 156 SimpleDateFormat format = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"); 157 if (path.length() > 0) { 158 blobList.append(path).append('/'); 159 } 160 blobList.append(doc.getTitle()).append(" "); 161 blobList.append(doc.getType()).append(" "); 162 163 Calendar c = (Calendar) doc.getPropertyValue("dc:modified"); 164 if (c != null) { 165 blobList.append(format.format(c.getTime())); 166 } 167 blobList.append("\n"); 168 } 169 170 for (Blob content : blobs) { 171 String fileName = content.getFilename(); 172 if (fileName == null) { 173 // use a default value 174 fileName = "file.bin"; 175 } 176 BufferedInputStream buffi = new BufferedInputStream(content.getStream(), BUFFER); 177 178 // Workaround to deal with duplicate file names. 179 int tryCount = 0; 180 String entryPath = null; 181 String entryName = null; 182 while (true) { 183 try { 184 ZipEntry entry = null; 185 if (tryCount == 0) { 186 entryName = fileName; 187 } else { 188 entryName = formatFileName(fileName, "(" + tryCount + ")"); 189 } 190 if (path.length() == 0) { 191 entryPath = entryName; 192 } else { 193 entryPath = path + "/" + entryName; 194 } 195 entryPath = escapeEntryPath(entryPath); 196 entry = new ZipEntry(entryPath); 197 out.putNextEntry(entry); 198 break; 199 } catch (ZipException e) { 200 tryCount++; 201 } 202 } 203 blobList.append(" - ").append(entryName).append("\n"); 204 205 int count = buffi.read(data, 0, BUFFER); 206 while (count != -1) { 207 out.write(data, 0, count); 208 count = buffi.read(data, 0, BUFFER); 209 } 210 out.closeEntry(); 211 buffi.close(); 212 } 213 } 214 215 private String formatFileName(String filename, String count) { 216 StringBuilder sb = new StringBuilder(); 217 CharSequence name = filename.subSequence(0, filename.lastIndexOf(".")); 218 CharSequence extension = filename.subSequence(filename.lastIndexOf("."), filename.length()); 219 sb.append(name).append(count).append(extension); 220 return sb.toString(); 221 } 222 223 protected String escapeEntryPath(String path) { 224 String zipEntryEncoding = Framework.getProperty(ZIP_ENTRY_ENCODING_PROPERTY); 225 if (zipEntryEncoding != null && zipEntryEncoding.equals(ZIP_ENTRY_ENCODING_OPTIONS.ascii.toString())) { 226 return StringUtils.toAscii(path, true); 227 } 228 return path; 229 } 230 231}