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