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