001/*
002 * (C) Copyright 2015 Nuxeo SA (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 *     Florent Guillaume
016 */
017package org.nuxeo.ecm.core.io.download;
018
019import java.io.IOException;
020
021import javax.servlet.http.HttpServletRequest;
022
023import org.apache.commons.logging.Log;
024import org.apache.commons.logging.LogFactory;
025import org.nuxeo.common.utils.RFC2231;
026import org.nuxeo.ecm.core.io.download.DownloadService.ByteRange;
027
028/**
029 * Helper class related to the download service.
030 *
031 * @since 7.3
032 */
033public class DownloadHelper {
034
035    private static final Log log = LogFactory.getLog(DownloadHelper.class);
036
037    // tomcat catalina
038    private static final String CLIENT_ABORT_EXCEPTION = "ClientAbortException";
039
040    // jetty (with CamelCase "Eof")
041    private static final String EOF_EXCEPTION = "EofException";
042
043    // utility class
044    private DownloadHelper() {
045    }
046
047    /**
048     * Parses a byte range.
049     *
050     * @param range the byte range as a string
051     * @param length the file length
052     * @return the byte range, or {@code null} if it couldn't be parsed.
053     */
054    public static ByteRange parseRange(String range, long length) {
055        try {
056            // TODO does no support multiple ranges
057            if (!range.startsWith("bytes=") || range.indexOf(',') >= 0) {
058                return null;
059            }
060            int i = range.indexOf('-', 6);
061            if (i < 0) {
062                return null;
063            }
064            String start = range.substring(6, i).trim();
065            String end = range.substring(i + 1).trim();
066            long rangeStart = 0;
067            long rangeEnd = length - 1;
068            if (start.isEmpty()) {
069                if (end.isEmpty()) {
070                    return null;
071                }
072                rangeStart = length - Integer.parseInt(end);
073                if (rangeStart < 0) {
074                    rangeStart = 0;
075                }
076            } else {
077                rangeStart = Integer.parseInt(start);
078                if (!end.isEmpty()) {
079                    rangeEnd = Integer.parseInt(end);
080                }
081            }
082            if (rangeStart > rangeEnd) {
083                return null;
084            }
085            return new ByteRange(rangeStart, rangeEnd);
086        } catch (NumberFormatException e) {
087            return null;
088        }
089    }
090
091    /**
092     * Generates a {@code Content-Disposition} string based on the servlet request for a given filename.
093     * <p>
094     * The value follows RFC2231.
095     *
096     * @param request the http servlet request
097     * @param filename the filename
098     * @return a full string to set as value of a {@code Content-Disposition} header
099     */
100    public static String getRFC2231ContentDisposition(HttpServletRequest request, String filename) {
101
102        String inline = request.getParameter("inline");
103        if (inline == null) {
104            inline = (String) request.getAttribute("inline");
105        }
106        boolean inlineFlag = (inline == null || "false".equals(inline)) ? false : true;
107
108        String userAgent = request.getHeader("User-Agent");
109        return RFC2231.encodeContentDisposition(filename, inlineFlag, userAgent);
110
111    }
112
113    public static boolean isClientAbortError(Throwable t) {
114        int loops = 20; // no infinite loop
115        while (t != null && loops > 0) {
116            if (t instanceof IOException) {
117                // handle all IOException that are ClientAbortException by looking
118                // at their class name since the package name is not the same for
119                // jboss, glassfish, tomcat and jetty and we don't want to add
120                // implementation specific build dependencies to this project
121                String name = t.getClass().getSimpleName();
122                if (CLIENT_ABORT_EXCEPTION.equals(name) || EOF_EXCEPTION.equals(name)) {
123                    return true;
124                }
125            }
126            loops--;
127            t = t.getCause();
128        }
129        return false;
130    }
131
132    public static void logClientAbort(Exception e) {
133        log.debug("Client disconnected: " + unwrapException(e).getMessage());
134    }
135
136    private static Throwable unwrapException(Throwable t) {
137        while (t.getCause() != null) {
138            t = t.getCause();
139        }
140        return t;
141    }
142
143    /**
144     * Re-throws the passed exception except if it corresponds to a client disconnect, for which logging doesn't bring
145     * us anything.
146     *
147     * @param e the original exception
148     * @throws IOException if this is not a client disconnect
149     */
150    public static void handleClientDisconnect(IOException e) throws IOException {
151        if (isClientAbortError(e)) {
152            logClientAbort(e);
153        } else {
154            // unexpected problem, let traditional error management handle it
155            throw e;
156        }
157    }
158
159}