001/*
002 * (C) Copyright 2006-2015 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Thierry Delprat
016 *     Florent Guillaume
017 */
018package org.nuxeo.ecm.platform.ui.web.download;
019
020import static org.nuxeo.ecm.core.io.download.DownloadService.NXBIGBLOB;
021import static org.nuxeo.ecm.core.io.download.DownloadService.NXBIGZIPFILE;
022import static org.nuxeo.ecm.core.io.download.DownloadService.NXDOWNLOADINFO;
023import static org.nuxeo.ecm.core.io.download.DownloadService.NXFILE;
024
025import java.io.File;
026import java.io.IOException;
027import java.net.URI;
028import java.net.URISyntaxException;
029import java.net.URLEncoder;
030import java.util.Arrays;
031
032import javax.servlet.http.HttpServlet;
033import javax.servlet.http.HttpServletRequest;
034import javax.servlet.http.HttpServletResponse;
035import javax.servlet.http.HttpSession;
036
037import org.apache.commons.lang.StringUtils;
038import org.apache.commons.lang3.tuple.Pair;
039import org.apache.commons.logging.Log;
040import org.apache.commons.logging.LogFactory;
041import org.nuxeo.ecm.core.api.Blob;
042import org.nuxeo.ecm.core.api.Blobs;
043import org.nuxeo.ecm.core.api.CoreInstance;
044import org.nuxeo.ecm.core.api.CoreSession;
045import org.nuxeo.ecm.core.api.DocumentModel;
046import org.nuxeo.ecm.core.api.DocumentRef;
047import org.nuxeo.ecm.core.api.IdRef;
048import org.nuxeo.ecm.core.api.NuxeoException;
049import org.nuxeo.ecm.core.io.download.DownloadHelper;
050import org.nuxeo.ecm.core.io.download.DownloadService;
051import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
052import org.nuxeo.runtime.api.Framework;
053import org.nuxeo.runtime.transaction.TransactionHelper;
054
055/**
056 * Simple download servlet used for big files that can not be downloaded from within the JSF context (because of
057 * buffered ResponseWrapper).
058 */
059public class DownloadServlet extends HttpServlet {
060
061    private static final long serialVersionUID = 1L;
062
063    private static final Log log = LogFactory.getLog(DownloadServlet.class);
064
065    /** @deprecated since 7.4, use nxfile instead */
066    @Deprecated
067    public static final String NXBIGFILE = "nxbigfile";
068
069    @Override
070    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
071        try {
072            handleDownload(req, resp);
073        } catch (IOException ioe) {
074            DownloadHelper.handleClientDisconnect(ioe);
075        }
076    }
077
078    protected void handleDownload(HttpServletRequest req, HttpServletResponse resp) throws IOException {
079        String requestURI;
080        try {
081            requestURI = new URI(req.getRequestURI()).getPath();
082        } catch (URISyntaxException e) {
083            requestURI = req.getRequestURI();
084        }
085        // remove context
086        String context = VirtualHostHelper.getContextPath(req) + "/";
087        if (!requestURI.startsWith(context)) {
088            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Invalid URL syntax");
089            return;
090        }
091        String localURI = requestURI.substring(context.length());
092        // find what to do
093        int slash = localURI.indexOf('/');
094        if (slash < 0) {
095            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Invalid URL syntax");
096            return;
097        }
098        String what = localURI.substring(0, slash);
099        String path = localURI.substring(slash + 1);
100        switch (what) {
101        case NXFILE:
102        case NXBIGFILE:
103            downloadBlob(req, resp, path, false);
104            break;
105        case NXDOWNLOADINFO:
106            // used by nxdropout.js
107            downloadBlob(req, resp, path, true);
108            break;
109        case NXBIGZIPFILE:
110            // handle the download for a big zip created in the tmp directory;
111            // the name of this zip is sent in the request
112            handleDownloadTemporaryZip(req, resp, path);
113            break;
114        case NXBIGBLOB:
115            // handle the download of a Blob referenced in HTTP Request or Session
116            handleDownloadSessionBlob(req, resp, path);
117            break;
118        default:
119            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Invalid URL syntax");
120        }
121    }
122
123    protected void downloadBlob(HttpServletRequest req, HttpServletResponse resp, String urlPath, boolean info)
124            throws IOException {
125        String[] parts = urlPath.split("/");
126        if (parts.length < 2) {
127            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Invalid URL syntax");
128            return;
129        }
130        String repositoryName = parts[0];
131        String docId = parts[1];
132        boolean tx = false;
133        try {
134            if (!TransactionHelper.isTransactionActive()) {
135                // Manually start and stop a transaction around repository access to be able to release transactional
136                // resources without waiting for the download that can take a long time (longer than the transaction
137                // timeout) especially if the client or the connection is slow.
138                tx = TransactionHelper.startTransaction();
139            }
140            try (CoreSession session = CoreInstance.openCoreSession(repositoryName)) {
141                DocumentRef docRef = new IdRef(docId);
142                if (!session.exists(docRef)) {
143                    resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No Blob found");
144                    return;
145                }
146                DocumentModel doc = session.getDocument(docRef);
147                Pair<String, String> pair = parsePath(urlPath);
148                String xpath = pair.getLeft();
149                String filename = pair.getRight();
150                DownloadService downloadService = Framework.getService(DownloadService.class);
151                if (info) {
152                    Blob blob = downloadService.resolveBlob(doc, xpath);
153                    if (blob == null) {
154                        resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No blob found");
155                        return;
156                    }
157                    String downloadUrl = VirtualHostHelper.getBaseURL(req)
158                            + downloadService.getDownloadUrl(doc, xpath, filename);
159                    String result = blob.getMimeType() + ':' + URLEncoder.encode(blob.getFilename(), "UTF-8") + ':'
160                            + downloadUrl;
161                    resp.setContentType("text/plain");
162                    resp.getWriter().write(result);
163                    resp.getWriter().flush();
164
165                } else {
166                    downloadService.downloadBlob(req, resp, doc, xpath, null, filename, "download");
167                }
168            }
169        } catch (NuxeoException e) {
170            if (tx) {
171                TransactionHelper.setTransactionRollbackOnly();
172            }
173            throw new IOException(e);
174        } finally {
175            if (tx) {
176                TransactionHelper.commitOrRollbackTransaction();
177            }
178        }
179    }
180
181    // first two parts are repository name and doc id, already parsed
182    protected static Pair<String, String> parsePath(String urlPath) {
183        String[] parts = urlPath.split("/");
184        int length = parts.length;
185        String xpath;
186        String filename;
187        if (length == 2) {
188            xpath = DownloadService.BLOBHOLDER_0;
189            filename = null;
190        } else if (length == 3) {
191            xpath = parts[2];
192            filename = null;
193        } else {
194            xpath = StringUtils.join(Arrays.asList(parts).subList(2, length - 1), "/");
195            filename = parts[length - 1];
196        }
197        return Pair.of(xpath, filename);
198    }
199
200    // used by DownloadFile operation
201    protected void handleDownloadSessionBlob(HttpServletRequest req, HttpServletResponse resp, String blobId)
202            throws IOException {
203        Blob blob = (Blob) req.getAttribute(blobId);
204        if (blob != null) {
205            req.removeAttribute(blobId);
206        } else {
207            HttpSession session = req.getSession(false);
208            if (session == null) {
209                log.error("Unable to download blob " + blobId + " since the holding http session does not exist");
210                return;
211            }
212            blob = (Blob) session.getAttribute(blobId);
213            if (blob == null) {
214                return;
215            }
216            session.removeAttribute(blobId);
217        }
218        DownloadService downloadService = Framework.getService(DownloadService.class);
219        downloadService.downloadBlob(req, resp, null, null, blob, null, "operation");
220    }
221
222    // used by ClipboardActionsBean
223    protected void handleDownloadTemporaryZip(HttpServletRequest req, HttpServletResponse resp, String filePath)
224            throws IOException {
225        String[] pathParts = filePath.split("/");
226        String tmpFileName = pathParts[0];
227        File tmpZip = new File(System.getProperty("java.io.tmpdir") + "/" + tmpFileName);
228        try {
229            Blob zipBlob = Blobs.createBlob(tmpZip);
230            DownloadService downloadService = Framework.getService(DownloadService.class);
231            downloadService.downloadBlob(req, resp, null, null, zipBlob, "clipboard.zip", "clipboardZip");
232        } finally {
233            tmpZip.delete();
234        }
235    }
236
237}