Permalink
Please sign in to comment.
Browse files
Implement option to get one object instead of list for executeAsBlock…
…ing for ContentResolver
- Loading branch information...
Showing
with
569 additions
and 0 deletions.
- +12 −0 ...t-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGet.java
- +185 −0 ...lver/src/main/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGetObject.java
- +13 −0 ...esolver/src/test/java/com/pushtorefresh/storio/contentresolver/design/GetOperationDesignTest.java
- +64 −0 ...resolver/src/test/java/com/pushtorefresh/storio/contentresolver/integration/GetOperationTest.java
- +134 −0 ...resolver/src/test/java/com/pushtorefresh/storio/contentresolver/operations/get/GetObjectStub.java
- +146 −0 .../src/test/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGetObjectTest.java
- +15 −0 ...om/pushtorefresh/storio/test_without_rxjava/contentresolver/DefaultStorIOContentResolverTest.java
12
...er/src/main/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGet.java
185
.../main/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGetObject.java
@@ -0,0 +1,185 @@ | ||
+package com.pushtorefresh.storio.contentresolver.operations.get; | ||
+ | ||
+import android.database.Cursor; | ||
+import android.support.annotation.NonNull; | ||
+import android.support.annotation.Nullable; | ||
+import android.support.annotation.WorkerThread; | ||
+ | ||
+import com.pushtorefresh.storio.StorIOException; | ||
+import com.pushtorefresh.storio.contentresolver.ContentResolverTypeMapping; | ||
+import com.pushtorefresh.storio.contentresolver.StorIOContentResolver; | ||
+import com.pushtorefresh.storio.contentresolver.queries.Query; | ||
+ | ||
+import rx.Observable; | ||
+ | ||
+import static com.pushtorefresh.storio.internal.Checks.checkNotNull; | ||
+ | ||
+/** | ||
+ * Represents Get Operation for {@link StorIOContentResolver} | ||
+ * which performs query that retrieves single object | ||
+ * from {@link android.content.ContentProvider}. | ||
+ * | ||
+ * @param <T> type of result. | ||
+ */ | ||
+public final class PreparedGetObject<T> extends PreparedGet<T> { | ||
+ | ||
+ @NonNull | ||
+ private final Class<T> type; | ||
+ | ||
+ @Nullable | ||
+ private final GetResolver<T> explicitGetResolver; | ||
+ | ||
+ PreparedGetObject(@NonNull StorIOContentResolver storIOContentResolver, | ||
+ @NonNull Class<T> type, | ||
+ @NonNull Query query, | ||
+ @Nullable GetResolver<T> explicitGetResolver) { | ||
+ super(storIOContentResolver, query); | ||
+ this.type = type; | ||
+ this.explicitGetResolver = explicitGetResolver; | ||
+ } | ||
+ | ||
+ /** | ||
+ * Executes Prepared Operation immediately in current thread. | ||
+ * <p> | ||
+ * Notice: This is blocking I/O operation that should not be executed on the Main Thread, | ||
+ * it can cause ANR (Activity Not Responding dialog), block the UI and drop animations frames. | ||
+ * So please, call this method on some background thread. See {@link WorkerThread}. | ||
+ * | ||
+ * @return single instance of mapped result. Can be {@code null}, if no items are found. | ||
+ */ | ||
+ @SuppressWarnings({"ConstantConditions", "NullableProblems"}) | ||
+ @WorkerThread | ||
+ @Nullable | ||
+ @Override | ||
+ public T executeAsBlocking() { | ||
+ try { | ||
+ final GetResolver<T> getResolver; | ||
+ | ||
+ if (explicitGetResolver != null) { | ||
+ getResolver = explicitGetResolver; | ||
+ } else { | ||
+ final ContentResolverTypeMapping<T> typeMapping = storIOContentResolver.internal().typeMapping(type); | ||
+ | ||
+ if (typeMapping == null) { | ||
+ throw new IllegalStateException("This type does not have type mapping: " + | ||
+ "type = " + type + "," + | ||
+ "ContentProvider was not touched by this operation, please add type mapping for this type"); | ||
+ } | ||
+ | ||
+ getResolver = typeMapping.getResolver(); | ||
+ } | ||
+ | ||
+ final Cursor cursor = getResolver.performGet(storIOContentResolver, query); | ||
+ | ||
+ try { | ||
+ final int count = cursor.getCount(); | ||
+ | ||
+ if (count == 0) { | ||
+ return null; | ||
+ } | ||
+ | ||
+ cursor.moveToFirst(); | ||
+ | ||
+ return getResolver.mapFromCursor(cursor); | ||
+ } finally { | ||
+ cursor.close(); | ||
+ } | ||
+ } catch (Exception exception) { | ||
+ throw new StorIOException(exception); | ||
+ } | ||
+ } | ||
+ | ||
+ @NonNull | ||
+ @Override | ||
+ public Observable<T> createObservable() { | ||
+ throw new RuntimeException("not implemented yet"); | ||
+ } | ||
+ | ||
+ /** | ||
+ * Builder for {@link PreparedGetObject}. | ||
+ * | ||
+ * @param <T> type of objects for query. | ||
+ */ | ||
+ public static final class Builder<T> { | ||
+ | ||
+ @NonNull | ||
+ private final StorIOContentResolver storIOContentResolver; | ||
+ | ||
+ @NonNull | ||
+ private final Class<T> type; | ||
+ | ||
+ public Builder(@NonNull StorIOContentResolver storIOContentResolver, @NonNull Class<T> type) { | ||
+ this.storIOContentResolver = storIOContentResolver; | ||
+ this.type = type; | ||
+ } | ||
+ | ||
+ /** | ||
+ * Required: Specifies {@link Query} for Get Operation. | ||
+ * | ||
+ * @param query query. | ||
+ * @return builder. | ||
+ */ | ||
+ @NonNull | ||
+ public CompleteBuilder<T> withQuery(@NonNull Query query) { | ||
+ checkNotNull(query, "Please specify query"); | ||
+ return new CompleteBuilder<T>(storIOContentResolver, type, query); | ||
+ } | ||
+ } | ||
+ | ||
+ /** | ||
+ * Compile-time safe part of builder for {@link PreparedGetObject}. | ||
+ * | ||
+ * @param <T> type of objects for query. | ||
+ */ | ||
+ public static final class CompleteBuilder<T> { | ||
+ | ||
+ @NonNull | ||
+ private final StorIOContentResolver storIOContentResolver; | ||
+ | ||
+ @NonNull | ||
+ private final Class<T> type; | ||
+ | ||
+ @NonNull | ||
+ private final Query query; | ||
+ | ||
+ @Nullable | ||
+ private GetResolver<T> getResolver; | ||
+ | ||
+ CompleteBuilder(@NonNull StorIOContentResolver storIOContentResolver, @NonNull Class<T> type, @NonNull Query query) { | ||
+ this.storIOContentResolver = storIOContentResolver; | ||
+ this.type = type; | ||
+ this.query = query; | ||
+ } | ||
+ | ||
+ /** | ||
+ * Optional: Specifies {@link GetResolver} for Get Operation | ||
+ * which allows you to customize behavior of Get Operation. | ||
+ * <p> | ||
+ * Can be set via {@link ContentResolverTypeMapping}, | ||
+ * If value is not set via {@link ContentResolverTypeMapping} — exception will be thrown. | ||
+ * | ||
+ * @param getResolver GetResolver. | ||
+ * @return builder. | ||
+ */ | ||
+ @NonNull | ||
+ public CompleteBuilder<T> withGetResolver(@Nullable GetResolver<T> getResolver) { | ||
+ this.getResolver = getResolver; | ||
+ return this; | ||
+ } | ||
+ | ||
+ /** | ||
+ * Builds new instance of {@link PreparedGetObject}. | ||
+ * | ||
+ * @return new instance of {@link PreparedGetObject}. | ||
+ */ | ||
+ @NonNull | ||
+ public PreparedGetObject<T> prepare() { | ||
+ return new PreparedGetObject<T>( | ||
+ storIOContentResolver, | ||
+ type, | ||
+ query, | ||
+ getResolver | ||
+ ); | ||
+ } | ||
+ } | ||
+} |
13
...src/test/java/com/pushtorefresh/storio/contentresolver/design/GetOperationDesignTest.java
64
.../src/test/java/com/pushtorefresh/storio/contentresolver/integration/GetOperationTest.java
134
.../src/test/java/com/pushtorefresh/storio/contentresolver/operations/get/GetObjectStub.java
@@ -0,0 +1,134 @@ | ||
+package com.pushtorefresh.storio.contentresolver.operations.get; | ||
+ | ||
+import android.database.Cursor; | ||
+import android.net.Uri; | ||
+import android.support.annotation.NonNull; | ||
+ | ||
+import com.pushtorefresh.storio.contentresolver.Changes; | ||
+import com.pushtorefresh.storio.contentresolver.ContentResolverTypeMapping; | ||
+import com.pushtorefresh.storio.contentresolver.StorIOContentResolver; | ||
+import com.pushtorefresh.storio.contentresolver.queries.Query; | ||
+ | ||
+import rx.Observable; | ||
+ | ||
+import static org.assertj.core.api.Assertions.assertThat; | ||
+import static org.mockito.Mockito.mock; | ||
+import static org.mockito.Mockito.verify; | ||
+import static org.mockito.Mockito.verifyNoMoreInteractions; | ||
+import static org.mockito.Mockito.when; | ||
+ | ||
+class GetObjectStub { | ||
+ | ||
+ @NonNull | ||
+ final StorIOContentResolver storIOContentResolver; | ||
+ | ||
+ @NonNull | ||
+ private final StorIOContentResolver.Internal internal; | ||
+ | ||
+ @NonNull | ||
+ final Query query; | ||
+ | ||
+ @NonNull | ||
+ final TestItem item; | ||
+ | ||
+ @NonNull | ||
+ final GetResolver<TestItem> getResolver; | ||
+ | ||
+ @NonNull | ||
+ final Cursor cursor; | ||
+ | ||
+ @NonNull | ||
+ private final ContentResolverTypeMapping<TestItem> typeMapping; | ||
+ | ||
+ private final boolean withTypeMapping; | ||
+ | ||
+ @SuppressWarnings("unchecked") | ||
+ private GetObjectStub(boolean withTypeMapping) { | ||
+ this.withTypeMapping = withTypeMapping; | ||
+ | ||
+ storIOContentResolver = mock(StorIOContentResolver.class); | ||
+ internal = mock(StorIOContentResolver.Internal.class); | ||
+ | ||
+ query = Query.builder() | ||
+ .uri(mock(Uri.class)) | ||
+ .build(); | ||
+ | ||
+ getResolver = mock(GetResolver.class); | ||
+ cursor = mock(Cursor.class); | ||
+ | ||
+ item = new TestItem(); | ||
+ | ||
+ when(storIOContentResolver.internal()) | ||
+ .thenReturn(internal); | ||
+ | ||
+ when(cursor.getCount()) | ||
+ .thenReturn(1); | ||
+ | ||
+ when(cursor.moveToFirst()).thenReturn(true); | ||
+ | ||
+ when(storIOContentResolver.get()) | ||
+ .thenReturn(new PreparedGet.Builder(storIOContentResolver)); | ||
+ | ||
+ when(getResolver.performGet(storIOContentResolver, query)) | ||
+ .thenReturn(cursor); | ||
+ | ||
+ when(storIOContentResolver.observeChangesOfUri(query.uri())) | ||
+ .thenReturn(Observable.<Changes>empty()); | ||
+ | ||
+ when(getResolver.mapFromCursor(cursor)) | ||
+ .thenReturn(item); | ||
+ | ||
+ typeMapping = mock(ContentResolverTypeMapping.class); | ||
+ | ||
+ if (withTypeMapping) { | ||
+ when(internal.typeMapping(TestItem.class)).thenReturn(typeMapping); | ||
+ when(typeMapping.getResolver()).thenReturn(getResolver); | ||
+ } | ||
+ } | ||
+ | ||
+ @NonNull | ||
+ static GetObjectStub newStubWithoutTypeMapping() { | ||
+ return new GetObjectStub(false); | ||
+ } | ||
+ | ||
+ @NonNull | ||
+ static GetObjectStub newStubWithTypeMapping() { | ||
+ return new GetObjectStub(true); | ||
+ } | ||
+ | ||
+ void verifyBehavior(@NonNull TestItem actualItem) { | ||
+ // should be called once | ||
+ verify(storIOContentResolver).get(); | ||
+ | ||
+ // should be called once | ||
+ verify(getResolver).performGet(storIOContentResolver, query); | ||
+ | ||
+ // should be called only once because of Performance! | ||
+ verify(cursor).getCount(); | ||
+ | ||
+ // should be called once | ||
+ verify(cursor).moveToFirst(); | ||
+ | ||
+ // should be called once | ||
+ verify(getResolver).mapFromCursor(cursor); | ||
+ | ||
+ // cursor should be closed! | ||
+ verify(cursor).close(); | ||
+ | ||
+ // checks that items are okay | ||
+ assertThat(actualItem).isEqualTo(item); | ||
+ | ||
+ if (withTypeMapping) { | ||
+ // should be called only once because of Performance! | ||
+ verify(storIOContentResolver).internal(); | ||
+ | ||
+ // should be called only once because of Performance! | ||
+ verify(internal).typeMapping(TestItem.class); | ||
+ | ||
+ // should be called only once | ||
+ verify(typeMapping).getResolver(); | ||
+ } | ||
+ | ||
+ verifyNoMoreInteractions(storIOContentResolver, internal, getResolver, cursor); | ||
+ } | ||
+} |
146
...t/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGetObjectTest.java
@@ -0,0 +1,146 @@ | ||
+package com.pushtorefresh.storio.contentresolver.operations.get; | ||
+ | ||
+import android.database.Cursor; | ||
+import android.net.Uri; | ||
+ | ||
+import com.pushtorefresh.storio.StorIOException; | ||
+import com.pushtorefresh.storio.contentresolver.StorIOContentResolver; | ||
+import com.pushtorefresh.storio.contentresolver.queries.Query; | ||
+ | ||
+import org.junit.Test; | ||
+import org.junit.experimental.runners.Enclosed; | ||
+import org.junit.runner.RunWith; | ||
+ | ||
+import static org.assertj.core.api.Assertions.assertThat; | ||
+import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; | ||
+import static org.mockito.Matchers.any; | ||
+import static org.mockito.Mockito.mock; | ||
+import static org.mockito.Mockito.never; | ||
+import static org.mockito.Mockito.verify; | ||
+import static org.mockito.Mockito.verifyNoMoreInteractions; | ||
+import static org.mockito.Mockito.when; | ||
+ | ||
+@RunWith(Enclosed.class) | ||
+public class PreparedGetObjectTest { | ||
+ | ||
+ public static class WithoutTypeMapping { | ||
+ | ||
+ @Test | ||
+ public void shouldGetObjectWithoutTypeMappingBlocking() { | ||
+ final GetObjectStub getStub = GetObjectStub.newStubWithoutTypeMapping(); | ||
+ | ||
+ final TestItem testItem = getStub.storIOContentResolver | ||
+ .get() | ||
+ .object(TestItem.class) | ||
+ .withQuery(getStub.query) | ||
+ .withGetResolver(getStub.getResolver) | ||
+ .prepare() | ||
+ .executeAsBlocking(); | ||
+ | ||
+ getStub.verifyBehavior(testItem); | ||
+ } | ||
+ } | ||
+ | ||
+ public static class WithTypeMapping { | ||
+ | ||
+ @Test | ||
+ public void shouldGetObjectWithTypeMappingBlocking() { | ||
+ final GetObjectStub getStub = GetObjectStub.newStubWithTypeMapping(); | ||
+ | ||
+ final TestItem testItem = getStub.storIOContentResolver | ||
+ .get() | ||
+ .object(TestItem.class) | ||
+ .withQuery(getStub.query) | ||
+ .prepare() | ||
+ .executeAsBlocking(); | ||
+ | ||
+ getStub.verifyBehavior(testItem); | ||
+ } | ||
+ } | ||
+ | ||
+ public static class NoTypeMappingError { | ||
+ | ||
+ @Test | ||
+ public void shouldThrowExceptionIfNoTypeMappingWasFoundWithoutAccessingContentProviderBlocking() { | ||
+ final StorIOContentResolver storIOContentResolver = mock(StorIOContentResolver.class); | ||
+ final StorIOContentResolver.Internal internal = mock(StorIOContentResolver.Internal.class); | ||
+ | ||
+ when(storIOContentResolver.internal()).thenReturn(internal); | ||
+ | ||
+ when(storIOContentResolver.get()).thenReturn(new PreparedGet.Builder(storIOContentResolver)); | ||
+ | ||
+ final PreparedGet<TestItem> preparedGet = storIOContentResolver | ||
+ .get() | ||
+ .object(TestItem.class) | ||
+ .withQuery(Query.builder().uri(mock(Uri.class)).build()) | ||
+ .prepare(); | ||
+ | ||
+ try { | ||
+ preparedGet.executeAsBlocking(); | ||
+ failBecauseExceptionWasNotThrown(StorIOException.class); | ||
+ } catch (StorIOException expected) { | ||
+ // it's okay, no type mapping was found | ||
+ assertThat(expected).hasCauseInstanceOf(IllegalStateException.class); | ||
+ assertThat(expected.getCause()).hasMessage("This type does not have type mapping: " + | ||
+ "type = " + TestItem.class + "," + | ||
+ "ContentProvider was not touched by this operation, please add type mapping for this type"); | ||
+ } | ||
+ | ||
+ verify(storIOContentResolver).get(); | ||
+ verify(storIOContentResolver).internal(); | ||
+ verify(internal).typeMapping(TestItem.class); | ||
+ verify(internal, never()).query(any(Query.class)); | ||
+ verifyNoMoreInteractions(storIOContentResolver, internal); | ||
+ } | ||
+ } | ||
+ | ||
+ // With Enclosed runner we can not have tests in root class | ||
+ public static class OtherTests { | ||
+ | ||
+ @Test | ||
+ public void shouldCloseCursorInCaseOfException() { | ||
+ StorIOContentResolver storIOContentResolver = mock(StorIOContentResolver.class); | ||
+ | ||
+ Query query = Query.builder() | ||
+ .uri(mock(Uri.class)) | ||
+ .build(); | ||
+ | ||
+ //noinspection unchecked | ||
+ GetResolver<Object> getResolver = mock(GetResolver.class); | ||
+ | ||
+ Cursor cursor = mock(Cursor.class); | ||
+ | ||
+ when(getResolver.performGet(storIOContentResolver, query)) | ||
+ .thenReturn(cursor); | ||
+ | ||
+ when(getResolver.mapFromCursor(cursor)) | ||
+ .thenThrow(new IllegalStateException("Breaking execution")); | ||
+ | ||
+ when(cursor.getCount()).thenReturn(1); | ||
+ | ||
+ when(cursor.moveToFirst()).thenReturn(true); | ||
+ | ||
+ try { | ||
+ new PreparedGetObject.Builder<Object>(storIOContentResolver, Object.class) | ||
+ .withQuery(query) | ||
+ .withGetResolver(getResolver) | ||
+ .prepare() | ||
+ .executeAsBlocking(); | ||
+ | ||
+ failBecauseExceptionWasNotThrown(StorIOException.class); | ||
+ } catch (StorIOException expected) { | ||
+ assertThat(expected.getCause()) | ||
+ .isInstanceOf(IllegalStateException.class) | ||
+ .hasMessage("Breaking execution"); | ||
+ | ||
+ // Main check: in case of exception cursor must be closed | ||
+ verify(cursor).close(); | ||
+ | ||
+ verify(cursor).getCount(); | ||
+ verify(cursor).moveToFirst(); | ||
+ | ||
+ verifyNoMoreInteractions(storIOContentResolver, cursor); | ||
+ } | ||
+ } | ||
+ } | ||
+} |
15
...orefresh/storio/test_without_rxjava/contentresolver/DefaultStorIOContentResolverTest.java
0 comments on commit
b3a3df2