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