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}