Coverage Report - org.webslinger.servlet.Binary
 
Classes in this File Line Coverage Branch Coverage Complexity
Binary
36%
40/112
52%
22/42
0
Binary$AbstractFileItemContent
0%
0/8
N/A
0
Binary$CommonsVfsContent
0%
0/4
N/A
0
Binary$Content
N/A
N/A
0
Binary$FileContent
88%
7/8
N/A
0
Binary$FileItemContent
0%
0/4
N/A
0
Binary$MultiContent
0%
0/41
0%
0/14
0
Binary$MultiRange
0%
0/4
N/A
0
Binary$Range
0%
0/21
0%
0/4
0
Binary$RequestData
71%
10/14
50%
1/2
0
 
 1  
 package org.webslinger.servlet;
 2  
 
 3  
 import java.io.InputStream;
 4  
 import java.io.IOException;
 5  
 import java.io.OutputStream;
 6  
 import java.io.SequenceInputStream;
 7  
 import java.util.ArrayList;
 8  
 import java.util.Arrays;
 9  
 import java.util.Date;
 10  
 import java.util.List;
 11  
 import java.util.Map;
 12  
 import java.util.StringTokenizer;
 13  
 import javax.servlet.ServletException;
 14  
 import javax.servlet.ServletOutputStream;
 15  
 import javax.servlet.ServletRequest;
 16  
 import javax.servlet.ServletResponse;
 17  
 import javax.servlet.http.HttpServletRequest;
 18  
 import javax.servlet.http.HttpServletResponse;
 19  
 
 20  
 import org.apache.bsf.BSFException;
 21  
 import org.apache.commons.collections.iterators.IteratorEnumeration;
 22  
 import org.apache.commons.fileupload.FileItem;
 23  
 import org.apache.commons.vfs.FileContent;
 24  
 import org.apache.commons.vfs.FileContentInfo;
 25  
 import org.apache.commons.vfs.FileObject;
 26  
 import org.apache.commons.vfs.FileSystemException;
 27  
 
 28  
 import org.webslinger.Webslinger;
 29  
 import org.webslinger.io.HttpUtil;
 30  
 import org.webslinger.io.IOUtil;
 31  
 import org.webslinger.lang.ObjectUtil;
 32  
 
 33  0
 public class Binary {
 34  
     public static final String mimeSeparator = "VFS_MIME_BOUNDARY";
 35  
 
 36  
     public static Object run(Content content, ServletRequest request, ServletResponse response) throws IOException, ServletException {
 37  83
         HttpServletRequest httpRequest = request instanceof HttpServletRequest ? (HttpServletRequest) request : null;
 38  83
         HttpServletResponse httpResponse = response instanceof HttpServletResponse ? (HttpServletResponse) response : null;
 39  83
         if (!content.exists()) {
 40  0
             if (httpResponse != null) httpResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
 41  0
             return null;
 42  
         }
 43  83
         if (!content.isReadable()) {
 44  0
             if (httpResponse != null) httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
 45  0
             return null;
 46  
         }
 47  83
         RequestData requestData = new RequestData(content);
 48  
         int mode;
 49  83
         if (httpRequest != null) {
 50  83
             String method = httpRequest.getMethod();
 51  83
             mode = "GET".equals(method) ? 0 : "HEAD".equals(method) ? 1 : 2;
 52  83
         } else {
 53  0
             mode = 0;
 54  
         }
 55  83
         if (mode == 2) {
 56  0
             if (httpResponse != null) httpResponse.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
 57  0
             return null;
 58  
         }
 59  83
         Object data = setResponse(requestData, request, response);
 60  83
         if (mode == 1) {
 61  0
             if (httpResponse != null) httpResponse.setStatus(HttpServletResponse.SC_OK);
 62  0
             return null;
 63  
         }
 64  83
         if (mode == 0 && httpRequest != null) {
 65  83
             String possibleEtag = httpRequest.getHeader("If-None-Match");
 66  83
             long modifiedSince = httpRequest.getDateHeader("If-Modified-Since");
 67  83
             boolean notModified = true;
 68  83
             if (possibleEtag != null && !requestData.etag.equals(possibleEtag)) notModified = false;
 69  83
             if (modifiedSince != 0 && modifiedSince != requestData.lastModifiedTime) notModified = false;
 70  83
             if (notModified) {
 71  0
                 httpResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
 72  0
                 return null;
 73  
             }
 74  
         }
 75  83
         if (data instanceof Content) {
 76  83
             if (httpResponse != null) httpResponse.setStatus(HttpServletResponse.SC_OK);
 77  83
             IOUtil.copy(((Content) data).getInputStream(), true, response.getOutputStream(), false);
 78  0
         } else if (data instanceof Range) {
 79  0
             Range range = (Range) data;
 80  0
             if (httpResponse != null) httpResponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
 81  0
             range.copyTo(response.getOutputStream());
 82  0
         } else if (data instanceof MultiRange) {
 83  0
             MultiRange mr = (MultiRange) data;
 84  0
             if (httpResponse != null) httpResponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
 85  0
             ServletOutputStream out = response.getOutputStream();
 86  0
             for (Range range: mr.ranges) {
 87  0
                 out.println("--" + mimeSeparator);
 88  0
                 if (mr.contentType != null) out.println("Content-type: " + mr.contentType);
 89  0
                 out.println("Content-Range: bytes " + range.start + '-' + range.end + '/' + range.length);
 90  0
                 out.println();
 91  0
                 range.copyTo(out);
 92  
             }
 93  
         }
 94  83
         return null;
 95  
     }
 96  
 
 97  
     protected static Object setResponse(RequestData requestData, ServletRequest request, ServletResponse response) throws IOException, ServletException {
 98  83
         HttpServletResponse httpResponse = response instanceof HttpServletResponse ? (HttpServletResponse) response : null;
 99  83
         List<Range> ranges = getRanges(requestData, request, response);
 100  83
         if (httpResponse != null) {
 101  83
             httpResponse.setHeader("ETag", requestData.etag);
 102  83
             httpResponse.setHeader("Last-Modified", HttpUtil.formatDate(requestData.lastModifiedTime));
 103  
         }
 104  83
         String contentType = requestData.content.getContentType();
 105  83
         if (contentType != null) {
 106  83
             String contentEncoding = requestData.content.getContentEncoding();
 107  83
             if (contentEncoding != null) contentType += "; charset=" + contentEncoding;
 108  
         }
 109  83
         if (ranges == null) {
 110  83
             response.setContentLength((int) requestData.size);
 111  83
             if (contentType != null) response.setContentType(contentType);
 112  83
             return requestData.content;
 113  
         }
 114  0
         if (ranges.size() == 1) {
 115  0
             Range range = (Range) ranges.get(0);
 116  0
             if (httpResponse != null) {
 117  0
                 httpResponse.addHeader("Content-Range", "bytes " + range.start + '-' + range.end + '/' + range.length);
 118  
             }
 119  0
             response.setContentLength((int) range.length);
 120  0
             if (contentType != null) response.setContentType(contentType);
 121  0
             return range;
 122  
         }
 123  0
         response.setContentType("multipart/byteranges; boundary=" + mimeSeparator);
 124  0
         return new MultiRange(ranges, contentType);
 125  
     }
 126  
 
 127  
     protected static List<Range> getRanges(RequestData file, ServletRequest request, ServletResponse response) throws IOException, ServletException {
 128  83
         if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) return null;
 129  83
         return getRanges(file, (HttpServletRequest) request, (HttpServletResponse) response);
 130  
     }
 131  
 
 132  
     protected static List<Range> getRanges(RequestData file, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
 133  83
         String range = request.getHeader("Range");
 134  83
         if (range == null) return null;
 135  0
         String ifRange = request.getHeader("If-Range");
 136  0
         Date ifDate = HttpUtil.parseDate(ifRange);
 137  0
         if (ifDate != null && file.lastModifiedTime / 1000 != ifDate.getTime() / 1000) return null;
 138  0
         long size = file.size;
 139  0
         if (!range.startsWith("bytes=")) {
 140  0
             response.addHeader("Content-Range", "bytes */" + size);
 141  0
             response.sendError(HttpServletResponse.SC_OK);
 142  0
             return null;
 143  
         }
 144  0
         range = range.substring(6);
 145  0
         List<Range> ranges = new ArrayList<Range>();
 146  0
         StringTokenizer st = new StringTokenizer(range);
 147  0
         while (st.hasMoreTokens()) {
 148  0
             String token = st.nextToken();
 149  0
             int dash = token.indexOf("-");
 150  0
             if (dash == -1) {
 151  0
                 ranges = null;
 152  0
                 break;
 153  
             }
 154  
             long start, end, length;
 155  
             try {
 156  0
                 if (dash == 0) {
 157  
                     // - is the first char; parseLong returns a negative number
 158  0
                     start = size + Long.parseLong(token);
 159  0
                     end = size - 1;
 160  
                 } else {
 161  0
                     start = Long.parseLong(token.substring(0, dash));
 162  0
                     if (dash + 1 == token.length()) {
 163  0
                         end = size - 1;
 164  
                     } else {
 165  0
                         end = Long.parseLong(token.substring(dash + 1));
 166  
                     }
 167  
                 }
 168  0
             } catch (NumberFormatException e) {
 169  0
                 ranges = null;
 170  0
                 break;
 171  0
             }
 172  0
             length = end - start + 1;
 173  0
             ranges.add(new Range(start, end, length, file.content));
 174  0
         }
 175  0
         if (ranges == null || ranges.isEmpty()) {
 176  0
             response.addHeader("Content-Range", "bytes */" + size);
 177  0
             response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
 178  0
             return null;
 179  
         }
 180  0
         return ranges;
 181  
     }
 182  
 
 183  
     public static interface Content {
 184  
         long getLastModifiedTime() throws IOException;
 185  
         long getSize() throws IOException;
 186  
         InputStream getInputStream() throws IOException;
 187  
         String getContentType() throws IOException;
 188  
         String getContentEncoding() throws IOException;
 189  
         boolean exists() throws IOException;
 190  
         boolean isReadable() throws IOException;
 191  
     }
 192  
 
 193  
     public static class MultiContent implements Content {
 194  
         protected final List<Content> contents;
 195  
 
 196  0
         public MultiContent(Content[] contents) {
 197  0
             this.contents = Arrays.asList(contents);
 198  0
         }
 199  
 
 200  
         public long getLastModifiedTime() throws IOException {
 201  0
             long lastModifiedTime = 0;
 202  0
             for (Content content: contents) {
 203  0
                 long newLastModifiedTime = content.getLastModifiedTime();
 204  0
                 if (newLastModifiedTime > lastModifiedTime) lastModifiedTime = newLastModifiedTime;
 205  0
             }
 206  0
             return lastModifiedTime;
 207  
         }
 208  
 
 209  
         public long getSize() throws IOException {
 210  0
             long size = 0;
 211  0
             for (Content content: contents) {
 212  0
                 size += content.getSize();
 213  
             }
 214  0
             return size;
 215  
         }
 216  
 
 217  
         public InputStream getInputStream() throws IOException {
 218  0
             ArrayList<InputStream> streams = new ArrayList<InputStream>();
 219  0
             for (Content content: contents) {
 220  0
                 streams.add(content.getInputStream());
 221  
             }
 222  0
             return new SequenceInputStream(new IteratorEnumeration(streams.iterator()));
 223  
         }
 224  
 
 225  
         public String getContentType() throws IOException {
 226  0
             String contentType = null;
 227  0
             for (Content content: contents) {
 228  0
                 if (contentType == null) {
 229  0
                     contentType = content.getContentType();
 230  
                 } else {
 231  0
                     String newContentType = content.getContentType();
 232  0
                     if (!ObjectUtil.equalsHelper(contentType, newContentType)) {
 233  0
                         throw new IOException("Couldn't determine content type(different)");
 234  
                     }
 235  0
                 }
 236  
             }
 237  0
             return contentType;
 238  
         }
 239  
 
 240  
         public String getContentEncoding() throws IOException {
 241  0
             String contentEncoding = null;
 242  0
             for (Content content: contents) {
 243  0
                 if (contentEncoding == null) {
 244  0
                     contentEncoding = content.getContentEncoding();
 245  
                 } else {
 246  0
                     String newContentEncoding = content.getContentEncoding();
 247  0
                     if (!ObjectUtil.equalsHelper(contentEncoding, newContentEncoding)) {
 248  0
                         throw new IOException("Couldn't determine content encoding(different)");
 249  
                     }
 250  0
                 }
 251  
             }
 252  0
             return contentEncoding;
 253  
         }
 254  
 
 255  
         public boolean exists() throws IOException {
 256  0
             for (Content content: contents) {
 257  0
                 if (!content.exists()) return false;
 258  
             }
 259  0
             return true;
 260  
         }
 261  
 
 262  
         public boolean isReadable() throws IOException {
 263  0
             for (Content content: contents) {
 264  0
                 if (!content.isReadable()) return false;
 265  
             }
 266  0
             return true;
 267  
         }
 268  
     }
 269  
 
 270  83
     public abstract static class FileContent implements Content {
 271  
         public abstract FileObject getFile() throws IOException;
 272  
 
 273  
         public long getLastModifiedTime() throws IOException {
 274  83
             return getFile().getContent().getLastModifiedTime();
 275  
         }
 276  
 
 277  
         public long getSize() throws IOException {
 278  83
             return getFile().getContent().getSize();
 279  
         }
 280  
 
 281  
         public InputStream getInputStream() throws IOException {
 282  83
             return getFile().getContent().getInputStream();
 283  
         }
 284  
 
 285  
         public String getContentType() throws IOException {
 286  166
             return getFile().getContent().getContentInfo().getContentType();
 287  
         }
 288  
 
 289  
         public String getContentEncoding() throws IOException {
 290  0
             return getFile().getContent().getContentInfo().getContentEncoding();
 291  
         }
 292  
 
 293  
         public boolean exists() throws IOException {
 294  83
             return getFile().exists();
 295  
         }
 296  
 
 297  
         public boolean isReadable() throws IOException {
 298  83
             return getFile().isReadable();
 299  
         }
 300  
     }
 301  
 
 302  
     public static final class CommonsVfsContent extends FileContent {
 303  
         public final FileObject file;
 304  
 
 305  0
         public CommonsVfsContent(FileObject file) {
 306  0
             this.file = file;
 307  0
         }
 308  
 
 309  
         public FileObject getFile() throws IOException {
 310  0
             return file;
 311  
         }
 312  
     }
 313  
 
 314  0
     public static abstract class AbstractFileItemContent implements Content {
 315  
         protected abstract FileItem getFileItem();
 316  
 
 317  
         public long getLastModifiedTime() throws IOException {
 318  0
             return System.currentTimeMillis();
 319  
         }
 320  
 
 321  
         public long getSize() throws IOException {
 322  0
             return getFileItem().getSize();
 323  
         }
 324  
 
 325  
         public InputStream getInputStream() throws IOException {
 326  0
             return getFileItem().getInputStream();
 327  
         }
 328  
 
 329  
         public String getContentType() throws IOException {
 330  0
             return getFileItem().getContentType();
 331  
         }
 332  
 
 333  
         public String getContentEncoding() throws IOException {
 334  0
             return null;
 335  
         }
 336  
 
 337  
         public boolean exists() throws IOException {
 338  0
             return true;
 339  
         }
 340  
 
 341  
         public boolean isReadable() throws IOException {
 342  0
             return true;
 343  
         }
 344  
     }
 345  
 
 346  
     public static final class FileItemContent extends AbstractFileItemContent {
 347  
         protected final FileItem file;
 348  
 
 349  0
         public FileItemContent(FileItem file) {
 350  0
             this.file = file;
 351  0
         }
 352  
 
 353  
         protected FileItem getFileItem() {
 354  0
             return file;
 355  
         }
 356  
     }
 357  
 
 358  
     public static final class RequestData {
 359  
         public final Content content;
 360  
         public final long lastModifiedTime;
 361  
         public final long size;
 362  
         public final String etag;
 363  
 
 364  
         public RequestData(FileObject file) throws IOException {
 365  0
             this(new CommonsVfsContent(file));
 366  0
         }
 367  
 
 368  83
         public RequestData(Content content) throws IOException {
 369  83
             this.content = content;
 370  83
             lastModifiedTime = content.getLastModifiedTime();
 371  83
             size = content.getSize();
 372  83
             StringBuilder sb = new StringBuilder();
 373  83
             sb.append("W/\"").append(size);
 374  83
             if (content instanceof MultiContent) {
 375  0
                 for (Content c: ((MultiContent) content).contents) {
 376  0
                     sb.append('-').append(c.getLastModifiedTime());
 377  
                 }
 378  
             } else {
 379  83
                 sb.append('-').append(lastModifiedTime);
 380  
             }
 381  83
             etag = sb.append('"').toString();
 382  83
         }
 383  
     }
 384  
 
 385  
     protected static final class Range {
 386  
         public final long start, end, length;
 387  
         public final Content content;
 388  
 
 389  0
         public Range(long start, long end, long length, Content content) {
 390  0
             this.start = start;
 391  0
             this.end = end >= length ? length - 1 : end;
 392  0
             this.length = length;
 393  0
             this.content = content;
 394  0
         }
 395  
 
 396  
         public boolean check() {
 397  0
             return start >= 0 && end >= 0 && start <= end && length > 0;
 398  
         }
 399  
 
 400  
         public void copyTo(OutputStream out) throws IOException {
 401  0
             InputStream in = content.getInputStream();
 402  0
             in.skip(start);
 403  0
             byte[] buf = new byte[1024];
 404  0
             long left = length;
 405  
             try {
 406  0
                 while (left > 0) {
 407  0
                     int bytesRead = in.read(buf);
 408  0
                     if (bytesRead == -1) break;
 409  0
                     left -= bytesRead;
 410  0
                     out.write(buf, 0, bytesRead);
 411  0
                 }
 412  
             } finally {
 413  0
                 in.close();
 414  0
             }
 415  0
         }
 416  
 
 417  
         public String toString() {
 418  0
             return "Range<" + start + ", " + end + ", " + length + ">";
 419  
         }
 420  
     }
 421  
 
 422  0
     protected static final class MultiRange {
 423  
         public final List<Range> ranges;
 424  
         public final String contentType;
 425  
 
 426  0
         public MultiRange(List<Range> ranges, String contentType) {
 427  0
             this.ranges = ranges;
 428  0
             this.contentType = contentType;
 429  0
         }
 430  
     }
 431  
 }