Skip to content

Commit

Permalink
FileOperator, a utility for doing random access I/O with Okio.
Browse files Browse the repository at this point in the history
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
squarejesse committed Jul 25, 2016
1 parent 6014ab9 commit a003e84
Show file tree
Hide file tree
Showing 2 changed files with 296 additions and 0 deletions.
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 okhttp/src/main/java/okhttp3/internal/cache2/FileOperator.java
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();
}
}
}
}

0 comments on commit a003e84

Please sign in to comment.