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}