1 /*
2 * Copyright 2012 The Netty Project
3 *
4 * The Netty Project licenses this file to you under the Apache License,
5 * version 2.0 (the "License"); you may not use this file except in compliance
6 * with the License. You may obtain a copy of the License at:
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16 package org.jboss.netty.handler.stream;
17
18 import org.jboss.netty.channel.FileRegion;
19
20 import java.io.File;
21 import java.io.FileInputStream;
22 import java.io.IOException;
23 import java.nio.ByteBuffer;
24 import java.nio.channels.FileChannel;
25
26 import static org.jboss.netty.buffer.ChannelBuffers.*;
27
28 /**
29 * A {@link ChunkedInput} that fetches data from a file chunk by chunk using
30 * NIO {@link FileChannel}.
31 * <p>
32 * If your operating system supports
33 * <a href="http://en.wikipedia.org/wiki/Zero-copy">zero-copy file transfer</a>
34 * such as {@code sendfile()}, you might want to use {@link FileRegion} instead.
35 */
36 public class ChunkedNioFile implements ChunkedInput {
37
38 private final FileChannel in;
39 private final long startOffset;
40 private final long endOffset;
41 private final int chunkSize;
42 private long offset;
43
44 /**
45 * Creates a new instance that fetches data from the specified file.
46 */
47 public ChunkedNioFile(File in) throws IOException {
48 this(new FileInputStream(in).getChannel());
49 }
50
51 /**
52 * Creates a new instance that fetches data from the specified file.
53 *
54 * @param chunkSize the number of bytes to fetch on each
55 * {@link #nextChunk()} call
56 */
57 public ChunkedNioFile(File in, int chunkSize) throws IOException {
58 this(new FileInputStream(in).getChannel(), chunkSize);
59 }
60
61 /**
62 * Creates a new instance that fetches data from the specified file.
63 */
64 public ChunkedNioFile(FileChannel in) throws IOException {
65 this(in, ChunkedStream.DEFAULT_CHUNK_SIZE);
66 }
67
68 /**
69 * Creates a new instance that fetches data from the specified file.
70 *
71 * @param chunkSize the number of bytes to fetch on each
72 * {@link #nextChunk()} call
73 */
74 public ChunkedNioFile(FileChannel in, int chunkSize) throws IOException {
75 this(in, 0, in.size(), chunkSize);
76 }
77
78 /**
79 * Creates a new instance that fetches data from the specified file.
80 *
81 * @param offset the offset of the file where the transfer begins
82 * @param length the number of bytes to transfer
83 * @param chunkSize the number of bytes to fetch on each
84 * {@link #nextChunk()} call
85 */
86 public ChunkedNioFile(FileChannel in, long offset, long length, int chunkSize)
87 throws IOException {
88 if (in == null) {
89 throw new NullPointerException("in");
90 }
91 if (offset < 0) {
92 throw new IllegalArgumentException(
93 "offset: " + offset + " (expected: 0 or greater)");
94 }
95 if (length < 0) {
96 throw new IllegalArgumentException(
97 "length: " + length + " (expected: 0 or greater)");
98 }
99 if (chunkSize <= 0) {
100 throw new IllegalArgumentException(
101 "chunkSize: " + chunkSize +
102 " (expected: a positive integer)");
103 }
104
105 if (offset != 0) {
106 in.position(offset);
107 }
108 this.in = in;
109 this.chunkSize = chunkSize;
110 this.offset = startOffset = offset;
111 endOffset = offset + length;
112 }
113
114 /**
115 * Returns the offset in the file where the transfer began.
116 */
117 public long getStartOffset() {
118 return startOffset;
119 }
120
121 /**
122 * Returns the offset in the file where the transfer will end.
123 */
124 public long getEndOffset() {
125 return endOffset;
126 }
127
128 /**
129 * Returns the offset in the file where the transfer is happening currently.
130 */
131 public long getCurrentOffset() {
132 return offset;
133 }
134
135 public boolean hasNextChunk() throws Exception {
136 return offset < endOffset && in.isOpen();
137 }
138
139 public boolean isEndOfInput() throws Exception {
140 return !hasNextChunk();
141 }
142
143 public void close() throws Exception {
144 in.close();
145 }
146
147 public Object nextChunk() throws Exception {
148 long offset = this.offset;
149 if (offset >= endOffset) {
150 return null;
151 }
152
153 int chunkSize = (int) Math.min(this.chunkSize, endOffset - offset);
154 byte[] chunkArray = new byte[chunkSize];
155 ByteBuffer chunk = ByteBuffer.wrap(chunkArray);
156 int readBytes = 0;
157 for (;;) {
158 int localReadBytes = in.read(chunk);
159 if (localReadBytes < 0) {
160 break;
161 }
162 readBytes += localReadBytes;
163 if (readBytes == chunkSize) {
164 break;
165 }
166 }
167
168 this.offset += readBytes;
169 return wrappedBuffer(chunkArray);
170 }
171 }