Commit f182c0c1 by olly Committed by Oliver Woodman

Centralize serialization in CachedContentIndex

We need to support serialization to/from an SQLite table. The
model of passing something around for each class to write into
doesn't work well for SQL, and it would be messy to have two
different structural designs for serialization. This change
centralizes the logic in CachedContentIndex, where a centralized
SQL based version can more easily sit alongside it.

PiperOrigin-RevId: 230692291
parent 49b9775d
...@@ -18,17 +18,11 @@ package com.google.android.exoplayer2.upstream.cache; ...@@ -18,17 +18,11 @@ package com.google.android.exoplayer2.upstream.cache;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.TreeSet; import java.util.TreeSet;
/** Defines the cached content for a single stream. */ /** Defines the cached content for a single stream. */
/* package */ final class CachedContent { /* package */ final class CachedContent {
private static final int VERSION_METADATA_INTRODUCED = 2;
private static final int VERSION_MAX = Integer.MAX_VALUE;
/** The cache file id that uniquely identifies the original stream. */ /** The cache file id that uniquely identifies the original stream. */
public final int id; public final int id;
/** The cache key that uniquely identifies the original stream. */ /** The cache key that uniquely identifies the original stream. */
...@@ -41,55 +35,24 @@ import java.util.TreeSet; ...@@ -41,55 +35,24 @@ import java.util.TreeSet;
private boolean locked; private boolean locked;
/** /**
* Reads an instance from a {@link DataInputStream}.
*
* @param version Version of the encoded data.
* @param input Input stream containing values needed to initialize CachedContent instance.
* @throws IOException If an error occurs during reading values.
*/
public static CachedContent readFromStream(int version, DataInputStream input)
throws IOException {
int id = input.readInt();
String key = input.readUTF();
CachedContent cachedContent = new CachedContent(id, key);
if (version < VERSION_METADATA_INTRODUCED) {
long length = input.readLong();
ContentMetadataMutations mutations = new ContentMetadataMutations();
ContentMetadataMutations.setContentLength(mutations, length);
cachedContent.applyMetadataMutations(mutations);
} else {
cachedContent.metadata = DefaultContentMetadata.readFromStream(input);
}
return cachedContent;
}
/**
* Creates a CachedContent. * Creates a CachedContent.
* *
* @param id The cache file id. * @param id The cache file id.
* @param key The cache stream key. * @param key The cache stream key.
*/ */
public CachedContent(int id, String key) { public CachedContent(int id, String key) {
this(id, key, DefaultContentMetadata.EMPTY);
}
public CachedContent(int id, String key, DefaultContentMetadata metadata) {
this.id = id; this.id = id;
this.key = key; this.key = key;
this.metadata = DefaultContentMetadata.EMPTY; this.metadata = metadata;
this.cachedSpans = new TreeSet<>(); this.cachedSpans = new TreeSet<>();
} }
/**
* Writes the instance to a {@link DataOutputStream}.
*
* @param output Output stream to store the values.
* @throws IOException If an error occurs during writing values to output.
*/
public void writeToStream(DataOutputStream output) throws IOException {
output.writeInt(id);
output.writeUTF(key);
metadata.writeToStream(output);
}
/** Returns the metadata. */ /** Returns the metadata. */
public ContentMetadata getMetadata() { public DefaultContentMetadata getMetadata() {
return metadata; return metadata;
} }
...@@ -208,26 +171,11 @@ import java.util.TreeSet; ...@@ -208,26 +171,11 @@ import java.util.TreeSet;
return false; return false;
} }
/** @Override
* Calculates a hash code for the header of this {@code CachedContent} which is compatible with public int hashCode() {
* the index file with {@code version}.
*/
public int headerHashCode(int version) {
int result = id; int result = id;
result = 31 * result + key.hashCode(); result = 31 * result + key.hashCode();
if (version < VERSION_METADATA_INTRODUCED) {
long length = ContentMetadata.getContentLength(metadata);
result = 31 * result + (int) (length ^ (length >>> 32));
} else {
result = 31 * result + metadata.hashCode(); result = 31 * result + metadata.hashCode();
}
return result;
}
@Override
public int hashCode() {
int result = headerHashCode(VERSION_MAX);
result = 31 * result + cachedSpans.hashCode();
return result; return result;
} }
......
...@@ -33,8 +33,10 @@ import java.io.OutputStream; ...@@ -33,8 +33,10 @@ import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import javax.crypto.Cipher; import javax.crypto.Cipher;
...@@ -51,6 +53,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -51,6 +53,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
public static final String FILE_NAME = "cached_content_index.exi"; public static final String FILE_NAME = "cached_content_index.exi";
private static final int VERSION = 2; private static final int VERSION = 2;
private static final int VERSION_METADATA_INTRODUCED = 2;
private static final int INCREMENTAL_METADATA_READ_LENGTH = 10 * 1024 * 1024;
private static final int FLAG_ENCRYPTED_INDEX = 1; private static final int FLAG_ENCRYPTED_INDEX = 1;
...@@ -245,6 +249,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -245,6 +249,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY; return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY;
} }
private CachedContent addNew(String key) {
int id = getNewId(idToKey);
CachedContent cachedContent = new CachedContent(id, key);
add(cachedContent);
changed = true;
return cachedContent;
}
private void add(CachedContent cachedContent) {
keyToContent.put(cachedContent.key, cachedContent);
idToKey.put(cachedContent.id, cachedContent.key);
}
private boolean readFile() { private boolean readFile() {
DataInputStream input = null; DataInputStream input = null;
try { try {
...@@ -276,9 +293,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -276,9 +293,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int count = input.readInt(); int count = input.readInt();
int hashCode = 0; int hashCode = 0;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
CachedContent cachedContent = CachedContent.readFromStream(version, input); CachedContent cachedContent = readCachedContent(version, input);
add(cachedContent); add(cachedContent);
hashCode += cachedContent.headerHashCode(version); hashCode += hashCachedContent(cachedContent, version);
} }
int fileHashCode = input.readInt(); int fileHashCode = input.readInt();
boolean isEOF = input.read() == -1; boolean isEOF = input.read() == -1;
...@@ -327,8 +344,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -327,8 +344,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
output.writeInt(keyToContent.size()); output.writeInt(keyToContent.size());
int hashCode = 0; int hashCode = 0;
for (CachedContent cachedContent : keyToContent.values()) { for (CachedContent cachedContent : keyToContent.values()) {
cachedContent.writeToStream(output); writeCachedContent(cachedContent, output);
hashCode += cachedContent.headerHashCode(VERSION); hashCode += hashCachedContent(cachedContent, VERSION);
} }
output.writeInt(hashCode); output.writeInt(hashCode);
atomicFile.endWrite(output); atomicFile.endWrite(output);
...@@ -342,17 +359,108 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -342,17 +359,108 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} }
} }
private CachedContent addNew(String key) { /**
int id = getNewId(idToKey); * Calculates a hash code for a {@link CachedContent} which is compatible with a particular index
CachedContent cachedContent = new CachedContent(id, key); * version.
add(cachedContent); */
changed = true; private int hashCachedContent(CachedContent cachedContent, int version) {
return cachedContent; int result = cachedContent.id;
result = 31 * result + cachedContent.key.hashCode();
if (version < VERSION_METADATA_INTRODUCED) {
long length = ContentMetadata.getContentLength(cachedContent.getMetadata());
result = 31 * result + (int) (length ^ (length >>> 32));
} else {
result = 31 * result + cachedContent.getMetadata().hashCode();
}
return result;
} }
private void add(CachedContent cachedContent) { /**
keyToContent.put(cachedContent.key, cachedContent); * Reads a {@link CachedContent} from a {@link DataInputStream}.
idToKey.put(cachedContent.id, cachedContent.key); *
* @param version Version of the encoded data.
* @param input Input stream containing values needed to initialize CachedContent instance.
* @throws IOException If an error occurs during reading values.
*/
private static CachedContent readCachedContent(int version, DataInputStream input)
throws IOException {
int id = input.readInt();
String key = input.readUTF();
DefaultContentMetadata metadata;
if (version < VERSION_METADATA_INTRODUCED) {
long length = input.readLong();
ContentMetadataMutations mutations = new ContentMetadataMutations();
ContentMetadataMutations.setContentLength(mutations, length);
metadata = DefaultContentMetadata.EMPTY.copyWithMutationsApplied(mutations);
} else {
metadata = readContentMetadata(input);
}
return new CachedContent(id, key, metadata);
}
/**
* Writes a {@link CachedContent} to a {@link DataOutputStream}.
*
* @param output Output stream to store the values.
* @throws IOException If an error occurs during writing values to output.
*/
private static void writeCachedContent(CachedContent cachedContent, DataOutputStream output)
throws IOException {
output.writeInt(cachedContent.id);
output.writeUTF(cachedContent.key);
writeContentMetadata(cachedContent.getMetadata(), output);
}
/**
* Deserializes a {@link DefaultContentMetadata} from the given input stream.
*
* @param input Input stream to read from.
* @return a {@link DefaultContentMetadata} instance.
* @throws IOException If an error occurs during reading from input.
*/
private static DefaultContentMetadata readContentMetadata(DataInputStream input)
throws IOException {
int size = input.readInt();
HashMap<String, byte[]> metadata = new HashMap<>();
for (int i = 0; i < size; i++) {
String name = input.readUTF();
int valueSize = input.readInt();
if (valueSize < 0) {
throw new IOException("Invalid value size: " + valueSize);
}
// Grow the array incrementally to avoid OutOfMemoryError in the case that a corrupt (and very
// large) valueSize was read. In such cases the implementation below is expected to throw
// IOException from one of the readFully calls, due to the end of the input being reached.
int bytesRead = 0;
int nextBytesToRead = Math.min(valueSize, INCREMENTAL_METADATA_READ_LENGTH);
byte[] value = Util.EMPTY_BYTE_ARRAY;
while (bytesRead != valueSize) {
value = Arrays.copyOf(value, bytesRead + nextBytesToRead);
input.readFully(value, bytesRead, nextBytesToRead);
bytesRead += nextBytesToRead;
nextBytesToRead = Math.min(valueSize - bytesRead, INCREMENTAL_METADATA_READ_LENGTH);
}
metadata.put(name, value);
}
return new DefaultContentMetadata(metadata);
}
/**
* Serializes itself to a {@link DataOutputStream}.
*
* @param output Output stream to store the values.
* @throws IOException If an error occurs during writing values to output.
*/
private static void writeContentMetadata(DefaultContentMetadata metadata, DataOutputStream output)
throws IOException {
Set<Map.Entry<String, byte[]>> entrySet = metadata.entrySet();
output.writeInt(entrySet.size());
for (Map.Entry<String, byte[]> entry : entrySet) {
output.writeUTF(entry.getKey());
byte[] value = entry.getValue();
output.writeInt(value.length);
output.write(value);
}
} }
private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
......
...@@ -17,9 +17,6 @@ package com.google.android.exoplayer2.upstream.cache; ...@@ -17,9 +17,6 @@ package com.google.android.exoplayer2.upstream.cache;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Arrays; import java.util.Arrays;
...@@ -28,6 +25,7 @@ import java.util.HashMap; ...@@ -28,6 +25,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
/** Default implementation of {@link ContentMetadata}. Values are stored as byte arrays. */ /** Default implementation of {@link ContentMetadata}. Values are stored as byte arrays. */
public final class DefaultContentMetadata implements ContentMetadata { public final class DefaultContentMetadata implements ContentMetadata {
...@@ -36,39 +34,16 @@ public final class DefaultContentMetadata implements ContentMetadata { ...@@ -36,39 +34,16 @@ public final class DefaultContentMetadata implements ContentMetadata {
public static final DefaultContentMetadata EMPTY = public static final DefaultContentMetadata EMPTY =
new DefaultContentMetadata(Collections.emptyMap()); new DefaultContentMetadata(Collections.emptyMap());
private static final int MAX_VALUE_LENGTH = 10 * 1024 * 1024;
private int hashCode; private int hashCode;
/**
* Deserializes a {@link DefaultContentMetadata} from the given input stream.
*
* @param input Input stream to read from.
* @return a {@link DefaultContentMetadata} instance.
* @throws IOException If an error occurs during reading from input.
*/
public static DefaultContentMetadata readFromStream(DataInputStream input) throws IOException {
int size = input.readInt();
HashMap<String, byte[]> metadata = new HashMap<>();
for (int i = 0; i < size; i++) {
String name = input.readUTF();
int valueSize = input.readInt();
if (valueSize < 0 || valueSize > MAX_VALUE_LENGTH) {
throw new IOException("Invalid value size: " + valueSize);
}
byte[] value = new byte[valueSize];
input.readFully(value);
metadata.put(name, value);
}
return new DefaultContentMetadata(metadata);
}
private final Map<String, byte[]> metadata; private final Map<String, byte[]> metadata;
public DefaultContentMetadata() { public DefaultContentMetadata() {
this(Collections.emptyMap()); this(Collections.emptyMap());
} }
private DefaultContentMetadata(Map<String, byte[]> metadata) { /** @param metadata The metadata entries in their raw byte array form. */
public DefaultContentMetadata(Map<String, byte[]> metadata) {
this.metadata = Collections.unmodifiableMap(metadata); this.metadata = Collections.unmodifiableMap(metadata);
} }
...@@ -84,20 +59,9 @@ public final class DefaultContentMetadata implements ContentMetadata { ...@@ -84,20 +59,9 @@ public final class DefaultContentMetadata implements ContentMetadata {
return new DefaultContentMetadata(mutatedMetadata); return new DefaultContentMetadata(mutatedMetadata);
} }
/** /** Returns the set of metadata entries in their raw byte array form. */
* Serializes itself to a {@link DataOutputStream}. public Set<Entry<String, byte[]>> entrySet() {
* return metadata.entrySet();
* @param output Output stream to store the values.
* @throws IOException If an error occurs during writing values to output.
*/
public void writeToStream(DataOutputStream output) throws IOException {
output.writeInt(metadata.size());
for (Entry<String, byte[]> entry : metadata.entrySet()) {
output.writeUTF(entry.getKey());
byte[] value = entry.getValue();
output.writeInt(value.length);
output.write(value);
}
} }
@Override @Override
...@@ -190,18 +154,7 @@ public final class DefaultContentMetadata implements ContentMetadata { ...@@ -190,18 +154,7 @@ public final class DefaultContentMetadata implements ContentMetadata {
private static void addValues(HashMap<String, byte[]> metadata, Map<String, Object> values) { private static void addValues(HashMap<String, byte[]> metadata, Map<String, Object> values) {
for (String name : values.keySet()) { for (String name : values.keySet()) {
Object value = values.get(name); metadata.put(name, getBytes(values.get(name)));
byte[] bytes = getBytes(value);
if (bytes.length > MAX_VALUE_LENGTH) {
throw new IllegalArgumentException(
"The size of "
+ name
+ " ("
+ bytes.length
+ ") is greater than maximum allowed: "
+ MAX_VALUE_LENGTH);
}
metadata.put(name, bytes);
} }
} }
......
...@@ -17,10 +17,6 @@ package com.google.android.exoplayer2.upstream.cache; ...@@ -17,10 +17,6 @@ package com.google.android.exoplayer2.upstream.cache;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -134,24 +130,6 @@ public class DefaultContentMetadataTest { ...@@ -134,24 +130,6 @@ public class DefaultContentMetadataTest {
} }
@Test @Test
public void testSerializeDeserialize() throws Exception {
byte[] metadata3 = {1, 2, 3};
contentMetadata =
createContentMetadata(
"metadata1 name", "value", "metadata2 name", 12345, "metadata3 name", metadata3);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
contentMetadata.writeToStream(new DataOutputStream(outputStream));
ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
DefaultContentMetadata contentMetadata2 =
DefaultContentMetadata.readFromStream(new DataInputStream(inputStream));
assertThat(contentMetadata2.get("metadata1 name", "default value")).isEqualTo("value");
assertThat(contentMetadata2.get("metadata2 name", 0)).isEqualTo(12345);
assertThat(contentMetadata2.get("metadata3 name", new byte[] {})).isEqualTo(metadata3);
}
@Test
public void testEqualsStringValues() throws Exception { public void testEqualsStringValues() throws Exception {
DefaultContentMetadata metadata1 = createContentMetadata("metadata1", "value"); DefaultContentMetadata metadata1 = createContentMetadata("metadata1", "value");
DefaultContentMetadata metadata2 = createContentMetadata("metadata1", "value"); DefaultContentMetadata metadata2 = createContentMetadata("metadata1", "value");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment