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