out = store.get(TEST_KEY, TestProto.Basic.parser());
+ try {
+ Futures.getChecked(out, InvalidProtocolBufferException.class);
+ fail();
+ } catch (InvalidProtocolBufferException e) {
+ // expected
+ assertThat(e).hasMessageThat().contains("invalid wire type");
+ }
+ }
+ }
+
+ @Test
+ public void whenCache_returnsDefaultOnParseFailure() throws Exception {
+ try (SimpleStore simpleStore = SimpleProtoStoreFactory.create(context, "")) {
+ simpleStore.put(TEST_KEY, "garbage".getBytes(Charset.defaultCharset())).get();
+ }
+
+ try (SimpleProtoStore store = SimpleProtoStoreFactory.create(context, "", ScopeConfig.CACHE)) {
+ TestProto.Basic failed = store.get(TEST_KEY, TestProto.Basic.parser()).get();
+ assertThat(failed).isEqualTo(TestProto.Basic.getDefaultInstance());
+ }
+ }
+}
diff --git a/protosimplestore/src/test/proto/TestProto.proto b/protosimplestore/src/test/proto/TestProto.proto
new file mode 100644
index 0000000..cebc06f
--- /dev/null
+++ b/protosimplestore/src/test/proto/TestProto.proto
@@ -0,0 +1,16 @@
+syntax = "proto2";
+
+package uber.simplestore;
+
+option java_package = "com.uber.simplestore.proto.test";
+option java_outer_classname = "TestProto";
+
+message Basic {
+ optional string name = 1;
+}
+
+message Required {
+ optional string option = 1;
+ required string withDefault = 2 [default = "default"];
+ required string noDefault = 3;
+}
\ No newline at end of file
diff --git a/sample/build.gradle b/sample/build.gradle
index 8081281..236deea 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -43,6 +43,8 @@ dependencies {
implementation 'com.google.guava:guava:27.0.1-android'
implementation project(":simplestore")
implementation project(":protosimplestore")
+ annotationProcessor "com.uber.nullaway:nullaway:0.6.4"
+ testAnnotationProcessor "com.uber.nullaway:nullaway:0.6.4"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
diff --git a/simplestore/build.gradle b/simplestore/build.gradle
index 7481ea4..2af35bd 100644
--- a/simplestore/build.gradle
+++ b/simplestore/build.gradle
@@ -39,6 +39,8 @@ dependencies {
implementation 'com.google.code.findbugs:jsr305:3.0.2'
implementation 'com.google.guava:guava:27.0.1-android'
+ annotationProcessor "com.uber.nullaway:nullaway:0.6.4"
+ testAnnotationProcessor "com.uber.nullaway:nullaway:0.6.4"
testImplementation "com.google.truth:truth:0.42"
testImplementation "org.robolectric:robolectric:4.1"
testImplementation 'junit:junit:4.12'
diff --git a/simplestore/src/main/java/com/uber/simplestore/ScopeConfig.java b/simplestore/src/main/java/com/uber/simplestore/ScopeConfig.java
index 4854e61..3905563 100644
--- a/simplestore/src/main/java/com/uber/simplestore/ScopeConfig.java
+++ b/simplestore/src/main/java/com/uber/simplestore/ScopeConfig.java
@@ -5,7 +5,11 @@ public final class ScopeConfig {
/** No-op currently. */
public static final ScopeConfig CRITICAL = new ScopeConfig();
- /** Use the cache directory. */
+ /**
+ * Use the cache directory.
+ *
+ * Hides errors due to data corruption by returning a miss.
+ */
public static final ScopeConfig CACHE = new ScopeConfig();
/** Default settings. */
diff --git a/simplestore/src/main/java/com/uber/simplestore/SimpleStore.java b/simplestore/src/main/java/com/uber/simplestore/SimpleStore.java
index abee875..017529f 100644
--- a/simplestore/src/main/java/com/uber/simplestore/SimpleStore.java
+++ b/simplestore/src/main/java/com/uber/simplestore/SimpleStore.java
@@ -42,10 +42,20 @@ public interface SimpleStore extends Closeable {
@CheckReturnValue
ListenableFuture put(String key, @Nullable byte[] value);
+ /**
+ * Determine if a key exists in storage.
+ *
+ * @param key to check
+ * @return if key is set
+ */
+ @CheckReturnValue
+ ListenableFuture contains(String key);
+
/** Delete all keys in this direct scope. */
@CheckReturnValue
ListenableFuture deleteAll();
/** Fails all outstanding operations then releases the memory cache. */
+ @Override
void close();
}
diff --git a/simplestore/src/main/java/com/uber/simplestore/SimpleStoreConfig.java b/simplestore/src/main/java/com/uber/simplestore/SimpleStoreConfig.java
index 0a0d9d8..77f6c6b 100644
--- a/simplestore/src/main/java/com/uber/simplestore/SimpleStoreConfig.java
+++ b/simplestore/src/main/java/com/uber/simplestore/SimpleStoreConfig.java
@@ -13,9 +13,9 @@ public final class SimpleStoreConfig {
private static final Object writeLock = new Object();
- @Nullable private static Executor ioExecutor;
+ @Nullable private static volatile Executor ioExecutor;
- @Nullable private static Executor computationExecutor;
+ @Nullable private static volatile Executor computationExecutor;
public static Executor getIOExecutor() {
if (ioExecutor == null) {
diff --git a/simplestore/src/main/java/com/uber/simplestore/impl/SimpleStoreImpl.java b/simplestore/src/main/java/com/uber/simplestore/impl/SimpleStoreImpl.java
index 7dc2a2a..c0c5877 100644
--- a/simplestore/src/main/java/com/uber/simplestore/impl/SimpleStoreImpl.java
+++ b/simplestore/src/main/java/com/uber/simplestore/impl/SimpleStoreImpl.java
@@ -9,6 +9,7 @@
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@@ -21,6 +22,7 @@ final class SimpleStoreImpl implements SimpleStore {
private static final int OPEN = 0;
private static final int CLOSED = 1;
private static final int TOMBSTONED = 2;
+ private static final byte[] EMPTY_BYTES = new byte[0];
private final Context context;
private final String scope;
@@ -56,7 +58,7 @@ public ListenableFuture getString(String key) {
get(key),
(bytes) -> {
if (bytes != null && bytes.length > 0) {
- return new String(bytes);
+ return new String(bytes, Charset.defaultCharset());
} else {
return null;
}
@@ -65,8 +67,8 @@ public ListenableFuture getString(String key) {
}
@Override
- public ListenableFuture putString(String key, String value) {
- byte[] bytes = value != null ? value.getBytes() : null;
+ public ListenableFuture putString(String key, @Nullable String value) {
+ byte[] bytes = value != null ? value.getBytes(Charset.defaultCharset()) : null;
return Futures.transform(put(key, bytes), (b) -> value, MoreExecutors.directExecutor());
}
@@ -87,11 +89,10 @@ public ListenableFuture get(String key) {
} catch (IOException e) {
return Futures.immediateFailedFuture(e);
}
- if (value == null) {
- cache.remove(key);
- } else {
- cache.put(key, value);
+ if (value == null || value.length == 0) {
+ value = EMPTY_BYTES;
}
+ cache.put(key, value);
}
return Futures.immediateFuture(value);
},
@@ -106,9 +107,10 @@ public ListenableFuture put(String key, @Nullable byte[] value) {
if (isClosed()) {
return Futures.immediateFailedFuture(new StoreClosedException());
}
- if (value == null) {
- cache.remove(key);
+ if (value == null || value.length == 0) {
+ cache.put(key, EMPTY_BYTES);
deleteFile(key);
+ return Futures.immediateFuture(EMPTY_BYTES);
} else {
cache.put(key, value);
try {
@@ -116,12 +118,21 @@ public ListenableFuture put(String key, @Nullable byte[] value) {
} catch (IOException e) {
return Futures.immediateFailedFuture(e);
}
+ return Futures.immediateFuture(value);
}
- return Futures.immediateFuture(value);
},
orderedIoExecutor);
}
+ @Override
+ public ListenableFuture contains(String key) {
+ requireOpen();
+ return Futures.transform(
+ get(key),
+ (value) -> value != null && value.length > 0,
+ SimpleStoreConfig.getComputationExecutor());
+ }
+
@Override
public ListenableFuture deleteAll() {
requireOpen();
@@ -184,6 +195,7 @@ private void deleteFile(String key) {
file.delete();
}
+ @Nullable
private byte[] readFile(String key) throws IOException {
File baseFile = new File(scopedDirectory, key);
AtomicFile file = new AtomicFile(baseFile);
diff --git a/simplestore/src/test/java/com/uber/simplestore/impl/SimpleStoreImplTest.java b/simplestore/src/test/java/com/uber/simplestore/impl/SimpleStoreImplTest.java
index 3ae6ccb..c8a9335 100644
--- a/simplestore/src/test/java/com/uber/simplestore/impl/SimpleStoreImplTest.java
+++ b/simplestore/src/test/java/com/uber/simplestore/impl/SimpleStoreImplTest.java
@@ -32,10 +32,10 @@ public void reset() {
}
@Test
- public void nullWhenMissing() throws Exception {
+ public void zeroLengthWhenMissing() throws Exception {
try (SimpleStore store = SimpleStoreFactory.create(context, "")) {
ListenableFuture future = store.get(TEST_KEY);
- assertThat(future.get()).isNull();
+ assertThat(future.get()).hasLength(0);
}
}
@@ -44,7 +44,7 @@ public void puttingNullDeletesKey() throws Exception {
try (SimpleStore store = SimpleStoreFactory.create(context, "")) {
ListenableFuture first = store.put(TEST_KEY, new byte[1]);
ListenableFuture second = store.put(TEST_KEY, null);
- assertThat(second.get()).isNull();
+ assertThat(second.get()).isEmpty();
}
}
@@ -54,7 +54,7 @@ public void deleteAll() throws Exception {
ListenableFuture first = store.put(TEST_KEY, new byte[1]);
ListenableFuture second = store.deleteAll();
ListenableFuture empty = store.get(TEST_KEY);
- assertThat(empty.get()).isNull();
+ assertThat(empty.get()).isEmpty();
}
}