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}