blob: 01f75d3107f14c3c5811c48eaca606c2fb0b6aa2 [file] [log] [blame]
/*
* Copyright 2009 Google 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 com.google.gwt.dev.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* A nifty class that lets you squirrel away data on the file system. Write
* once, read many times. Instance of this are thread-safe by way of internal
* synchronization.
*
* Note that in the current implementation, the backing temp file will get
* arbitrarily large as you continue adding things to it. There is no internal
* GC or compaction.
*/
public class DiskCache {
/**
* For future thought: if we used Object tokens instead of longs, we could
* actually track references and do GC/compaction on the underlying file.
*
* I considered using memory mapping, but I didn't see any obvious way to make
* the map larger after the fact, which kind of defeats the infinite-append
* design. At any rate, I measured the current performance of this design to
* be so fast relative to what I'm using it for, I didn't pursue this further.
*/
private static class Shutdown implements Runnable {
public void run() {
for (WeakReference<DiskCache> ref : shutdownList) {
try {
DiskCache diskCache = ref.get();
if (diskCache != null) {
diskCache.close();
}
} catch (Throwable e) {
}
}
}
}
/**
* A global shared Disk cache.
*/
public static DiskCache INSTANCE = new DiskCache();
private static List<WeakReference<DiskCache>> shutdownList;
private boolean atEnd = true;
private RandomAccessFile file;
DiskCache() {
try {
File temp = File.createTempFile("gwt", "byte-cache");
temp.deleteOnExit();
file = new RandomAccessFile(temp, "rw");
file.setLength(0);
if (shutdownList == null) {
shutdownList = new ArrayList<WeakReference<DiskCache>>();
Runtime.getRuntime().addShutdownHook(new Thread(new Shutdown()));
}
shutdownList.add(new WeakReference<DiskCache>(this));
} catch (IOException e) {
throw new RuntimeException("Unable to initialize byte cache", e);
}
}
/**
* Retrieve the underlying bytes.
*
* @param token a previously returned token
* @return the bytes that were written
*/
public synchronized byte[] readByteArray(long token) {
try {
atEnd = false;
file.seek(token);
int length = file.readInt();
byte[] result = new byte[length];
file.readFully(result);
return result;
} catch (IOException e) {
throw new RuntimeException("Unable to read from byte cache", e);
}
}
/**
* Deserialize the underlying bytes as an object.
*
* @param <T> the type of the object to deserialize
* @param token a previously returned token
* @param type the type of the object to deserialize
* @return the deserialized object
*/
public <T> T readObject(long token, Class<T> type) {
try {
byte[] bytes = readByteArray(token);
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
return Util.readStreamAsObject(in, type);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Unexpected exception deserializing from disk cache", e);
} catch (IOException e) {
throw new RuntimeException("Unexpected exception deserializing from disk cache", e);
}
}
/**
* Read the underlying bytes as a String.
*
* @param token a previously returned token
* @return the String that was written
*/
public String readString(long token) {
return Util.toString(readByteArray(token));
}
/**
* Write the rest of the data in an input stream to disk.
*
* @return a token to retrieve the data later
*/
public synchronized long transferFromStream(InputStream in) {
byte[] buf = Util.takeThreadLocalBuf();
try {
long position = moveToEndPosition();
// Placeholder, we don't know the length yet.
file.writeInt(-1);
// Transfer all the bytes.
int length = 0;
int bytesRead;
while ((bytesRead = in.read(buf)) != -1) {
file.write(buf, 0, bytesRead);
length += bytesRead;
}
// Now go back and fill in the length.
file.seek(position);
file.writeInt(length);
// Don't eagerly seek the end, the next operation might be a read.
atEnd = false;
return position;
} catch (IOException e) {
throw new RuntimeException("Unable to read from byte cache", e);
} finally {
Util.releaseThreadLocalBuf(buf);
}
}
/**
* Writes the underlying bytes into the specified output stream.
*
* @param token a previously returned token
* @param out the stream to write into
*/
public synchronized void transferToStream(long token, OutputStream out) {
byte[] buf = Util.takeThreadLocalBuf();
try {
atEnd = false;
file.seek(token);
int length = file.readInt();
int bufLen = buf.length;
while (length > bufLen) {
int read = file.read(buf, 0, bufLen);
length -= read;
out.write(buf, 0, read);
}
while (length > 0) {
int read = file.read(buf, 0, length);
length -= read;
out.write(buf, 0, read);
}
} catch (IOException e) {
throw new RuntimeException("Unable to read from byte cache", e);
} finally {
Util.releaseThreadLocalBuf(buf);
}
}
/**
* Write a byte array to disk.
*
* @return a token to retrieve the data later
*/
public synchronized long writeByteArray(byte[] bytes) {
try {
long position = moveToEndPosition();
file.writeInt(bytes.length);
file.write(bytes);
return position;
} catch (IOException e) {
throw new RuntimeException("Unable to write to byte cache", e);
}
}
/**
* Serialize an Object to disk.
*
* @return a token to retrieve the data later
*/
public long writeObject(Object object) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
Util.writeObjectToStream(out, object);
return writeByteArray(out.toByteArray());
} catch (IOException e) {
throw new RuntimeException("Unexpected IOException on in-memory stream", e);
}
}
/**
* Write a String to disk as bytes.
*
* @return a token to retrieve the data later
*/
public long writeString(String str) {
return writeByteArray(Util.getBytes(str));
}
@Override
protected synchronized void finalize() throws Throwable {
close();
}
private void close() throws Throwable {
if (file != null) {
file.setLength(0);
file.close();
file = null;
}
}
/**
* Moves to the end of the file if necessary and returns the offset position.
* Caller must synchronize.
*
* @return the offset position of the end of the file
* @throws IOException
*/
private long moveToEndPosition() throws IOException {
// Get an end pointer.
if (atEnd) {
return file.getFilePointer();
} else {
long position = file.length();
file.seek(position);
atEnd = true;
return position;
}
}
}