forked from square/okhttp
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
FileOperator, a utility for doing random access I/O with Okio.
This is just the basics that I need for DiskLruCache2. It's not a particularly general purpose API and at the moment there are no plans to expose it as such. square#2682
- Loading branch information
1 parent
6014ab9
commit a003e84
Showing
2 changed files
with
296 additions
and
0 deletions.
There are no files selected for viewing
200 changes: 200 additions & 0 deletions
200
okhttp-tests/src/test/java/okhttp3/internal/cache2/FileOperatorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
/* | ||
* Copyright (C) 2016 Square, Inc. | ||
* | ||
* 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. | ||
*/ | ||
package okhttp3.internal.cache2; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.io.RandomAccessFile; | ||
import java.util.Random; | ||
import okio.Buffer; | ||
import okio.BufferedSink; | ||
import okio.BufferedSource; | ||
import okio.ByteString; | ||
import okio.Okio; | ||
import org.junit.After; | ||
import org.junit.Before; | ||
import org.junit.Rule; | ||
import org.junit.Test; | ||
import org.junit.rules.TemporaryFolder; | ||
|
||
import static org.junit.Assert.assertEquals; | ||
import static org.junit.Assert.fail; | ||
|
||
public final class FileOperatorTest { | ||
@Rule public final TemporaryFolder tempDir = new TemporaryFolder(); | ||
|
||
private File file; | ||
private RandomAccessFile randomAccessFile; | ||
|
||
@Before public void setUp() throws Exception { | ||
file = tempDir.newFile(); | ||
randomAccessFile = new RandomAccessFile(file, "rw"); | ||
} | ||
|
||
@After public void tearDown() throws Exception { | ||
randomAccessFile.close(); | ||
} | ||
|
||
@Test public void read() throws Exception { | ||
write(ByteString.encodeUtf8("Hello, World")); | ||
|
||
FileOperator operator = new FileOperator(randomAccessFile.getChannel()); | ||
|
||
Buffer buffer = new Buffer(); | ||
operator.read(0, buffer, 5); | ||
assertEquals("Hello", buffer.readUtf8()); | ||
|
||
operator.read(4, buffer, 5); | ||
assertEquals("o, Wo", buffer.readUtf8()); | ||
} | ||
|
||
@Test public void write() throws Exception { | ||
FileOperator operator = new FileOperator(randomAccessFile.getChannel()); | ||
|
||
Buffer buffer1 = new Buffer().writeUtf8("Hello, World"); | ||
operator.write(0, buffer1, 5); | ||
assertEquals(", World", buffer1.readUtf8()); | ||
|
||
Buffer buffer2 = new Buffer().writeUtf8("icopter!"); | ||
operator.write(3, buffer2, 7); | ||
assertEquals("!", buffer2.readUtf8()); | ||
|
||
assertEquals(ByteString.encodeUtf8("Helicopter"), snapshot()); | ||
} | ||
|
||
@Test public void readAndWrite() throws Exception { | ||
FileOperator operator = new FileOperator(randomAccessFile.getChannel()); | ||
|
||
write(ByteString.encodeUtf8("woman god creates dinosaurs destroys. ")); | ||
Buffer buffer = new Buffer(); | ||
operator.read(6, buffer, 21); | ||
operator.read(36, buffer, 1); | ||
operator.read(5, buffer, 5); | ||
operator.read(28, buffer, 8); | ||
operator.read(17, buffer, 10); | ||
operator.read(36, buffer, 2); | ||
operator.read(2, buffer, 4); | ||
operator.write(0, buffer, buffer.size()); | ||
operator.read(0, buffer, 12); | ||
operator.read(47, buffer, 3); | ||
operator.read(45, buffer, 2); | ||
operator.read(47, buffer, 3); | ||
operator.read(26, buffer, 10); | ||
operator.read(23, buffer, 3); | ||
operator.write(47, buffer, buffer.size()); | ||
operator.read(62, buffer, 6); | ||
operator.read(4, buffer, 19); | ||
operator.write(80, buffer, buffer.size()); | ||
|
||
assertEquals(snapshot(), ByteString.encodeUtf8("" | ||
+ "god creates dinosaurs. " | ||
+ "god destroys dinosaurs. " | ||
+ "god creates man. " | ||
+ "man destroys god. " | ||
+ "man creates dinosaurs. ")); | ||
} | ||
|
||
@Test public void multipleOperatorsShareOneFile() throws Exception { | ||
FileOperator operatorA = new FileOperator(randomAccessFile.getChannel()); | ||
FileOperator operatorB = new FileOperator(randomAccessFile.getChannel()); | ||
|
||
Buffer bufferA = new Buffer(); | ||
Buffer bufferB = new Buffer(); | ||
|
||
bufferA.writeUtf8("Dodgson!\n"); | ||
operatorA.write(0, bufferA, 9); | ||
|
||
bufferB.writeUtf8("You shouldn't use my name.\n"); | ||
operatorB.write(9, bufferB, 27); | ||
|
||
bufferA.writeUtf8("Dodgson, we've got Dodgson here!\n"); | ||
operatorA.write(36, bufferA, 33); | ||
|
||
operatorB.read(0, bufferB, 9); | ||
assertEquals("Dodgson!\n", bufferB.readUtf8()); | ||
|
||
operatorA.read(9, bufferA, 27); | ||
assertEquals("You shouldn't use my name.\n", bufferA.readUtf8()); | ||
|
||
operatorB.read(36, bufferB, 33); | ||
assertEquals("Dodgson, we've got Dodgson here!\n", bufferB.readUtf8()); | ||
} | ||
|
||
@Test public void largeRead() throws Exception { | ||
ByteString data = randomByteString(1000000); | ||
write(data); | ||
|
||
FileOperator operator = new FileOperator(randomAccessFile.getChannel()); | ||
|
||
Buffer buffer = new Buffer(); | ||
operator.read(0, buffer, data.size()); | ||
assertEquals(data, buffer.readByteString()); | ||
} | ||
|
||
@Test public void largeWrite() throws Exception { | ||
ByteString data = randomByteString(1000000); | ||
|
||
FileOperator operator = new FileOperator(randomAccessFile.getChannel()); | ||
|
||
Buffer buffer = new Buffer().write(data); | ||
operator.write(0, buffer, data.size()); | ||
|
||
assertEquals(data, snapshot()); | ||
} | ||
|
||
@Test public void readBounds() throws Exception { | ||
FileOperator operator = new FileOperator(randomAccessFile.getChannel()); | ||
Buffer buffer = new Buffer(); | ||
try { | ||
operator.read(0, buffer, -1L); | ||
fail(); | ||
} catch (IndexOutOfBoundsException expected) { | ||
} | ||
} | ||
|
||
@Test public void writeBounds() throws Exception { | ||
FileOperator operator = new FileOperator(randomAccessFile.getChannel()); | ||
Buffer buffer = new Buffer().writeUtf8("abc"); | ||
try { | ||
operator.write(0, buffer, -1L); | ||
fail(); | ||
} catch (IndexOutOfBoundsException expected) { | ||
} | ||
try { | ||
operator.write(0, buffer, 4L); | ||
fail(); | ||
} catch (IndexOutOfBoundsException expected) { | ||
} | ||
} | ||
|
||
private ByteString randomByteString(int byteCount) { | ||
byte[] bytes = new byte[byteCount]; | ||
new Random(0).nextBytes(bytes); | ||
return ByteString.of(bytes); | ||
} | ||
|
||
private ByteString snapshot() throws IOException { | ||
randomAccessFile.getChannel().force(false); | ||
BufferedSource source = Okio.buffer(Okio.source(file)); | ||
return source.readByteString(); | ||
} | ||
|
||
private void write(ByteString data) throws IOException { | ||
BufferedSink sink = Okio.buffer(Okio.sink(file)); | ||
sink.write(data); | ||
sink.close(); | ||
} | ||
} |
96 changes: 96 additions & 0 deletions
96
okhttp/src/main/java/okhttp3/internal/cache2/FileOperator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/* | ||
* Copyright (C) 2016 Square, Inc. | ||
* | ||
* 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. | ||
*/ | ||
package okhttp3.internal.cache2; | ||
|
||
import java.io.EOFException; | ||
import java.io.IOException; | ||
import java.nio.ByteBuffer; | ||
import java.nio.channels.FileChannel; | ||
import okio.Buffer; | ||
import okio.Okio; | ||
|
||
/** | ||
* Read and write a target file. Unlike Okio's built-in {@linkplain Okio#source(java.io.File) file | ||
* source} and {@linkplain Okio#sink(java.io.File) file sink} this class offers: | ||
* | ||
* <ul> | ||
* <li><strong>Read/write:</strong> read and write using the same operator. | ||
* <li><strong>Random access:</strong> access any position within the file. | ||
* <li><strong>Shared channels:</strong> read and write a file channel that's shared between | ||
* multiple operators. Note that although the underlying {@code FileChannel} may be shared, | ||
* each {@code FileOperator} should not be. | ||
* </ul> | ||
*/ | ||
final class FileOperator { | ||
private static final int BUFFER_SIZE = 8192; | ||
|
||
private final byte[] byteArray = new byte[BUFFER_SIZE]; | ||
private final ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray); | ||
private final FileChannel fileChannel; | ||
|
||
public FileOperator(FileChannel fileChannel) { | ||
this.fileChannel = fileChannel; | ||
} | ||
|
||
/** Write {@code byteCount} bytes from {@code source} to the file at {@code pos}. */ | ||
public void write(long pos, Buffer source, long byteCount) throws IOException { | ||
if (byteCount < 0 || byteCount > source.size()) throw new IndexOutOfBoundsException(); | ||
|
||
while (byteCount > 0L) { | ||
try { | ||
// Write bytes to the byte[], and tell the ByteBuffer wrapper about 'em. | ||
int toWrite = (int) Math.min(BUFFER_SIZE, byteCount); | ||
source.read(byteArray, 0, toWrite); | ||
byteBuffer.limit(toWrite); | ||
|
||
// Copy bytes from the ByteBuffer to the file. | ||
do { | ||
int bytesWritten = fileChannel.write(byteBuffer, pos); | ||
pos += bytesWritten; | ||
} while (byteBuffer.hasRemaining()); | ||
|
||
byteCount -= toWrite; | ||
} finally { | ||
byteBuffer.clear(); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Copy {@code byteCount} bytes from the file at {@code pos} into to {@code source}. It is the | ||
* caller's responsibility to make sure there are sufficient bytes to read: if there aren't this | ||
* method throws an {@link EOFException}. | ||
*/ | ||
public void read(long pos, Buffer sink, long byteCount) throws IOException { | ||
if (byteCount < 0) throw new IndexOutOfBoundsException(); | ||
|
||
while (byteCount > 0L) { | ||
try { | ||
// Read up to byteCount bytes. | ||
byteBuffer.limit((int) Math.min(BUFFER_SIZE, byteCount)); | ||
if (fileChannel.read(byteBuffer, pos) == -1) throw new EOFException(); | ||
int bytesRead = byteBuffer.position(); | ||
|
||
// Write those bytes to sink. | ||
sink.write(byteArray, 0, bytesRead); | ||
pos += bytesRead; | ||
byteCount -= bytesRead; | ||
} finally { | ||
byteBuffer.clear(); | ||
} | ||
} | ||
} | ||
} |