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}