Skip to content
Browse files

Type mapping now works for subclasses

  • Loading branch information...
1 parent a5b317c commit 98664c1d169b1e4b5e6d9716f1903cc164eb0751 @artem-zinnatullin artem-zinnatullin committed
Showing with 873 additions and 51 deletions.
  1. +1 −2 README.md
  2. +2 −15 build.gradle
  3. +1 −1 checkstyle/checkstyle.xml
  4. +0 −1 docs/StorIOSQLite.md
  5. +4 −0 storio-content-resolver/build.gradle
  6. +69 −9 ...ver/src/main/java/com/pushtorefresh/storio/contentresolver/impl/DefaultStorIOContentResolver.java
  7. +1 −0 ...io-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/query/DeleteQuery.java
  8. +1 −0 ...io-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/query/InsertQuery.java
  9. +1 −0 storio-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/query/Query.java
  10. +1 −0 ...io-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/query/UpdateQuery.java
  11. +223 −0 ...src/test/java/com/pushtorefresh/storio/contentresolver/impl/DefaultStorIOContentResolverTest.java
  12. +1 −1 ...ontent-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/query/DeleteQueryTest.java
  13. +1 −1 ...ontent-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/query/InsertQueryTest.java
  14. +1 −1 storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/query/QueryTest.java
  15. +1 −1 ...ontent-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/query/UpdateQueryTest.java
  16. +3 −0 storio-sqlite/build.gradle
  17. +4 −0 storio-sqlite/src/androidTest/java/com/pushtorefresh/storio/sqlite/impl/UserTableMeta.java
  18. +160 −0 ...-sqlite/src/androidTest/java/com/pushtorefresh/storio/sqlite/impl/auto_parcel/AutoParcelTest.java
  19. +38 −0 storio-sqlite/src/androidTest/java/com/pushtorefresh/storio/sqlite/impl/auto_parcel/Book.java
  20. +99 −0 ...o-sqlite/src/androidTest/java/com/pushtorefresh/storio/sqlite/impl/auto_parcel/BookTableMeta.java
  21. +23 −0 storio-sqlite/src/androidTest/java/com/pushtorefresh/storio/sqlite/impl/auto_parcel/OpenHelper.java
  22. +71 −9 storio-sqlite/src/main/java/com/pushtorefresh/storio/sqlite/impl/DefaultStorIOSQLite.java
  23. +167 −10 storio-sqlite/src/test/java/com/pushtorefresh/storio/sqlite/impl/DefaultStorIOSQLiteTest.java
View
3 README.md
@@ -7,7 +7,7 @@ Currently in development.
* API for Humans: Type Safety, Immutability & Thread-Safety
* Convenient builders with compile-time guarantees for required params. Forget about 6-7 `null` in queries
* Optional Type-Safe Object Mapping, if you don't want to work with `Cursor` and `ContentValues` you don't have to
-* No reflection and no annotations in core, also `StorIO` is not ORM
+* No reflection in Operations and no annotations in the core, also `StorIO` is not ORM
* Every Operation over `StorIO` can be executed as blocking call or as `rx.Observable`
* `RxJava` as first class citizen, but it's not required dependency!
* `rx.Observable` from `Get` Operation **can observe changes** in `StorIO` and receive updates automatically
@@ -129,7 +129,6 @@ StorIOSQLite storIOSQLite = new DefaultStorIOSQLite.Builder()
```
You can override Operation Resolver per each individual Operation, it can be useful for working with `SQL JOIN`.
-Also, as you can see, there is no Reflection, and no performance reduction in compare to manual object mapping code.
To **save you from coding boilerplate classes** we created **Annotation Processor** which will generate `PutResolver`, `GetResolver` and `DeleteResolver` at compile time, you just need to use generated classes
View
17 build.gradle
@@ -60,6 +60,8 @@ ext {
mockitoCore = 'org.mockito:mockito-core:1.10.19'
testingSupportLib = 'com.android.support.test:testing-support-lib:0.1'
guava = 'com.google.guava:guava:18.0'
+ autoParcel = 'com.github.frankiesardo:auto-parcel:0.3'
+ autoParcelProcessor = 'com.github.frankiesardo:auto-parcel-processor:0.3'
}
// Option to disable Pre-Dexing on CI env
@@ -81,19 +83,4 @@ subprojects {
project.android.compileOptions.targetCompatibility = JavaVersion.VERSION_1_6
}
}
-
- project.afterEvaluate {
- // StorIO Sample App uses Dagger's annotations and Java Compiler throws warning:
- // 'warning: No processor claimed any of these annotations suppress'
- // Unfortunately this warning can not be ignored
- // Please feel free to suggest workaround for this problem
- if (!'storio-sample-app'.equals(project.name)
- && !'storio-sqlite-annotation'.equals(project.name)
- && !'storio-sqlite-annotation-processor'.equals(project.name)) {
- tasks.withType(JavaCompile) {
- // treat all Java Compiler warnings as errors
- options.compilerArgs << "-Xlint:all" << "-Werror"
- }
- }
- }
}
View
2 checkstyle/checkstyle.xml
@@ -29,7 +29,7 @@
<module name="PackageName"/>
<module name="ParameterName"/>
<!--<module name="StaticVariableName"/>-->
- <module name="TypeName"/>
+ <!--<module name="TypeName"/>-->
<!-- Checks for imports -->
<!-- See http://checkstyle.sf.net/config_import.html -->
<module name="AvoidStarImport"/>
View
1 docs/StorIOSQLite.md
@@ -260,7 +260,6 @@ StorIOSQLite storIOSQLite = new DefaultStorIOSQLite.Builder()
```
You can override Operation Resolver per each individual Operation, it can be useful for working with `SQL JOIN`.
-Also, as you can see, there is no Reflection, and no performance reduction in compare to manual object mapping code.
To **save you from coding boilerplate classes** we created **Annotation Processor** which will generate `PutResolver`, `GetResolver` and `DeleteResolver` at compile time, you just need to use generated classes
View
4 storio-content-resolver/build.gradle
@@ -17,6 +17,10 @@ android {
packagingOptions {
exclude 'LICENSE.txt' // multiple libs have this file -> cause build error
}
+
+ testOptions {
+ unitTests.returnDefaultValues = true
+ }
}
dependencies {
View
78 ...main/java/com/pushtorefresh/storio/contentresolver/impl/DefaultStorIOContentResolver.java
@@ -20,10 +20,10 @@
import com.pushtorefresh.storio.contentresolver.query.Query;
import com.pushtorefresh.storio.contentresolver.query.UpdateQuery;
-import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import rx.Observable;
@@ -31,6 +31,7 @@
import static com.pushtorefresh.storio.internal.Environment.throwExceptionIfRxJavaIsNotAvailable;
import static com.pushtorefresh.storio.internal.Queries.nullableArrayOfStrings;
import static com.pushtorefresh.storio.internal.Queries.nullableString;
+import static java.util.Collections.unmodifiableMap;
/**
* Default, thread-safe implementation of {@link StorIOContentResolver}.
@@ -124,7 +125,7 @@ public CompleteBuilder contentResolver(@NonNull ContentResolver contentResolver)
* @return builder.
*/
@NonNull
- public <T> CompleteBuilder addTypeMapping(@NonNull Class<T> type, ContentResolverTypeMapping<T> typeMapping) {
+ public <T> CompleteBuilder addTypeMapping(@NonNull Class<T> type, @NonNull ContentResolverTypeMapping<T> typeMapping) {
checkNotNull(type, "Please specify type");
checkNotNull(typeMapping, "Please specify type mapping");
@@ -151,24 +152,83 @@ public DefaultStorIOContentResolver build() {
protected class InternalImpl extends Internal {
@Nullable
- private final Map<Class<?>, ContentResolverTypeMapping<?>> typesMapping;
+ private final Map<Class<?>, ContentResolverTypeMapping<?>> directTypesMapping;
+
+ @NonNull
+ private final Map<Class<?>, ContentResolverTypeMapping<?>> indirectTypesMappingCache
+ = new ConcurrentHashMap<Class<?>, ContentResolverTypeMapping<?>>();
protected InternalImpl(@Nullable Map<Class<?>, ContentResolverTypeMapping<?>> typesMapping) {
- this.typesMapping = typesMapping != null
- ? Collections.unmodifiableMap(typesMapping)
+ this.directTypesMapping = typesMapping != null
+ ? unmodifiableMap(typesMapping)
: null;
}
/**
- * {@inheritDoc}
+ * Gets type mapping for required type.
+ * <p/>
+ * This implementation can handle subclasses of types, that registered its type mapping.
+ * For example: You've added type mapping for {@code User.class},
+ * and you have {@code UserFromServiceA.class} which extends {@code User.class},
+ * and you didn't add type mapping for {@code UserFromServiceA.class}
+ * because they have same fields and you just want to have multiple classes.
+ * This implementation will find type mapping of {@code User.class}
+ * and use it as type mapping for {@code UserFromServiceA.class}.
+ *
+ * @return direct or indirect type mapping for passed type, or {@code null}.
*/
@SuppressWarnings("unchecked")
@Nullable
@Override
public <T> ContentResolverTypeMapping<T> typeMapping(@NonNull Class<T> type) {
- return typesMapping != null
- ? (ContentResolverTypeMapping<T>) typesMapping.get(type)
- : null;
+ if (directTypesMapping == null) {
+ return null;
+ }
+
+ final ContentResolverTypeMapping<T> directTypeMapping = (ContentResolverTypeMapping<T>) directTypesMapping.get(type);
+
+ if (directTypeMapping != null) {
+ // fffast! O(1)
+ return directTypeMapping;
+ } else {
+ // If no direct type mapping found — search for indirect type mapping
+
+ // May be value already in cache.
+ ContentResolverTypeMapping<T> indirectTypeMapping =
+ (ContentResolverTypeMapping<T>) indirectTypesMappingCache.get(type);
+
+ if (indirectTypeMapping != null) {
+ // fffast! O(1)
+ return indirectTypeMapping;
+ }
+
+ // Okay, we don't have direct type mapping.
+ // And we don't have cache for indirect type mapping.
+ // Let's find indirect type mapping and cache it!
+ Class<?> parentType = type.getSuperclass();
+
+ // Search algorithm:
+ // Walk through all parent types of passed type.
+ // If parent type has direct mapping -> we found indirect type mapping!
+ // If current parent type == Object.class -> there is no indirect type mapping.
+ // Complexity:
+ // O(n) where n is number of parent types of passed type (pretty fast).
+
+ // Stop search if root parent is Object.class
+ while (parentType != Object.class) {
+ indirectTypeMapping = (ContentResolverTypeMapping<T>) directTypesMapping.get(parentType);
+
+ if (indirectTypeMapping != null) {
+ indirectTypesMappingCache.put(type, indirectTypeMapping);
+ return indirectTypeMapping;
+ }
+
+ parentType = parentType.getSuperclass();
+ }
+
+ // No indirect type mapping found.
+ return null;
+ }
}
/**
View
1 ...nt-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/query/DeleteQuery.java
@@ -129,6 +129,7 @@ public CompleteBuilder uri(@NonNull Uri uri) {
*/
@NonNull
public CompleteBuilder uri(@NonNull String uri) {
+ checkNotNull(uri, "Uri should not be null");
return new CompleteBuilder(Uri.parse(uri));
}
}
View
1 ...nt-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/query/InsertQuery.java
@@ -85,6 +85,7 @@ public CompleteBuilder uri(@NonNull Uri uri) {
*/
@NonNull
public CompleteBuilder uri(@NonNull String uri) {
+ checkNotNull(uri, "Uri should not be null");
return new CompleteBuilder(Uri.parse(uri));
}
}
View
1 ...-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/query/Query.java
@@ -182,6 +182,7 @@ public CompleteBuilder uri(@NonNull Uri uri) {
*/
@NonNull
public CompleteBuilder uri(@NonNull String uri) {
+ checkNotNull(uri, "Uri should not be null");
return new CompleteBuilder(Uri.parse(uri));
}
}
View
1 ...nt-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/query/UpdateQuery.java
@@ -126,6 +126,7 @@ public CompleteBuilder uri(@NonNull Uri uri) {
*/
@NonNull
public CompleteBuilder uri(@NonNull String uri) {
+ checkNotNull(uri, "Uri should not be null");
return new CompleteBuilder(Uri.parse(uri));
}
}
View
223 .../java/com/pushtorefresh/storio/contentresolver/impl/DefaultStorIOContentResolverTest.java
@@ -0,0 +1,223 @@
+package com.pushtorefresh.storio.contentresolver.impl;
+
+import android.content.ContentResolver;
+
+import com.pushtorefresh.storio.contentresolver.ContentResolverTypeMapping;
+import com.pushtorefresh.storio.contentresolver.StorIOContentResolver;
+import com.pushtorefresh.storio.contentresolver.operation.delete.DeleteResolver;
+import com.pushtorefresh.storio.contentresolver.operation.get.GetResolver;
+import com.pushtorefresh.storio.contentresolver.operation.put.PutResolver;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+
+public class DefaultStorIOContentResolverTest {
+
+ @SuppressWarnings("ConstantConditions")
+ @Test(expected = NullPointerException.class)
+ public void nullContentResolver() {
+ new DefaultStorIOContentResolver.Builder()
+ .contentResolver(null);
+ }
+
+ @SuppressWarnings({"unchecked", "ConstantConditions"})
+ @Test(expected = NullPointerException.class)
+ public void addTypeMappingNullType() {
+ new DefaultStorIOContentResolver.Builder()
+ .contentResolver(mock(ContentResolver.class))
+ .addTypeMapping(null, new ContentResolverTypeMapping.Builder<Object>()
+ .putResolver(mock(PutResolver.class))
+ .getResolver(mock(GetResolver.class))
+ .deleteResolver(mock(DeleteResolver.class))
+ .build());
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ @Test(expected = NullPointerException.class)
+ public void addTypeMappingNullMapping() {
+ new DefaultStorIOContentResolver.Builder()
+ .contentResolver(mock(ContentResolver.class))
+ .addTypeMapping(Object.class, null);
+ }
+
+ @Test
+ public void shouldReturnNullIfNoTypeMappingsRegistered() {
+ class TestItem {
+
+ }
+
+ final StorIOContentResolver storIOContentResolver = new DefaultStorIOContentResolver.Builder()
+ .contentResolver(mock(ContentResolver.class))
+ .build();
+
+ assertNull(storIOContentResolver.internal().typeMapping(TestItem.class));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void shouldReturnNullIfNotTypeMappingRegisteredForType() {
+ class TestItem {
+
+ }
+
+ class Entity {
+
+ }
+
+ final ContentResolverTypeMapping<Entity> entityContentResolverTypeMapping = new ContentResolverTypeMapping.Builder<Entity>()
+ .putResolver(mock(PutResolver.class))
+ .getResolver(mock(GetResolver.class))
+ .deleteResolver(mock(DeleteResolver.class))
+ .build();
+
+ final StorIOContentResolver storIOContentResolver = new DefaultStorIOContentResolver.Builder()
+ .contentResolver(mock(ContentResolver.class))
+ .addTypeMapping(Entity.class, entityContentResolverTypeMapping)
+ .build();
+
+ assertSame(entityContentResolverTypeMapping, storIOContentResolver.internal().typeMapping(Entity.class));
+
+ assertNull(storIOContentResolver.internal().typeMapping(TestItem.class));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void directTypeMappingShouldWork() {
+ class TestItem {
+
+ }
+
+ final ContentResolverTypeMapping<TestItem> typeMapping = new ContentResolverTypeMapping.Builder<TestItem>()
+ .putResolver(mock(PutResolver.class))
+ .getResolver(mock(GetResolver.class))
+ .deleteResolver(mock(DeleteResolver.class))
+ .build();
+
+ final StorIOContentResolver storIOContentResolver = new DefaultStorIOContentResolver.Builder()
+ .contentResolver(mock(ContentResolver.class))
+ .addTypeMapping(TestItem.class, typeMapping)
+ .build();
+
+ assertSame(typeMapping, storIOContentResolver.internal().typeMapping(TestItem.class));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void indirectTypeMappingShouldWork() {
+ class TestItem {
+
+ }
+
+ final ContentResolverTypeMapping<TestItem> typeMapping = new ContentResolverTypeMapping.Builder<TestItem>()
+ .putResolver(mock(PutResolver.class))
+ .getResolver(mock(GetResolver.class))
+ .deleteResolver(mock(DeleteResolver.class))
+ .build();
+
+ final StorIOContentResolver storIOContentResolver = new DefaultStorIOContentResolver.Builder()
+ .contentResolver(mock(ContentResolver.class))
+ .addTypeMapping(TestItem.class, typeMapping)
+ .build();
+
+ class TestItemSubclass extends TestItem {
+
+ }
+
+ // Direct type mapping should still work
+ assertSame(typeMapping, storIOContentResolver.internal().typeMapping(TestItem.class));
+
+ // Indirect type mapping should give same type mapping as for parent class
+ assertSame(typeMapping, storIOContentResolver.internal().typeMapping(TestItemSubclass.class));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void typeMappingShouldWorkInCaseOfMoreConcreteTypeMapping() {
+ class TestItem {
+
+ }
+
+ final ContentResolverTypeMapping<TestItem> typeMapping = new ContentResolverTypeMapping.Builder<TestItem>()
+ .putResolver(mock(PutResolver.class))
+ .getResolver(mock(GetResolver.class))
+ .deleteResolver(mock(DeleteResolver.class))
+ .build();
+
+ class TestItemSubclass extends TestItem {
+
+ }
+
+ final ContentResolverTypeMapping<TestItemSubclass> subclassTypeMapping = new ContentResolverTypeMapping.Builder<TestItemSubclass>()
+ .putResolver(mock(PutResolver.class))
+ .getResolver(mock(GetResolver.class))
+ .deleteResolver(mock(DeleteResolver.class))
+ .build();
+
+ final StorIOContentResolver storIOContentResolver = new DefaultStorIOContentResolver.Builder()
+ .contentResolver(mock(ContentResolver.class))
+ .addTypeMapping(TestItem.class, typeMapping)
+ .addTypeMapping(TestItemSubclass.class, subclassTypeMapping)
+ .build();
+
+ // Parent class should have its own type mapping
+ assertSame(typeMapping, storIOContentResolver.internal().typeMapping(TestItem.class));
+
+ // Child class should have its own type mapping
+ assertSame(subclassTypeMapping, storIOContentResolver.internal().typeMapping(TestItemSubclass.class));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void typeMappingShouldFindIndirectTypeMappingInCaseOfComplexInheritance() {
+ // Good test case — inheritance with AutoValue/AutoParcel
+
+ class Entity {
+
+ }
+
+ class AutoValue_Entity extends Entity {
+
+ }
+
+ class ConcreteEntity extends Entity {
+
+ }
+
+ class AutoValue_ConcreteEntity extends ConcreteEntity {
+
+ }
+
+ final ContentResolverTypeMapping<Entity> entitySQLiteTypeMapping = new ContentResolverTypeMapping.Builder<Entity>()
+ .putResolver(mock(PutResolver.class))
+ .getResolver(mock(GetResolver.class))
+ .deleteResolver(mock(DeleteResolver.class))
+ .build();
+
+ final ContentResolverTypeMapping<ConcreteEntity> concreteEntitySQLiteTypeMapping = new ContentResolverTypeMapping.Builder<ConcreteEntity>()
+ .putResolver(mock(PutResolver.class))
+ .getResolver(mock(GetResolver.class))
+ .deleteResolver(mock(DeleteResolver.class))
+ .build();
+
+ final StorIOContentResolver storIOContentResolver = new DefaultStorIOContentResolver.Builder()
+ .contentResolver(mock(ContentResolver.class))
+ .addTypeMapping(Entity.class, entitySQLiteTypeMapping)
+ .addTypeMapping(ConcreteEntity.class, concreteEntitySQLiteTypeMapping)
+ .build();
+
+ // Direct type mapping for Entity should work
+ assertSame(entitySQLiteTypeMapping, storIOContentResolver.internal().typeMapping(Entity.class));
+
+ // Direct type mapping for ConcreteEntity should work
+ assertSame(concreteEntitySQLiteTypeMapping, storIOContentResolver.internal().typeMapping(ConcreteEntity.class));
+
+ // Indirect type mapping for AutoValue_Entity should get type mapping for Entity
+ assertSame(entitySQLiteTypeMapping, storIOContentResolver.internal().typeMapping(AutoValue_Entity.class));
+
+ // Indirect type mapping for AutoValue_ConcreteEntity should get type mapping for ConcreteEntity, not for Entity!
+ assertSame(concreteEntitySQLiteTypeMapping, storIOContentResolver.internal().typeMapping(AutoValue_ConcreteEntity.class));
+ }
+}
View
2 ...esolver/src/test/java/com/pushtorefresh/storio/contentresolver/query/DeleteQueryTest.java
@@ -22,7 +22,7 @@ public void shouldNotAllowNullUriObject() {
}
@SuppressWarnings("ConstantConditions")
- @Test(expected = RuntimeException.class) // Uri#parse() not mocked
+ @Test(expected = NullPointerException.class) // Uri#parse() not mocked
public void shouldNotAllowNullUriString() {
new DeleteQuery.Builder()
.uri((String) null)
View
2 ...esolver/src/test/java/com/pushtorefresh/storio/contentresolver/query/InsertQueryTest.java
@@ -18,7 +18,7 @@ public void shouldNotAllowNullUriObject() {
}
@SuppressWarnings("ConstantConditions")
- @Test(expected = RuntimeException.class)
+ @Test(expected = NullPointerException.class)
public void shouldNotAllowNullUriString() {
new InsertQuery.Builder()
.uri((String) null)
View
2 ...tent-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/query/QueryTest.java
@@ -21,7 +21,7 @@ public void shouldNotAllowNullUriObject() {
}
@SuppressWarnings("ConstantConditions")
- @Test(expected = RuntimeException.class)
+ @Test(expected = NullPointerException.class)
public void shouldNotAllowNullUriString() {
new Query.Builder()
.uri((String) null)
View
2 ...esolver/src/test/java/com/pushtorefresh/storio/contentresolver/query/UpdateQueryTest.java
@@ -21,7 +21,7 @@ public void shouldNotAllowNullUriObject() {
}
@SuppressWarnings("ConstantConditions")
- @Test(expected = RuntimeException.class)
+ @Test(expected = NullPointerException.class)
public void shouldNotAllowNullUriString() {
new UpdateQuery.Builder()
.uri((String) null)
View
3 storio-sqlite/build.gradle
@@ -1,6 +1,7 @@
apply plugin: 'android-sdk-manager'
apply plugin: 'com.android.library'
apply plugin: 'checkstyle'
+apply plugin: 'com.neenbedankt.android-apt'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
@@ -32,6 +33,8 @@ dependencies {
androidTestCompile project(':storio-test-common')
androidTestCompile rootProject.ext.testingSupportLib
+ androidTestCompile rootProject.ext.autoParcel
+ androidTestApt rootProject.ext.autoParcelProcessor
}
task checkstyle(type: Checkstyle) {
View
4 storio-sqlite/src/androidTest/java/com/pushtorefresh/storio/sqlite/impl/UserTableMeta.java
@@ -23,17 +23,21 @@
static final String TABLE = "users";
static final String COLUMN_ID = "_id";
static final String COLUMN_EMAIL = "email";
+
// We all will be very old when Java will support string interpolation :(
static final String SQL_CREATE_TABLE = "CREATE TABLE " + TABLE + "(" +
COLUMN_ID + " INTEGER PRIMARY KEY, " +
COLUMN_EMAIL + " TEXT NOT NULL" +
");";
+
static final Query QUERY_ALL = new Query.Builder()
.table(TABLE)
.build();
+
static final DeleteQuery DELETE_QUERY_ALL = new DeleteQuery.Builder()
.table(TABLE)
.build();
+
static final PutResolver<User> PUT_RESOLVER = new DefaultPutResolver<User>() {
@NonNull
@Override
View
160 ...src/androidTest/java/com/pushtorefresh/storio/sqlite/impl/auto_parcel/AutoParcelTest.java
@@ -0,0 +1,160 @@
+package com.pushtorefresh.storio.sqlite.impl.auto_parcel;
+
+import android.support.annotation.NonNull;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping;
+import com.pushtorefresh.storio.sqlite.StorIOSQLite;
+import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite;
+import com.pushtorefresh.storio.sqlite.operation.delete.DeleteResult;
+import com.pushtorefresh.storio.sqlite.operation.put.PutResult;
+import com.pushtorefresh.storio.sqlite.query.DeleteQuery;
+import com.pushtorefresh.storio.sqlite.query.Query;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(AndroidJUnit4.class)
+public class AutoParcelTest {
+
+ @NonNull // Initialized in @Before
+ private StorIOSQLite storIOSQLite;
+
+ @Before
+ public void setUp() {
+ storIOSQLite = new DefaultStorIOSQLite.Builder()
+ .sqliteOpenHelper(new OpenHelper(InstrumentationRegistry.getContext()))
+ .addTypeMapping(Book.class, new SQLiteTypeMapping.Builder<Book>()
+ .putResolver(BookTableMeta.PUT_RESOLVER)
+ .getResolver(BookTableMeta.GET_RESOLVER)
+ .deleteResolver(BookTableMeta.DELETE_RESOLVER)
+ .build())
+ .build();
+
+ // Clearing books table before each test case
+ storIOSQLite
+ .delete()
+ .byQuery(new DeleteQuery.Builder()
+ .table(BookTableMeta.TABLE)
+ .build())
+ .prepare()
+ .executeAsBlocking();
+ }
+
+ @Test
+ public void insertObject() {
+ final Book book = Book.builder()
+ .id(1)
+ .title("What a great book")
+ .author("Somebody")
+ .build();
+
+ final PutResult putResult = storIOSQLite
+ .put()
+ .object(book)
+ .prepare()
+ .executeAsBlocking();
+
+ assertTrue(putResult.wasInserted());
+
+ final List<Book> storedBooks = storIOSQLite
+ .get()
+ .listOfObjects(Book.class)
+ .withQuery(new Query.Builder()
+ .table(BookTableMeta.TABLE)
+ .build())
+ .prepare()
+ .executeAsBlocking();
+
+ assertEquals(1, storedBooks.size());
+
+ assertEquals(book, storedBooks.get(0));
+ }
+
+ @Test
+ public void updateObject() {
+ final Book book = Book.builder()
+ .id(1)
+ .title("What a great book")
+ .author("Somebody")
+ .build();
+
+ final PutResult putResult1 = storIOSQLite
+ .put()
+ .object(book)
+ .prepare()
+ .executeAsBlocking();
+
+ assertTrue(putResult1.wasInserted());
+
+ final Book bookWithUpdatedInfo = Book.builder()
+ .id(1) // Same id, should be updated
+ .title("Corrected title")
+ .author("Corrected author")
+ .build();
+
+ final PutResult putResult2 = storIOSQLite
+ .put()
+ .object(bookWithUpdatedInfo)
+ .prepare()
+ .executeAsBlocking();
+
+ assertTrue(putResult2.wasUpdated());
+
+ final List<Book> storedBooks = storIOSQLite
+ .get()
+ .listOfObjects(Book.class)
+ .withQuery(new Query.Builder()
+ .table(BookTableMeta.TABLE)
+ .build())
+ .prepare()
+ .executeAsBlocking();
+
+ assertEquals(1, storedBooks.size());
+
+ assertEquals(bookWithUpdatedInfo, storedBooks.get(0));
+ }
+
+ @Test
+ public void deleteObject() {
+ final Book book = Book.builder()
+ .id(1)
+ .title("What a great book")
+ .author("Somebody")
+ .build();
+
+ final PutResult putResult = storIOSQLite
+ .put()
+ .object(book)
+ .prepare()
+ .executeAsBlocking();
+
+ assertTrue(putResult.wasInserted());
+
+ final DeleteResult deleteResult = storIOSQLite
+ .delete()
+ .object(book)
+ .prepare()
+ .executeAsBlocking();
+
+ assertEquals(1, deleteResult.numberOfRowsDeleted());
+
+ final List<Book> storedBooks = storIOSQLite
+ .get()
+ .listOfObjects(Book.class)
+ .withQuery(new Query.Builder()
+ .table(BookTableMeta.TABLE)
+ .build())
+ .prepare()
+ .executeAsBlocking();
+
+ assertEquals(0, storedBooks.size());
+ }
+}
View
38 ...io-sqlite/src/androidTest/java/com/pushtorefresh/storio/sqlite/impl/auto_parcel/Book.java
@@ -0,0 +1,38 @@
+package com.pushtorefresh.storio.sqlite.impl.auto_parcel;
+
+import android.support.annotation.NonNull;
+
+import auto.parcel.AutoParcel;
+
+@AutoParcel
+abstract class Book {
+
+ abstract int id();
+
+ @NonNull
+ abstract String title();
+
+ @NonNull
+ abstract String author();
+
+ @NonNull
+ static Builder builder() {
+ return new AutoParcel_Book.Builder();
+ }
+
+ @AutoParcel.Builder
+ abstract static class Builder {
+
+ @NonNull
+ abstract Builder id(int id);
+
+ @NonNull
+ abstract Builder title(@NonNull String title);
+
+ @NonNull
+ abstract Builder author(@NonNull String author);
+
+ @NonNull
+ abstract Book build();
+ }
+}
View
99 .../src/androidTest/java/com/pushtorefresh/storio/sqlite/impl/auto_parcel/BookTableMeta.java
@@ -0,0 +1,99 @@
+package com.pushtorefresh.storio.sqlite.impl.auto_parcel;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.support.annotation.NonNull;
+
+import com.pushtorefresh.storio.sqlite.operation.delete.DefaultDeleteResolver;
+import com.pushtorefresh.storio.sqlite.operation.delete.DeleteResolver;
+import com.pushtorefresh.storio.sqlite.operation.get.DefaultGetResolver;
+import com.pushtorefresh.storio.sqlite.operation.get.GetResolver;
+import com.pushtorefresh.storio.sqlite.operation.put.DefaultPutResolver;
+import com.pushtorefresh.storio.sqlite.operation.put.PutResolver;
+import com.pushtorefresh.storio.sqlite.query.DeleteQuery;
+import com.pushtorefresh.storio.sqlite.query.InsertQuery;
+import com.pushtorefresh.storio.sqlite.query.UpdateQuery;
+
+final class BookTableMeta {
+
+ private BookTableMeta() {
+ throw new IllegalStateException("No instances please.");
+ }
+
+ @NonNull
+ static final String TABLE = "books";
+
+ @NonNull
+ static final String COLUMN_ID = "_id";
+
+ @NonNull
+ static final String COLUMN_TITLE = "title";
+
+ @NonNull
+ static final String COLUMN_AUTHOR = "author";
+
+ @NonNull
+ static final String SQL_CREATE_TABLE = "CREATE TABLE " + TABLE + "(" +
+ COLUMN_ID + " INTEGER PRIMARY KEY, " +
+ COLUMN_TITLE + " TEXT NOT NULL," +
+ COLUMN_AUTHOR + " TEXT NOT NULL);";
+
+ @NonNull
+ static final PutResolver<Book> PUT_RESOLVER = new DefaultPutResolver<Book>() {
+ @NonNull
+ @Override
+ protected InsertQuery mapToInsertQuery(@NonNull Book object) {
+ return new InsertQuery.Builder()
+ .table(TABLE)
+ .build();
+ }
+
+ @NonNull
+ @Override
+ protected UpdateQuery mapToUpdateQuery(@NonNull Book book) {
+ return new UpdateQuery.Builder()
+ .table(TABLE)
+ .where(COLUMN_ID + " = ?")
+ .whereArgs(book.id())
+ .build();
+ }
+
+ @NonNull
+ @Override
+ protected ContentValues mapToContentValues(@NonNull Book book) {
+ final ContentValues contentValues = new ContentValues(3);
+
+ contentValues.put(COLUMN_ID, book.id());
+ contentValues.put(COLUMN_TITLE, book.title());
+ contentValues.put(COLUMN_AUTHOR, book.author());
+
+ return contentValues;
+ }
+ };
+
+ @NonNull
+ static final GetResolver<Book> GET_RESOLVER = new DefaultGetResolver<Book>() {
+ @NonNull
+ @Override
+ public Book mapFromCursor(@NonNull Cursor cursor) {
+ return Book.builder()
+ .id(cursor.getInt(cursor.getColumnIndex(COLUMN_ID)))
+ .title(cursor.getString(cursor.getColumnIndex(COLUMN_TITLE)))
+ .author(cursor.getString(cursor.getColumnIndex(COLUMN_AUTHOR)))
+ .build();
+ }
+ };
+
+ @NonNull
+ static final DeleteResolver<Book> DELETE_RESOLVER = new DefaultDeleteResolver<Book>() {
+ @NonNull
+ @Override
+ protected DeleteQuery mapToDeleteQuery(@NonNull Book book) {
+ return new DeleteQuery.Builder()
+ .table(TABLE)
+ .where(COLUMN_ID + " = ?")
+ .whereArgs(book.id())
+ .build();
+ }
+ };
+}
View
23 ...ite/src/androidTest/java/com/pushtorefresh/storio/sqlite/impl/auto_parcel/OpenHelper.java
@@ -0,0 +1,23 @@
+package com.pushtorefresh.storio.sqlite.impl.auto_parcel;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.support.annotation.NonNull;
+
+class OpenHelper extends SQLiteOpenHelper {
+
+ OpenHelper(@NonNull Context context) {
+ super(context, "auto_parcel_db", null, 1);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(BookTableMeta.SQL_CREATE_TABLE);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+
+ }
+}
View
80 storio-sqlite/src/main/java/com/pushtorefresh/storio/sqlite/impl/DefaultStorIOSQLite.java
@@ -18,17 +18,18 @@
import com.pushtorefresh.storio.sqlite.query.UpdateQuery;
import java.io.IOException;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import rx.Observable;
import static com.pushtorefresh.storio.internal.Checks.checkNotNull;
import static com.pushtorefresh.storio.internal.Queries.nullableArrayOfStrings;
import static com.pushtorefresh.storio.internal.Queries.nullableString;
+import static java.util.Collections.unmodifiableMap;
/**
* Default implementation of {@link StorIOSQLite} for {@link android.database.sqlite.SQLiteDatabase}.
@@ -165,8 +166,13 @@ public DefaultStorIOSQLite build() {
@NonNull
private final Object lock = new Object();
+ // Unmodifiable
@Nullable
- private final Map<Class<?>, SQLiteTypeMapping<?>> typesMapping;
+ private final Map<Class<?>, SQLiteTypeMapping<?>> directTypesMapping;
+
+ @NonNull
+ private final Map<Class<?>, SQLiteTypeMapping<?>> indirectTypesMappingCache
+ = new ConcurrentHashMap<Class<?>, SQLiteTypeMapping<?>>();
/**
* Guarded by {@link #lock}.
@@ -180,21 +186,77 @@ public DefaultStorIOSQLite build() {
private final Set<Changes> pendingChanges = new HashSet<Changes>();
protected InternalImpl(@Nullable Map<Class<?>, SQLiteTypeMapping<?>> typesMapping) {
- this.typesMapping = typesMapping != null
- ? Collections.unmodifiableMap(typesMapping)
+ this.directTypesMapping = typesMapping != null
+ ? unmodifiableMap(typesMapping)
: null;
}
/**
- * {@inheritDoc}
+ * Gets type mapping for required type.
+ * <p/>
+ * This implementation can handle subclasses of types, that registered its type mapping.
+ * For example: You've added type mapping for {@code User.class},
+ * and you have {@code UserFromServiceA.class} which extends {@code User.class},
+ * and you didn't add type mapping for {@code UserFromServiceA.class}
+ * because they have same fields and you just want to have multiple classes.
+ * This implementation will find type mapping of {@code User.class}
+ * and use it as type mapping for {@code UserFromServiceA.class}.
+ *
+ * @return direct or indirect type mapping for passed type, or {@code null}.
*/
@SuppressWarnings("unchecked")
@Nullable
@Override
- public <T> SQLiteTypeMapping<T> typeMapping(@NonNull Class<T> type) {
- return typesMapping != null
- ? (SQLiteTypeMapping<T>) typesMapping.get(type)
- : null;
+ public <T> SQLiteTypeMapping<T> typeMapping(final @NonNull Class<T> type) {
+ if (directTypesMapping == null) {
+ return null;
+ }
+
+ final SQLiteTypeMapping<T> directTypeMapping = (SQLiteTypeMapping<T>) directTypesMapping.get(type);
+
+ if (directTypeMapping != null) {
+ // fffast! O(1)
+ return directTypeMapping;
+ } else {
+ // If no direct type mapping found — search for indirect type mapping
+
+ // May be value already in cache.
+ SQLiteTypeMapping<T> indirectTypeMapping
+ = (SQLiteTypeMapping<T>) indirectTypesMappingCache.get(type);
+
+ if (indirectTypeMapping != null) {
+ // fffast! O(1)
+ return indirectTypeMapping;
+ }
+
+ // Okay, we don't have direct type mapping.
+ // And we don't have cache for indirect type mapping.
+ // Let's find indirect type mapping and cache it!
+ Class<?> parentType = type.getSuperclass();
+
+ // Search algorithm:
+ // Walk through all parent types of passed type.
+ // If parent type has direct mapping -> we found indirect type mapping!
+ // If current parent type == Object.class -> there is no indirect type mapping.
+ // Complexity:
+ // O(n) where n is number of parent types of passed type (pretty fast).
+
+ // Stop search if root parent is Object.class
+ while (parentType != Object.class) {
+ indirectTypeMapping = (SQLiteTypeMapping<T>) directTypesMapping.get(parentType);
+
+ if (indirectTypeMapping != null) {
+ // Store this typeMapping as known to make resolving O(1) for the next time
+ indirectTypesMappingCache.put(type, indirectTypeMapping);
+ return indirectTypeMapping;
+ }
+
+ parentType = parentType.getSuperclass();
+ }
+
+ // No indirect type mapping found.
+ return null;
+ }
}
/**
View
177 ...io-sqlite/src/test/java/com/pushtorefresh/storio/sqlite/impl/DefaultStorIOSQLiteTest.java
@@ -12,7 +12,8 @@
import java.io.IOException;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -23,8 +24,7 @@
@Test(expected = NullPointerException.class)
public void nullSQLiteOpenHelper() {
new DefaultStorIOSQLite.Builder()
- .sqliteOpenHelper(null)
- .build();
+ .sqliteOpenHelper(null);
}
@SuppressWarnings({"ConstantConditions", "unchecked"})
@@ -36,8 +36,7 @@ public void addTypeMappingNullType() {
.putResolver(mock(PutResolver.class))
.getResolver(mock(GetResolver.class))
.deleteResolver(mock(DeleteResolver.class))
- .build())
- .build();
+ .build());
}
@SuppressWarnings({"unchecked", "ConstantConditions"})
@@ -45,18 +44,165 @@ public void addTypeMappingNullType() {
public void addTypeMappingNullMapping() {
new DefaultStorIOSQLite.Builder()
.sqliteOpenHelper(mock(SQLiteOpenHelper.class))
- .addTypeMapping(Object.class, null)
+ .addTypeMapping(Object.class, null);
+ }
+
+ @Test
+ public void shouldReturnNullIfNoTypeMappingsRegistered() {
+ class TestItem {
+
+ }
+
+ final StorIOSQLite storIOSQLite = new DefaultStorIOSQLite.Builder()
+ .sqliteOpenHelper(mock(SQLiteOpenHelper.class))
+ .build();
+
+ assertNull(storIOSQLite.internal().typeMapping(TestItem.class));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void shouldReturnNullIfNotTypeMappingRegisteredForType() {
+ class TestItem {
+
+ }
+
+ class Entity {
+
+ }
+
+ final SQLiteTypeMapping<Entity> entityContentResolverTypeMapping = new SQLiteTypeMapping.Builder<Entity>()
+ .putResolver(mock(PutResolver.class))
+ .getResolver(mock(GetResolver.class))
+ .deleteResolver(mock(DeleteResolver.class))
+ .build();
+
+ final StorIOSQLite storIOSQLite = new DefaultStorIOSQLite.Builder()
+ .sqliteOpenHelper(mock(SQLiteOpenHelper.class))
+ .addTypeMapping(Entity.class, entityContentResolverTypeMapping)
+ .build();
+
+ assertSame(entityContentResolverTypeMapping, storIOSQLite.internal().typeMapping(Entity.class));
+
+ assertNull(storIOSQLite.internal().typeMapping(TestItem.class));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void directTypeMappingShouldWork() {
+ class TestItem {
+
+ }
+
+ final SQLiteTypeMapping<TestItem> typeMapping = new SQLiteTypeMapping.Builder<TestItem>()
+ .putResolver(mock(PutResolver.class))
+ .getResolver(mock(GetResolver.class))
+ .deleteResolver(mock(DeleteResolver.class))
+ .build();
+
+ final StorIOSQLite storIOSQLite = new DefaultStorIOSQLite.Builder()
+ .sqliteOpenHelper(mock(SQLiteOpenHelper.class))
+ .addTypeMapping(TestItem.class, typeMapping)
+ .build();
+
+ assertSame(typeMapping, storIOSQLite.internal().typeMapping(TestItem.class));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void indirectTypeMappingShouldWork() {
+ class TestItem {
+
+ }
+
+ final SQLiteTypeMapping<TestItem> typeMapping = new SQLiteTypeMapping.Builder<TestItem>()
+ .putResolver(mock(PutResolver.class))
+ .getResolver(mock(GetResolver.class))
+ .deleteResolver(mock(DeleteResolver.class))
+ .build();
+
+ final StorIOSQLite storIOSQLite = new DefaultStorIOSQLite.Builder()
+ .sqliteOpenHelper(mock(SQLiteOpenHelper.class))
+ .addTypeMapping(TestItem.class, typeMapping)
.build();
+
+ class TestItemSubclass extends TestItem {
+
+ }
+
+ // Direct type mapping should still work
+ assertSame(typeMapping, storIOSQLite.internal().typeMapping(TestItem.class));
+
+ // Indirect type mapping should give same type mapping as for parent class
+ assertSame(typeMapping, storIOSQLite.internal().typeMapping(TestItemSubclass.class));
}
@SuppressWarnings("unchecked")
@Test
- public void typeMapping() {
+ public void typeMappingShouldWorkInCaseOfMoreConcreteTypeMapping() {
class TestItem {
}
- final SQLiteTypeMapping<TestItem> testItemTypeDefinition = new SQLiteTypeMapping.Builder<TestItem>()
+ final SQLiteTypeMapping<TestItem> typeMapping = new SQLiteTypeMapping.Builder<TestItem>()
+ .putResolver(mock(PutResolver.class))
+ .getResolver(mock(GetResolver.class))
+ .deleteResolver(mock(DeleteResolver.class))
+ .build();
+
+
+ class TestItemSubclass extends TestItem {
+
+ }
+
+ final SQLiteTypeMapping<TestItemSubclass> subclassTypeMapping = new SQLiteTypeMapping.Builder<TestItemSubclass>()
+ .putResolver(mock(PutResolver.class))
+ .getResolver(mock(GetResolver.class))
+ .deleteResolver(mock(DeleteResolver.class))
+ .build();
+
+
+ final StorIOSQLite storIOSQLite = new DefaultStorIOSQLite.Builder()
+ .sqliteOpenHelper(mock(SQLiteOpenHelper.class))
+ .addTypeMapping(TestItem.class, typeMapping)
+ .addTypeMapping(TestItemSubclass.class, subclassTypeMapping)
+ .build();
+
+ // Parent class should have its own type mapping
+ assertSame(typeMapping, storIOSQLite.internal().typeMapping(TestItem.class));
+
+ // Child class should have its own type mapping
+ assertSame(subclassTypeMapping, storIOSQLite.internal().typeMapping(TestItemSubclass.class));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void typeMappingShouldFindIndirectTypeMappingInCaseOfComplexInheritance() {
+ // Good test case — inheritance with AutoValue/AutoParcel
+
+ class Entity {
+
+ }
+
+ class AutoValue_Entity extends Entity {
+
+ }
+
+ class ConcreteEntity extends Entity {
+
+ }
+
+ class AutoValue_ConcreteEntity extends ConcreteEntity {
+
+ }
+
+ final SQLiteTypeMapping<Entity> entitySQLiteTypeMapping = new SQLiteTypeMapping.Builder<Entity>()
+ .putResolver(mock(PutResolver.class))
+ .getResolver(mock(GetResolver.class))
+ .deleteResolver(mock(DeleteResolver.class))
+ .build();
+
+ final SQLiteTypeMapping<ConcreteEntity> concreteEntitySQLiteTypeMapping = new SQLiteTypeMapping.Builder<ConcreteEntity>()
.putResolver(mock(PutResolver.class))
.getResolver(mock(GetResolver.class))
.deleteResolver(mock(DeleteResolver.class))
@@ -64,10 +210,21 @@ public void typeMapping() {
final StorIOSQLite storIOSQLite = new DefaultStorIOSQLite.Builder()
.sqliteOpenHelper(mock(SQLiteOpenHelper.class))
- .addTypeMapping(TestItem.class, testItemTypeDefinition)
+ .addTypeMapping(Entity.class, entitySQLiteTypeMapping)
+ .addTypeMapping(ConcreteEntity.class, concreteEntitySQLiteTypeMapping)
.build();
- assertEquals(testItemTypeDefinition, storIOSQLite.internal().typeMapping(TestItem.class));
+ // Direct type mapping for Entity should work
+ assertSame(entitySQLiteTypeMapping, storIOSQLite.internal().typeMapping(Entity.class));
+
+ // Direct type mapping for ConcreteEntity should work
+ assertSame(concreteEntitySQLiteTypeMapping, storIOSQLite.internal().typeMapping(ConcreteEntity.class));
+
+ // Indirect type mapping for AutoValue_Entity should get type mapping for Entity
+ assertSame(entitySQLiteTypeMapping, storIOSQLite.internal().typeMapping(AutoValue_Entity.class));
+
+ // Indirect type mapping for AutoValue_ConcreteEntity should get type mapping for ConcreteEntity, not for Entity!
+ assertSame(concreteEntitySQLiteTypeMapping, storIOSQLite.internal().typeMapping(AutoValue_ConcreteEntity.class));
}
@Test

0 comments on commit 98664c1

Please sign in to comment.
Something went wrong with that request. Please try again.