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