Package play.server

Source Code of play.server.FileService$ByteRangeInput$ByteRange

package play.server;

import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.http.*;
import org.jboss.netty.handler.stream.ChunkedFile;
import org.jboss.netty.handler.stream.ChunkedInput;
import play.Logger;
import play.exceptions.UnexpectedException;
import play.libs.MimeTypes;
import play.mvc.Http.Request;
import play.mvc.Http.Response;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

import static org.apache.commons.io.IOUtils.closeQuietly;
import static org.jboss.netty.buffer.ChannelBuffers.wrappedBuffer;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;

public class FileService  {

    public static void serve(File localFile, HttpRequest nettyRequest, HttpResponse nettyResponse, ChannelHandlerContext ctx, Request request, Response response, Channel channel) throws FileNotFoundException {
        final RandomAccessFile raf = new RandomAccessFile(localFile, "r");
        try {
            long fileLength = raf.length();
           
            boolean isKeepAlive = HttpHeaders.isKeepAlive(nettyRequest) && nettyRequest.getProtocolVersion().equals(HttpVersion.HTTP_1_1);
           
            if(Logger.isTraceEnabled()) {
                Logger.trace("keep alive %s", String.valueOf(isKeepAlive));
                Logger.trace("content type %s", (response.contentType != null ? response.contentType : MimeTypes.getContentType(localFile.getName(), "text/plain")));
            }
           
            if (!nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) {
                // Add 'Content-Length' header only for a keep-alive connection.
                if(Logger.isTraceEnabled())
                    Logger.trace("file length " + fileLength);
                nettyResponse.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(fileLength));
            }

            if (response.contentType != null) {
                nettyResponse.setHeader(CONTENT_TYPE, response.contentType);
            } else {
                nettyResponse.setHeader(CONTENT_TYPE, (MimeTypes.getContentType(localFile.getName(), "text/plain")));
            }

            nettyResponse.addHeader(HttpHeaders.Names.ACCEPT_RANGES, HttpHeaders.Values.BYTES);

            // Write the initial line and the header.
            ChannelFuture writeFuture;

            // Write the content.
            if (!nettyRequest.getMethod().equals(HttpMethod.HEAD)) {
                ChunkedInput chunkedInput = getChunckedInput(raf, MimeTypes.getContentType(localFile.getName(), "text/plain"), channel, nettyRequest, nettyResponse);
                channel.write(nettyResponse);
                writeFuture = channel.write(chunkedInput);
            } else {
                writeFuture = channel.write(nettyResponse);
                raf.close();
            }

            if (!isKeepAlive) {
                writeFuture.addListener(ChannelFutureListener.CLOSE);
            }
        } catch (Throwable exx) {
            exx.printStackTrace();
            closeQuietly(raf);
            try {
                ctx.getChannel().close();
            } catch (Throwable ex) { /* Left empty */ }
        }
    }
   
    public static ChunkedInput getChunckedInput(RandomAccessFile raf, String contentType, Channel channel, HttpRequest nettyRequest, HttpResponse nettyResponse) throws IOException {
        if(ByteRangeInput.accepts(nettyRequest)) {
            ByteRangeInput server = new ByteRangeInput(raf, contentType, nettyRequest);
            server.prepareNettyResponse(nettyResponse);
            return server;
        } else {
            return new ChunkedFile(raf);
        }
    }
   
    public static class ByteRangeInput implements ChunkedInput{
        RandomAccessFile raf;
        HttpRequest request;
        int chunkSize = 8096;
        ByteRange[] byteRanges;
        int currentByteRange = 0;
        String contentType;
       
        boolean unsatisfiable = false;
       
        long fileLength;
       
        public ByteRangeInput(File file, String contentType, HttpRequest request) throws FileNotFoundException, IOException {
            this(new RandomAccessFile(file, "r"), contentType, request);
        }
       
        public ByteRangeInput(RandomAccessFile raf, String contentType, HttpRequest request) throws FileNotFoundException, IOException {
            this.raf = raf;
            this.request = request;
            fileLength = raf.length();
            this.contentType = contentType;
            initRanges();
            if(Logger.isDebugEnabled())
                Logger.debug("Invoked ByteRangeServer, found byteRanges: %s (with header Range: %s)", Arrays.toString(byteRanges), request.getHeader("range"));
        }
       
        public void prepareNettyResponse(HttpResponse nettyResponse) {
            nettyResponse.addHeader("Accept-Ranges", "bytes");
            if(unsatisfiable) {
                nettyResponse.setStatus(HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
                nettyResponse.setHeader("Content-Range", "bytes " + 0 + "-" + (fileLength-1) + "/" + fileLength);
                nettyResponse.setHeader("Content-length", 0);
            } else {
                nettyResponse.setStatus(HttpResponseStatus.PARTIAL_CONTENT);
                if(byteRanges.length == 1) {
                    ByteRange range = byteRanges[0];
                    nettyResponse.setHeader("Content-Range", "bytes " + range.start + "-" + range.end + "/" + fileLength);
                } else {
                    nettyResponse.setHeader("Content-type", "multipart/byteranges; boundary="+DEFAULT_SEPARATOR);
                }
                long length = 0;
                for(ByteRange range: byteRanges) {
                    length += range.computeTotalLengh();
                }
                nettyResponse.setHeader("Content-length", length);
            }
        }
       
        @Override
        public Object nextChunk() throws Exception {
            if(Logger.isTraceEnabled())
                Logger.trace("FileService nextChunk");
            try {
                int count = 0;
                byte[] buffer = new byte[chunkSize];
                while(count < chunkSize && currentByteRange < byteRanges.length && byteRanges[currentByteRange] != null) {
                    if(byteRanges[currentByteRange].remaining() > 0) {
                        count += byteRanges[currentByteRange].fill(buffer, count);
                    } else {
                        currentByteRange++;
                    }
                }
                if(count == 0)
                    return null;
               
                return wrappedBuffer(buffer);
            } catch (Exception e) {
                Logger.error(e, "error sending file");
                throw e;
            }
        }
       
        @Override
        public boolean hasNextChunk() throws Exception {
            if(Logger.isTraceEnabled())
                Logger.trace("FileService hasNextChunk() : " + (currentByteRange < byteRanges.length && byteRanges[currentByteRange].remaining() > 0));
            return currentByteRange < byteRanges.length && byteRanges[currentByteRange].remaining() > 0;
        }
       
        @Override
        public boolean isEndOfInput() throws Exception {
            return !hasNextChunk();
        }
       
        @Override
        public void close() throws Exception {
            raf.close();
        }
       
        public static boolean accepts(HttpRequest request) {
            return request.containsHeader("range");
        }
       
        private void initRanges() {
            try {
                String headerValue = request.getHeader("range").trim().substring("bytes=".length());
                String[] rangesValues = headerValue.split(",");
                ArrayList<long[]> ranges = new ArrayList<long[]>(rangesValues.length);
                for(int i = 0; i < rangesValues.length; i++) {
                    String rangeValue = rangesValues[i];
                    long start, end;
                    if(rangeValue.startsWith("-")) {
                        end = fileLength - 1;
                        start = fileLength - 1 - Long.parseLong(rangeValue.substring("-".length()));
                    } else {
                        String[] range = rangeValue.split("-");
                        start = Long.parseLong(range[0]);
                        end = range.length > 1 ? Long.parseLong(range[1]) : fileLength - 1;
                    }
                    if(end > fileLength - 1)
                        end = fileLength - 1;
                    if(start <= end)
                        ranges.add(new long[] { start, end });
                }
                long[][] reducedRanges = reduceRanges(ranges.toArray(new long[0][]));
                ByteRange[] byteRanges = new ByteRange[reducedRanges.length];
                for(int i = 0; i < reducedRanges.length; i++) {
                    long[] range = reducedRanges[i];
                    byteRanges[i] = new ByteRange(range[0], range[1], fileLength, contentType, reducedRanges.length > 1);
                }
                this.byteRanges = byteRanges;
                if(this.byteRanges.length == 0)
                    unsatisfiable = true;
            } catch (Exception e) {
                if(Logger.isDebugEnabled())
                    Logger.debug(e, "byterange error");
                unsatisfiable = true;
            }
        }
       
        private static boolean rangesIntersect(long[] r1, long[] r2) {
            return r1[0] >= r2[0] && r1[0] <= r2[1] || r1[1] >= r2[0]
                    && r1[0] <= r2[1];
        }

        private static long[] mergeRanges(long[] r1, long[] r2) {
            return new long[] { r1[0] < r2[0] ? r1[0] : r2[0],
                    r1[1] > r2[1] ? r1[1] : r2[1] };
        }

        private static long[][] reduceRanges(long[]... chunks) {
            if (chunks.length == 0)
                return new long[0][];
            long[][] sortedChunks = Arrays.copyOf(chunks, chunks.length);
            Arrays.sort(sortedChunks, new Comparator<long[]>() {
                public int compare(long[] t1, long[] t2) {
                    return new Long(t1[0]).compareTo(t2[0]);
                }
            });
            ArrayList<long[]> result = new ArrayList<long[]>();
            result.add(sortedChunks[0]);
            for (int i = 1; i < sortedChunks.length; i++) {
                long[] c1 = sortedChunks[i];
                long[] r1 = result.get(result.size() - 1);
                if (rangesIntersect(c1, r1)) {
                    result.set(result.size() - 1, mergeRanges(c1, r1));
                } else {
                    result.add(c1);
                }
            }
            return result.toArray(new long[0][]);
        }
       
        private static String makeRangeBodyHeader(String separator, String contentType, long start, long end, long fileLength) {
            return  "--" + separator + "\r\n" +
                    "Content-Type: " + contentType + "\r\n" +
                    "ContentRange: bytes " + start + "-" + end + "/" + fileLength + "\r\n" +
                    "\r\n";
        }
       
        private class ByteRange {
            public long start;
            public long end;
            public byte[] header;
           
            public long length() {
                return end - start + 1;
            }
            public long remaining() {
                return end - start + 1 - servedRange;
            }
           
            public long computeTotalLengh() {
                return length() + header.length;
            }
           
            public int servedHeader = 0;
            public int servedRange = 0;
           
            public ByteRange(long start, long end, long fileLength, String contentType, boolean includeHeader) {
                this.start = start;
                this.end = end;
                if(includeHeader) {
                    header = makeRangeBodyHeader(DEFAULT_SEPARATOR, contentType, start, end, fileLength).getBytes();
                } else {
                    header = new byte[0];
                }
            }
           
            public int fill(byte[] into, int offset) throws IOException {
                if(Logger.isTraceEnabled())
                    Logger.trace("FileService fill at " + offset);
                int count = 0;
                for(; offset < into.length && servedHeader < header.length; offset++, servedHeader++, count++) {
                    into[offset] = header[servedHeader];
                }
                if(offset < into.length) {
                    try {
                        raf.seek(start + servedRange);
                        long maxToRead = remaining() > (into.length - offset) ? (into.length - offset) : remaining();
                        if(maxToRead > Integer.MAX_VALUE) {
                            if(Logger.isDebugEnabled())
                                Logger.debug("FileService: maxToRead >= 2^32 !");
                            maxToRead = Integer.MAX_VALUE;
                        }
                        int read = raf.read(into, offset, (int) maxToRead);
                        if(read < 0) {
                            throw new UnexpectedException("error while reading file : no more to read ! length=" + raf.length() + ", seek=" + (start + servedRange));
                        }
                        count += read;
                        servedRange += read;
                    } catch(IOException e) {
                        throw new UnexpectedException(e);
                    }
                }
                return count;
            }
           
            @Override
            public String toString() {
                return "ByteRange("+start+","+end+")";
            }
        }
       
        private static final String DEFAULT_SEPARATOR = "$$$THIS_STRING_SEPARATES$$$";
    }
}
TOP

Related Classes of play.server.FileService$ByteRangeInput$ByteRange

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.