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