com.kurento.kmf.repository.internal.http.RepositoryHttpServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.kurento.kmf.repository.internal.http.RepositoryHttpServlet.java

Source

/*
 * (C) Copyright 2013 Kurento (http://kurento.org/)
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public License
 * (LGPL) version 2.1 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl-2.1.html
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 */

package com.kurento.kmf.repository.internal.http;

import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_PARTIAL_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
import static javax.servlet.http.HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.StringTokenizer;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import com.kurento.kmf.common.exception.KurentoException;
import com.kurento.kmf.repository.RepositoryApiConfiguration;
import com.kurento.kmf.repository.RepositoryItem;
import com.kurento.kmf.repository.RepositoryItemAttributes;
import com.kurento.kmf.repository.internal.RepositoryHttpEndpointImpl;
import com.kurento.kmf.spring.KurentoApplicationContextUtils;

@WebServlet(value = "/repository_servlet/*", loadOnStartup = 1)
public class RepositoryHttpServlet extends HttpServlet {

    protected static class Range {

        public long start;
        public long end;
        public long length;

        /**
         * Validate range.
         * 
         */
        public boolean validate() {
            if (length != -1 && end >= length) {
                end = length - 1;
            }
            return (start >= 0) && (end >= 0) && (start <= end) && (length == -1 || length > 0);
        }
    }

    private static Logger log = LoggerFactory.getLogger(RepositoryHttpServlet.class);

    private static final long serialVersionUID = 1L;

    /**
     * Full range constant.
     */
    protected static final List<Range> FULL = new ArrayList<>();

    /**
     * MIME multipart separation string
     */
    protected static final String MIME_SEPARATION = "KURENTO_MIME_BOUNDARY";

    /**
     * Size of file transfer buffer in bytes.
     */
    protected static final int FILE_BUFFER_SIZE = 4096;

    /**
     * The input buffer size to use when serving resources.
     */
    private static final int INPUT_BUFFER_SIZE = 2048;

    /**
     * The output buffer size to use when serving resources.
     */
    private static final int OUTPUT_BUFFER_SIZE = 2048;

    /**
     * The debugging detail level for this servlet.
     */
    protected int debug;

    /**
     * RepoItemHttpElems
     */
    @Autowired
    protected transient RepositoryHttpManager repoHttpManager;

    @Autowired
    private RepositoryApiConfiguration config;

    /**
     * Finalize this servlet.
     */
    @Override
    public void destroy() {
        // NOOP
    }

    /**
     * Initialize this servlet.
     */
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

        super.init(servletConfig);

        configureKurentoAppContext(servletConfig);
        configureServletMapping(servletConfig);
        configureWebappPublicURL(servletConfig);

        if (servletConfig.getInitParameter("debug") != null) {
            debug = Integer.parseInt(getServletConfig().getInitParameter("debug"));
        }
    }

    private String configureWebappPublicURL(ServletConfig servletConfig) {
        String webappURL = config.getWebappPublicURL();
        if (webappURL == null || webappURL.trim().isEmpty()) {
            webappURL = servletConfig.getServletContext().getContextPath();
        } else {
            if (webappURL.endsWith("/")) {
                webappURL = webappURL.substring(0, webappURL.length() - 1);
            }
        }

        repoHttpManager.setWebappPublicURL(webappURL);
        return webappURL;
    }

    private String configureServletMapping(ServletConfig servletConfig) {
        Collection<String> mappings = servletConfig.getServletContext()
                .getServletRegistration(servletConfig.getServletName()).getMappings();

        if (mappings.isEmpty()) {
            throw new KurentoException("There is no mapping for servlet " + RepositoryHttpServlet.class.getName());
        }

        String mapping = mappings.iterator().next();

        // TODO: Document this. We assume a mapping starting with / and ending
        // with /*
        mapping = mapping.substring(0, mapping.length() - 1);

        repoHttpManager.setServletPath(mapping);

        return mapping;
    }

    private void configureKurentoAppContext(ServletConfig servletConfig) {

        if (KurentoApplicationContextUtils.getKurentoApplicationContext() == null) {
            KurentoApplicationContextUtils.createKurentoApplicationContext(servletConfig.getServletContext());
        }

        KurentoApplicationContextUtils.processInjectionBasedOnKurentoApplicationContext(this);
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        logRequest(req);
        super.service(req, resp);
        logResponse(resp);
    }

    /**
     * Override default implementation to ensure that TRACE is correctly
     * handled.
     * 
     * @param req
     *            the {@link HttpServletRequest} object that contains the
     *            request the client made of the servlet
     * 
     * @param resp
     *            the {@link HttpServletResponse} object that contains the
     *            response the servlet returns to the client
     * 
     * @exception IOException
     *                if an input or output error occurs while the servlet is
     *                handling the OPTIONS request
     * 
     * @exception ServletException
     *                if the request for the OPTIONS cannot be handled
     */
    @Override
    protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        resp.setHeader("Allow", "GET, HEAD, POST, PUT, OPTIONS");
    }

    /**
     * Process a HEAD request for the specified resource.
     * 
     * @param request
     *            The servlet request we are processing
     * @param response
     *            The servlet response we are creating
     * 
     * @exception IOException
     *                if an input/output error occurs
     * @exception ServletException
     *                if a servlet-specified error occurs
     */
    @Override
    protected void doHead(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {

        // Serve the requested resource, without the data content
        serveResource(request, response, false);

    }

    /**
     * Process a POST request for the specified resource.
     * 
     * @param request
     *            The servlet request we are processing
     * @param response
     *            The servlet response we are creating
     * 
     * @exception IOException
     *                if an input/output error occurs
     * @exception ServletException
     *                if a servlet-specified error occurs
     */
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        doPut(request, response);
    }

    /**
     * Process a PUT request for the specified resource.
     * 
     * @param req
     *            The servlet request we are processing
     * @param resp
     *            The servlet response we are creating
     * 
     * @exception IOException
     *                if an input/output error occurs
     * @exception ServletException
     *                if a servlet-specified error occurs
     */
    @Override
    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        uploadContent(req, resp);
    }

    /**
     * Process a GET request for the specified resource.
     * 
     * @param request
     *            The servlet request we are processing
     * @param response
     *            The servlet response we are creating
     * 
     * @exception IOException
     *                if an input/output error occurs
     * @exception ServletException
     *                if a servlet-specified error occurs
     */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        serveResource(request, response, true);
    }

    protected void uploadContent(HttpServletRequest req, HttpServletResponse resp) throws IOException {

        String sessionId = extractSessionId(req);

        RepositoryHttpEndpointImpl elem = repoHttpManager.getHttpRepoItemElem(sessionId);

        if (elem == null) {
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        elem.stopCurrentTimer();
        elem.fireStartedEventIfFirstTime();

        try (InputStream requestInputStream = req.getInputStream()) {

            try (OutputStream repoItemOutputStream = elem.getRepoItemOutputStream()) {

                Range range = parseContentRange(req, resp);

                if (range != null) {

                    if (range.start > elem.getWrittenBytes()) {
                        resp.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
                        resp.getOutputStream().println(
                                "The server doesn't support writing ranges " + "ahead of previously written bytes");
                    } else if (range.end == elem.getWrittenBytes()) {

                        // TODO We assume that the put range is the same than
                        // the
                        // previous one. Do we need to check this?

                        resp.setStatus(SC_OK);
                        resp.getOutputStream().println("The server has detected that the submited range "
                                + "has already submited in a previous request");
                    } else if (range.start < elem.getWrittenBytes() && range.end > elem.getWrittenBytes()) {

                        Range copyRange = new Range();
                        copyRange.start = elem.getWrittenBytes() - range.start;
                        copyRange.end = range.end - range.start;

                        copyStreamsRange(requestInputStream, repoItemOutputStream, copyRange);

                        resp.setStatus(SC_OK);

                    } else if (range.start == elem.getWrittenBytes()) {

                        IOUtils.copy(requestInputStream, repoItemOutputStream);

                        resp.setStatus(SC_OK);

                    }

                } else {

                    boolean isMultipart = ServletFileUpload.isMultipartContent(req);

                    if (isMultipart) {

                        uploadMultipart(req, resp, repoItemOutputStream);

                    } else {

                        try {

                            log.info("Start to receive bytes (estimated " + req.getContentLength() + " bytes)");
                            int bytes = IOUtils.copy(requestInputStream, repoItemOutputStream);
                            resp.setStatus(SC_OK);
                            log.info("Bytes received: " + bytes);

                        } catch (Exception e) {

                            log.warn("Exception when uploading content", e);

                            elem.fireSessionErrorEvent(e);
                            resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                        }
                    }
                }
            }

        } finally {
            elem.stopInTimeout();
        }
    }

    private void uploadMultipart(HttpServletRequest req, HttpServletResponse resp, OutputStream repoItemOutputStrem)
            throws IOException {

        log.info("Multipart detected");

        ServletFileUpload upload = new ServletFileUpload();

        try {

            // Parse the request
            FileItemIterator iter = upload.getItemIterator(req);
            while (iter.hasNext()) {
                FileItemStream item = iter.next();
                String name = item.getFieldName();
                try (InputStream stream = item.openStream()) {
                    if (item.isFormField()) {
                        // TODO What to do with this?
                        log.info("Form field {} with value {} detected.", name, Streams.asString(stream));
                    } else {

                        // TODO Must we support multiple files uploading?
                        log.info("File field {} with file name detected.", name, item.getName());

                        log.info("Start to receive bytes (estimated bytes)",
                                Integer.toString(req.getContentLength()));
                        int bytes = IOUtils.copy(stream, repoItemOutputStrem);
                        resp.setStatus(SC_OK);
                        log.info("Bytes received: {}", Integer.toString(bytes));
                    }
                }
            }

        } catch (FileUploadException e) {
            throw new IOException(e);
        }
    }

    private void logRequest(HttpServletRequest req) {
        log.info("Request received " + req.getRequestURL());
        log.info("  Method: " + req.getMethod());

        Enumeration<String> headerNames = req.getHeaderNames();
        while (headerNames.hasMoreElements()) {

            String headerName = headerNames.nextElement();
            Enumeration<String> values = req.getHeaders(headerName);
            List<String> valueList = new ArrayList<>();
            while (values.hasMoreElements()) {
                valueList.add(values.nextElement());
            }
            log.info("  Header {}: {}", headerName, valueList);
        }
    }

    private void logResponse(HttpServletResponse resp) {

        Collection<String> headerNames = resp.getHeaderNames();
        for (String headerName : headerNames) {
            Collection<String> values = resp.getHeaders(headerName);
            log.info("  Header {}: {}", headerName, values);
        }
    }

    /**
     * Return the sessionId from the request.
     * 
     * @param request
     *            The servlet request we are processing
     */
    protected String extractSessionId(HttpServletRequest request) {

        // Path info without leading "/"
        String pathInfo = request.getPathInfo();

        if (pathInfo != null && pathInfo.length() >= 1) {
            return pathInfo.substring(1);
        }

        return null;

    }

    /**
     * Handle a partial PUT. New content specified in request is appended to
     * existing content in oldRevisionContent (if present). This code does not
     * support simultaneous partial updates to the same resource.
     */
    protected File executePartialPut(HttpServletRequest req, Range range, String sessionId) throws IOException {

        // TODO: Change this implementation to avoid Files. Try to
        // make the work on the repository implementation.

        // Append data specified in ranges to existing content for this
        // resource - create a temp. file on the local filesystem to
        // perform this operation
        // Assume just one range is specified for now

        File tempDir = (File) getServletContext().getAttribute(ServletContext.TEMPDIR);

        // Convert all '/' characters to '.' in resourcePath
        String convertedResourcePath = sessionId.replace('/', '.');

        File contentFile = new File(tempDir, convertedResourcePath);

        if (contentFile.createNewFile()) {
            // Clean up contentFile when Tomcat is terminated
            contentFile.deleteOnExit();
        }

        try (RandomAccessFile randAccessContentFile = new RandomAccessFile(contentFile, "rw")) {

            RepositoryHttpEndpointImpl repoItemHttpElem = repoHttpManager.getHttpRepoItemElem(sessionId);

            // Copy data in oldRevisionContent to contentFile
            if (repoItemHttpElem != null) {

                try (BufferedInputStream bufOldRevStream = new BufferedInputStream(
                        repoItemHttpElem.createRepoItemInputStream(), FILE_BUFFER_SIZE)) {

                    int numBytesRead;
                    byte[] copyBuffer = new byte[FILE_BUFFER_SIZE];
                    while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1) {
                        randAccessContentFile.write(copyBuffer, 0, numBytesRead);
                    }
                }
            }

            randAccessContentFile.setLength(range.length);

            // Append data in request input stream to contentFile
            randAccessContentFile.seek(range.start);

            int numBytesRead;

            byte[] transferBuffer = new byte[FILE_BUFFER_SIZE];

            try (BufferedInputStream requestBufInStream = new BufferedInputStream(req.getInputStream(),
                    FILE_BUFFER_SIZE)) {

                while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1) {
                    randAccessContentFile.write(transferBuffer, 0, numBytesRead);
                }
            }
        }

        return contentFile;
    }

    /**
     * Check if the conditions specified in the optional If headers are
     * satisfied.
     * 
     * @param request
     *            The servlet request we are processing
     * @param response
     *            The servlet response we are creating
     * @param resourceAttributes
     *            The resource information
     * @return boolean true if the resource meets all the specified conditions,
     *         and false if any of the conditions is not satisfied, in which
     *         case request processing is stopped
     */
    protected boolean checkIfHeaders(HttpServletRequest request, HttpServletResponse response,
            RepositoryItemAttributes resourceAttributes) throws IOException {

        // TODO Investigate how to load properties for RepositoryItem (Mongo or
        // Filesystem)

        return checkIfMatch(request, response, resourceAttributes)
                && checkIfModifiedSince(request, response, resourceAttributes)
                && checkIfNoneMatch(request, response, resourceAttributes)
                && checkIfUnmodifiedSince(request, response, resourceAttributes);
    }

    /**
     * Serve the specified resource, optionally including the data content.
     * 
     * @param request
     *            The servlet request we are processing
     * @param response
     *            The servlet response we are creating
     * @param content
     *            Should the content be included?
     * 
     * @exception IOException
     *                if an input/output error occurs
     * @exception ServletException
     *                if a servlet-specified error occurs
     */
    protected void serveResource(HttpServletRequest request, HttpServletResponse response, boolean content)
            throws IOException, ServletException {

        boolean serveContent = content;

        // Identify the requested resource path
        String sessionId = extractSessionId(request);

        RepositoryHttpEndpointImpl elem = repoHttpManager.getHttpRepoItemElem(sessionId);

        if (elem == null) {

            if (debug > 0) {
                log("Resource with sessionId '" + sessionId + "' not found");
            }

            response.sendError(SC_NOT_FOUND, request.getRequestURI());
            return;
        }

        elem.fireStartedEventIfFirstTime();

        RepositoryItem repositoryItem = elem.getRepositoryItem();
        RepositoryItemAttributes attributes = repositoryItem.getAttributes();

        if (debug > 0) {
            if (serveContent) {
                log("Serving resource with sessionId '" + sessionId
                        + "' headers and data. This resource corresponds to repository item '"
                        + repositoryItem.getId() + "'");
            } else {
                log("Serving resource with sessionId '" + sessionId
                        + "' headers only. This resource corresponds to repository item '" + repositoryItem.getId()
                        + "'");
            }
        }

        boolean malformedRequest = response.getStatus() >= SC_BAD_REQUEST;

        if (!malformedRequest && !checkIfHeaders(request, response, attributes)) {
            return;
        }

        String contentType = getContentType(elem, attributes);

        List<Range> ranges = null;

        if (!malformedRequest) {

            response.setHeader("Accept-Ranges", "bytes");
            response.setHeader("ETag", attributes.getETag());
            response.setHeader("Last-Modified", attributes.getLastModifiedHttp());

            ranges = parseRange(request, response, attributes);
        }

        long contentLength = attributes.getContentLength();

        // Special case for zero length files, which would cause a
        // (silent) ISE when setting the output buffer size
        if (contentLength == 0L) {
            serveContent = false;
        }

        // Check to see if a Filter, Valve of wrapper has written some content.
        // If it has, disable range requests and setting of a content length
        // since neither can be done reliably.
        boolean contentWritten = response.isCommitted();

        if (contentWritten) {
            ranges = FULL;
        }

        boolean noRanges = (ranges == null || ranges.isEmpty());

        if (malformedRequest || (noRanges && request.getHeader("Range") == null) || ranges == FULL) {

            setContentType(response, contentType);

            if (contentLength >= 0) {
                // Don't set a content length if something else has already
                // written to the response.
                if (!contentWritten) {
                    setContentLength(response, contentLength);
                }
            }

            // Copy the input stream to our output stream (if requested)
            if (serveContent) {
                copy(elem, response);
            }

        } else {

            if (noRanges) {
                return;
            }

            // Partial content response.
            response.setStatus(SC_PARTIAL_CONTENT);

            if (ranges.size() == 1) {

                Range range = ranges.get(0);

                response.addHeader("Content-Range", "bytes " + range.start + "-" + range.end + "/" + range.length);

                long length = range.end - range.start + 1;

                setContentLength(response, length);
                setContentType(response, contentType);

                if (serveContent) {
                    copy(elem, response, range);
                }

            } else {

                response.setContentType("multipart/byteranges; boundary=" + MIME_SEPARATION);

                if (serveContent) {
                    copy(elem, response, ranges, contentType);
                }
            }
        }

        elem.stopInTimeout();

    }

    private String getContentType(RepositoryHttpEndpointImpl repoItemHttpElem,
            RepositoryItemAttributes attributes) {

        String contentType = attributes.getMimeType();
        if (contentType == null) {
            contentType = getServletContext().getMimeType(repoItemHttpElem.getRepositoryItem().getId());
            attributes.setMimeType(contentType);
        }
        return contentType;
    }

    private void setContentType(HttpServletResponse response, String contentType) {
        if (contentType != null) {
            if (debug > 0) {
                log("contentType='" + contentType + "'");
            }
            response.setContentType(contentType);
        }
    }

    private void setContentLength(HttpServletResponse response, long length) {
        if (debug > 0) {
            log("contentLength=" + length);
        }

        if (length < Integer.MAX_VALUE) {
            response.setContentLength((int) length);
        } else {
            // Set the content-length as String to be able to use a long
            response.setHeader("content-length", "" + length);
        }
    }

    /**
     * Parse the content-range header.
     * 
     * @param request
     *            The servlet request we are processing
     * @param response
     *            The servlet response we are creating
     * @return Range
     */
    protected Range parseContentRange(HttpServletRequest request, HttpServletResponse response) throws IOException {

        // Retrieving the content-range header (if any is specified
        String rangeHeader = request.getHeader("Content-Range");

        if (rangeHeader == null) {
            return null;
        }

        // bytes is the only range unit supported
        if (!rangeHeader.startsWith("bytes")) {
            response.sendError(SC_BAD_REQUEST);
            return null;
        }

        rangeHeader = rangeHeader.substring(6).trim();

        int dashPos = rangeHeader.indexOf('-');
        int slashPos = rangeHeader.indexOf('/');

        if (dashPos == -1) {
            response.sendError(SC_BAD_REQUEST);
            return null;
        }

        if (slashPos == -1) {
            response.sendError(SC_BAD_REQUEST);
            return null;
        }

        Range range = new Range();

        try {
            range.start = Long.parseLong(rangeHeader.substring(0, dashPos));
            range.end = Long.parseLong(rangeHeader.substring(dashPos + 1, slashPos));

            String lengthString = rangeHeader.substring(slashPos + 1, rangeHeader.length());

            if (lengthString.equals("*")) {
                range.length = -1;
            } else {
                range.length = Long.parseLong(lengthString);
            }

        } catch (NumberFormatException e) {
            response.sendError(SC_BAD_REQUEST);
            return null;
        }

        if (!range.validate()) {
            response.sendError(SC_BAD_REQUEST);
            return null;
        }

        return range;
    }

    /**
     * Parse the range header.
     * 
     * @param request
     *            The servlet request we are processing
     * @param response
     *            The servlet response we are creating
     * @return Vector of ranges
     */
    protected List<Range> parseRange(HttpServletRequest request, HttpServletResponse response,
            RepositoryItemAttributes resourceAttributes) throws IOException {

        // Checking If-Range
        String headerValue = request.getHeader("If-Range");

        if (headerValue != null) {

            long headerValueTime = -1L;
            try {
                headerValueTime = request.getDateHeader("If-Range");
            } catch (IllegalArgumentException e) {
                // Ignore
            }

            String eTag = resourceAttributes.getETag();
            long lastModified = resourceAttributes.getLastModified();

            if (headerValueTime == -1L) {

                // If the ETag the client gave does not match the entity
                // etag, then the entire entity is returned.
                if (!eTag.equals(headerValue.trim())) {
                    return FULL;
                }

            } else {

                // If the timestamp of the entity the client got is older than
                // the last modification date of the entity, the entire entity
                // is returned.
                if (lastModified > (headerValueTime + 1000)) {
                    return FULL;
                }

            }
        }

        long fileLength = resourceAttributes.getContentLength();

        if (fileLength == 0) {
            return null;
        }

        // Retrieving the range header (if any is specified
        String rangeHeader = request.getHeader("Range");

        if (rangeHeader == null) {
            return null;
        }
        // bytes is the only range unit supported (and I don't see the point
        // of adding new ones).
        if (!rangeHeader.startsWith("bytes")) {
            response.addHeader("Content-Range", "bytes */" + fileLength);
            response.sendError(SC_REQUESTED_RANGE_NOT_SATISFIABLE);
            return null;
        }

        rangeHeader = rangeHeader.substring(6);

        // Vector which will contain all the ranges which are successfully
        // parsed.
        List<Range> result = new ArrayList<>();
        StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ",");

        // Parsing the range list
        while (commaTokenizer.hasMoreTokens()) {

            String rangeDefinition = commaTokenizer.nextToken().trim();

            Range currentRange = new Range();
            currentRange.length = fileLength;

            int dashPos = rangeDefinition.indexOf('-');

            if (dashPos == -1) {
                response.addHeader("Content-Range", "bytes */" + fileLength);
                response.sendError(SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                return null;
            }

            if (dashPos == 0) {

                try {
                    long offset = Long.parseLong(rangeDefinition);
                    currentRange.start = fileLength + offset;
                    currentRange.end = fileLength - 1;
                } catch (NumberFormatException e) {
                    response.addHeader("Content-Range", "bytes */" + fileLength);
                    response.sendError(SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                    return null;
                }

            } else {

                try {
                    currentRange.start = Long.parseLong(rangeDefinition.substring(0, dashPos));
                    if (dashPos < rangeDefinition.length() - 1) {
                        currentRange.end = Long
                                .parseLong(rangeDefinition.substring(dashPos + 1, rangeDefinition.length()));
                    } else {
                        currentRange.end = fileLength - 1;
                    }
                } catch (NumberFormatException e) {
                    response.addHeader("Content-Range", "bytes */" + fileLength);
                    response.sendError(SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                    return null;
                }

            }

            if (!currentRange.validate()) {
                response.addHeader("Content-Range", "bytes */" + fileLength);
                response.sendError(SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                return null;
            }

            result.add(currentRange);
        }

        return result;
    }

    /**
     * Check if the if-match condition is satisfied.
     * 
     * @param request
     *            The servlet request we are processing
     * @param response
     *            The servlet response we are creating
     * @param resourceAttributes
     *            File object
     * @return boolean true if the resource meets the specified condition, and
     *         false if the condition is not satisfied, in which case request
     *         processing is stopped
     */
    protected boolean checkIfMatch(HttpServletRequest request, HttpServletResponse response,
            RepositoryItemAttributes resourceAttributes) throws IOException {

        String eTag = resourceAttributes.getETag();
        String headerValue = request.getHeader("If-Match");

        if (headerValue != null) {
            if (headerValue.indexOf('*') == -1) {

                StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");
                boolean conditionSatisfied = false;

                while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
                    String currentToken = commaTokenizer.nextToken();
                    if (currentToken.trim().equals(eTag)) {
                        conditionSatisfied = true;
                    }
                }

                // If none of the given ETags match, 412 Precodition failed is
                // sent back
                if (!conditionSatisfied) {
                    response.sendError(SC_PRECONDITION_FAILED);
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Check if the if-modified-since condition is satisfied.
     * 
     * @param request
     *            The servlet request we are processing
     * @param response
     *            The servlet response we are creating
     * @param resourceAttributes
     *            File object
     * @return boolean true if the resource meets the specified condition, and
     *         false if the condition is not satisfied, in which case request
     *         processing is stopped
     */
    protected boolean checkIfModifiedSince(HttpServletRequest request, HttpServletResponse response,
            RepositoryItemAttributes resourceAttributes) {

        try {
            long headerValue = request.getDateHeader("If-Modified-Since");
            long lastModified = resourceAttributes.getLastModified();
            if (headerValue != -1) {

                // If an If-None-Match header has been specified, if modified
                // since
                // is ignored.
                if ((request.getHeader("If-None-Match") == null) && (lastModified < headerValue + 1000)) {
                    // The entity has not been modified since the date
                    // specified by the client. This is not an error case.
                    response.setStatus(SC_NOT_MODIFIED);
                    response.setHeader("ETag", resourceAttributes.getETag());

                    return false;
                }
            }
        } catch (IllegalArgumentException illegalArgument) {
            return true;
        }

        return true;
    }

    /**
     * Check if the if-none-match condition is satisfied.
     * 
     * @param request
     *            The servlet request we are processing
     * @param response
     *            The servlet response we are creating
     * @param resourceAttributes
     *            File object
     * @return boolean true if the resource meets the specified condition, and
     *         false if the condition is not satisfied, in which case request
     *         processing is stopped
     */
    protected boolean checkIfNoneMatch(HttpServletRequest request, HttpServletResponse response,
            RepositoryItemAttributes resourceAttributes) throws IOException {

        String eTag = resourceAttributes.getETag();
        String headerValue = request.getHeader("If-None-Match");

        if (headerValue != null) {

            boolean conditionSatisfied = false;

            if (!headerValue.equals("*")) {

                StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");

                while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
                    String currentToken = commaTokenizer.nextToken();
                    if (currentToken.trim().equals(eTag)) {
                        conditionSatisfied = true;
                    }
                }

            } else {
                conditionSatisfied = true;
            }

            if (conditionSatisfied) {

                // For GET and HEAD, we should respond with
                // 304 Not Modified.
                // For every other method, 412 Precondition Failed is sent
                // back.
                if (("GET".equals(request.getMethod())) || ("HEAD".equals(request.getMethod()))) {
                    response.setStatus(SC_NOT_MODIFIED);
                    response.setHeader("ETag", eTag);

                    return false;
                }
                response.sendError(SC_PRECONDITION_FAILED);
                return false;
            }
        }

        return true;
    }

    /**
     * Check if the if-unmodified-since condition is satisfied.
     * 
     * @param request
     *            The servlet request we are processing
     * @param response
     *            The servlet response we are creating
     * @param resourceAttributes
     *            File object
     * @return boolean true if the resource meets the specified condition, and
     *         false if the condition is not satisfied, in which case request
     *         processing is stopped
     */
    protected boolean checkIfUnmodifiedSince(HttpServletRequest request, HttpServletResponse response,
            RepositoryItemAttributes resourceAttributes) throws IOException {

        try {
            long lastModified = resourceAttributes.getLastModified();
            long headerValue = request.getDateHeader("If-Unmodified-Since");
            if (headerValue != -1) {
                if (lastModified >= (headerValue + 1000)) {
                    // The entity has not been modified since the date
                    // specified by the client. This is not an error case.
                    response.sendError(SC_PRECONDITION_FAILED);
                    return false;
                }
            }
        } catch (IllegalArgumentException illegalArgument) {
            return true;
        }

        return true;
    }

    /**
     * Copy the contents of the specified input stream to the specified output
     * stream, and ensure that both streams are closed before returning (even in
     * the face of an exception).
     * 
     * @param repoItemHttpElem
     *            The cache entry for the source resource
     * @param response
     *            The HttpResponse where the resource will be copied
     * 
     * @exception IOException
     *                if an input/output error occurs
     */
    protected void copy(RepositoryHttpEndpointImpl repoItemHttpElem, HttpServletResponse response)
            throws IOException {
        copy(repoItemHttpElem, response, null);
    }

    /**
     * Copy the contents of the specified input stream to the specified output
     * stream, and ensure that both streams are closed before returning (even in
     * the face of an exception).
     * 
     * @param repoItemHttpElem
     *            The cache entry for the source resource
     * @param response
     *            The response we are writing to
     * @param range
     *            Range asked by the client
     * @exception IOException
     *                if an input/output error occurs
     */
    protected void copy(RepositoryHttpEndpointImpl repoItemHttpElem, HttpServletResponse response, Range range)
            throws IOException {

        try {
            response.setBufferSize(OUTPUT_BUFFER_SIZE);
        } catch (IllegalStateException e) {
            // Silent catch
        }

        IOException exception;

        try (ServletOutputStream ostream = response.getOutputStream()) {
            try (InputStream istream = new BufferedInputStream(repoItemHttpElem.createRepoItemInputStream(),
                    INPUT_BUFFER_SIZE)) {

                if (range != null) {
                    exception = copyStreamsRange(istream, ostream, range);
                } else {
                    exception = copyStreams(istream, ostream);
                }

            }
        }

        // Rethrow any exception that has occurred
        if (exception != null) {
            throw exception;
        }
    }

    /**
     * Copy the contents of the specified input stream to the specified output
     * stream, and ensure that both streams are closed before returning (even in
     * the face of an exception).
     * 
     * @param repoItemHttpElem
     *            The cache entry for the source resource
     * @param response
     *            The response we are writing to
     * @param ranges
     *            Enumeration of the ranges the client wanted to retrieve
     * @param contentType
     *            Content type of the resource
     * @exception IOException
     *                if an input/output error occurs
     */
    protected void copy(RepositoryHttpEndpointImpl repoItemHttpElem, HttpServletResponse response,
            List<Range> ranges, String contentType) throws IOException {

        try {
            response.setBufferSize(OUTPUT_BUFFER_SIZE);
        } catch (IllegalStateException e) {
            // Silent catch
        }

        IOException exception = null;
        try (ServletOutputStream ostream = response.getOutputStream()) {

            for (Range currentRange : ranges) {

                try (InputStream istream = new BufferedInputStream(repoItemHttpElem.createRepoItemInputStream(),
                        INPUT_BUFFER_SIZE)) {

                    // Writing MIME header.
                    ostream.println();
                    ostream.println("--" + MIME_SEPARATION);

                    if (contentType != null) {
                        ostream.println("Content-Type: " + contentType);
                    }

                    ostream.println("Content-Range: bytes " + currentRange.start + "-" + currentRange.end + "/"
                            + currentRange.length);
                    ostream.println();

                    exception = copyStreamsRange(istream, ostream, currentRange);

                    if (exception != null) {
                        break;
                    }
                }
            }

            ostream.println();
            ostream.print("--" + MIME_SEPARATION + "--");
        }
        // Rethrow any exception that has occurred
        if (exception != null) {
            throw exception;
        }

    }

    /**
     * Copy the contents of the specified input stream to the specified output
     * stream, and ensure that both streams are closed before returning (even in
     * the face of an exception).
     * 
     * @param istream
     *            The input stream to read from
     * @param ostream
     *            The output stream to write to
     * @return Exception which occurred during processing
     */
    protected IOException copyStreams(InputStream istream, OutputStream ostream) {

        // Copy the input stream to the output stream
        IOException exception = null;

        byte buffer[] = new byte[INPUT_BUFFER_SIZE];

        int len = buffer.length;
        while (true) {
            try {
                len = istream.read(buffer);
                if (len == -1) {
                    break;
                }
                ostream.write(buffer, 0, len);

                log.debug("{} bytes have been written to item" + len);

            } catch (IOException e) {
                exception = e;
                len = -1;
                break;
            }
        }

        return exception;
    }

    /**
     * Copy the contents of the specified input stream to the specified output
     * stream, and ensure that both streams are closed before returning (even in
     * the face of an exception).
     * 
     * @param istream
     *            The input stream to read from
     * @param ostream
     *            The output stream to write to
     * @param range
     *            Range we are copying
     *
     * @return Exception which occurred during processing
     */
    protected IOException copyStreamsRange(InputStream istream, OutputStream ostream, Range range) {

        long start = range.start;
        long end = range.end;

        if (debug > 10) {
            log("Serving bytes:" + start + "-" + end);
        }

        long skipped = 0;
        try {
            skipped = istream.skip(start);
        } catch (IOException e) {
            return e;
        }
        if (skipped < start) {
            return new IOException("Has been skiped " + skipped + " when " + start + " is required");
        }

        IOException exception = null;
        long remBytes = end - start + 1;

        byte buffer[] = new byte[INPUT_BUFFER_SIZE];
        int readBytes = buffer.length;
        while (remBytes > 0) {
            try {
                readBytes = istream.read(buffer);
                if (readBytes == -1) {
                    break;
                } else if (readBytes <= remBytes) {
                    ostream.write(buffer, 0, readBytes);
                    remBytes -= readBytes;
                } else {
                    ostream.write(buffer, 0, (int) remBytes);
                    break;
                }
            } catch (IOException e) {
                exception = e;
                break;
            }
        }

        return exception;
    }

}