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}