/*
* (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.core.io.download;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.utils.RFC2231;
import org.nuxeo.ecm.core.io.download.DownloadService.ByteRange;
/**
* Helper class related to the download service.
*
* @since 7.3
*/
public class DownloadHelper {
private static final Log log = LogFactory.getLog(DownloadHelper.class);
public static final String INLINE = "inline";
// tomcat catalina
private static final String CLIENT_ABORT_EXCEPTION = "ClientAbortException";
// jetty (with CamelCase "Eof")
private static final String EOF_EXCEPTION = "EofException";
// utility class
private DownloadHelper() {
}
/**
* Parses a byte range.
*
* @param range the byte range as a string
* @param length the file length
* @return the byte range, or {@code null} if it couldn't be parsed.
*/
public static ByteRange parseRange(String range, long length) {
try {
// TODO does no support multiple ranges
if (!range.startsWith("bytes=") || range.indexOf(',') >= 0) {
return null;
}
int i = range.indexOf('-', 6);
if (i < 0) {
return null;
}
String start = range.substring(6, i).trim();
String end = range.substring(i + 1).trim();
long rangeStart = 0;
long rangeEnd = length - 1;
if (start.isEmpty()) {
if (end.isEmpty()) {
return null;
}
rangeStart = length - Integer.parseInt(end);
if (rangeStart < 0) {
rangeStart = 0;
}
} else {
rangeStart = Integer.parseInt(start);
if (!end.isEmpty()) {
rangeEnd = Integer.parseInt(end);
}
}
if (rangeStart > rangeEnd) {
return null;
}
return new ByteRange(rangeStart, rangeEnd);
} catch (NumberFormatException e) {
return null;
}
}
/**
* Generates a {@code Content-Disposition} string based on the servlet request for a given filename.
* <p>
* The value follows RFC2231.
*
* @param request the http servlet request
* @param filename the filename
* @return a full string to set as value of a {@code Content-Disposition} header
*/
public static String getRFC2231ContentDisposition(HttpServletRequest request, String filename) {
return getRFC2231ContentDisposition(request, filename, null);
}
/**
* Generates a {@code Content-Disposition} string for a given filename.
* <p>
* The value follows RFC2231.
*
* @param request the http servlet request
* @param filename the filename
* @param inline how to set the content disposition; {@code TRUE} for {@code inline}, {@code FALSE} for
* {@code attachment}, or {@code null} to detect from {@code inline} request parameter or attribute
* @return a full string to set as value of a {@code Content-Disposition} header
* @since 7.10
*/
public static String getRFC2231ContentDisposition(HttpServletRequest request, String filename, Boolean inline) {
String userAgent = request.getHeader("User-Agent");
boolean binline;
if (inline == null) {
String inlineParam = request.getParameter(INLINE);
if (inlineParam == null) {
inlineParam = (String) request.getAttribute(INLINE);
}
binline = inlineParam != null && !"false".equals(inlineParam);
} else {
binline = inline.booleanValue();
}
return RFC2231.encodeContentDisposition(filename, binline, userAgent);
}
public static boolean isClientAbortError(Throwable t) {
int loops = 20; // no infinite loop
while (t != null && loops > 0) {
if (t instanceof IOException) {
// handle all IOException that are ClientAbortException by looking
// at their class name since the package name is not the same for
// jboss, glassfish, tomcat and jetty and we don't want to add
// implementation specific build dependencies to this project
String name = t.getClass().getSimpleName();
if (CLIENT_ABORT_EXCEPTION.equals(name) || EOF_EXCEPTION.equals(name)) {
return true;
}
}
loops--;
t = t.getCause();
}
return false;
}
public static void logClientAbort(Exception e) {
log.debug("Client disconnected: " + unwrapException(e).getMessage());
}
private static Throwable unwrapException(Throwable t) {
while (t.getCause() != null) {
t = t.getCause();
}
return t;
}
/**
* Re-throws the passed exception except if it corresponds to a client disconnect, for which logging doesn't bring
* us anything.
*
* @param e the original exception
* @throws IOException if this is not a client disconnect
*/
public static void handleClientDisconnect(IOException e) throws IOException {
if (isClientAbortError(e)) {
logClientAbort(e);
} else {
// unexpected problem, let traditional error management handle it
throw e;
}
}
}