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