Skip to content
Browse files

Implement option to get one object instead of list for executeAsBlock…

…ing for ContentResolver
  • Loading branch information...
1 parent ff4b08d commit b3a3df2c7d73caf9a083de3a84c1708c9846fd5f @geralt-encore geralt-encore committed
View
12 ...er/src/main/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGet.java
@@ -59,6 +59,18 @@ public Builder(@NonNull StorIOContentResolver storIOContentResolver) {
}
/**
+ * Returns builder for Get Operation that returns result as item instance.
+ *
+ * @param type type of item.
+ * @param <T> type of item.
+ * @return builder for Get Operation that returns result as item instance.
+ */
+ @NonNull
+ public <T> PreparedGetObject.Builder<T> object(@NonNull Class<T> type) {
+ return new PreparedGetObject.Builder<T>(storIOContentResolver, type);
+ }
+
+ /**
* Returns builder for Get Operation that returns number of results.
*
* @return builder for Get Operation that returns number of results.
View
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
+ );
+ }
+ }
+}
View
13 ...src/test/java/com/pushtorefresh/storio/contentresolver/design/GetOperationDesignTest.java
@@ -40,4 +40,17 @@ public void getListOfObjectsBlocking() {
.prepare()
.executeAsBlocking();
}
+
+ @Test
+ public void getObjectBlocking() {
+ Article article = storIOContentResolver()
+ .get()
+ .object(Article.class)
+ .withQuery(Query.builder()
+ .uri(mock(Uri.class))
+ .build())
+ .withGetResolver(ArticleMeta.GET_RESOLVER)
+ .prepare()
+ .executeAsBlocking();
+ }
}
View
64 .../src/test/java/com/pushtorefresh/storio/contentresolver/integration/GetOperationTest.java
@@ -200,4 +200,68 @@ public void getNumberOfResults() {
changesTestSubscriber.assertNoErrors();
changesTestSubscriber.assertValue(Changes.newInstance(TestItem.CONTENT_URI));
}
+
+ @Test
+ public void getExistedObjectExecuteAsBlocking() {
+ TestSubscriber<Changes> changesTestSubscriber = new TestSubscriber<Changes>();
+
+ storIOContentResolver
+ .observeChangesOfUri(TestItem.CONTENT_URI)
+ .take(1)
+ .subscribe(changesTestSubscriber);
+
+ TestItem testItemToInsert = TestItem.create(null, "value");
+ contentResolver.insert(TestItem.CONTENT_URI, testItemToInsert.toContentValues());
+ contentResolver.insert(TestItem.CONTENT_URI, TestItem.create(null, "value1").toContentValues());
+ contentResolver.insert(TestItem.CONTENT_URI, TestItem.create(null, "value2").toContentValues());
+
+ TestItem testItem = storIOContentResolver
+ .get()
+ .object(TestItem.class)
+ .withQuery(Query.builder()
+ .uri(TestItem.CONTENT_URI)
+ .where(TestItem.COLUMN_VALUE + "=?")
+ .whereArgs("value")
+ .build())
+ .prepare()
+ .executeAsBlocking();
+
+ assertThat(testItem).isNotNull();
+
+ assertThat(testItemToInsert.equalsWithoutId(testItem)).isTrue();
+
+ changesTestSubscriber.awaitTerminalEvent(60, SECONDS);
+ changesTestSubscriber.assertNoErrors();
+ changesTestSubscriber.assertValue(Changes.newInstance(TestItem.CONTENT_URI));
+ }
+
+ @Test
+ public void getNonExistedObjectExecuteAsBlocking() {
+ TestSubscriber<Changes> changesTestSubscriber = new TestSubscriber<Changes>();
+
+ storIOContentResolver
+ .observeChangesOfUri(TestItem.CONTENT_URI)
+ .take(1)
+ .subscribe(changesTestSubscriber);
+
+ TestItem testItemToInsert = TestItem.create(null, "value");
+ contentResolver.insert(TestItem.CONTENT_URI, testItemToInsert.toContentValues());
+
+ TestItem testItem = storIOContentResolver
+ .get()
+ .object(TestItem.class)
+ .withQuery(Query.builder()
+ .uri(TestItem.CONTENT_URI)
+ .where(TestItem.COLUMN_VALUE + "=?")
+ .whereArgs("some value")
+ .build())
+ .prepare()
+ .executeAsBlocking();
+
+ assertThat(testItem).isNull();
+
+ changesTestSubscriber.awaitTerminalEvent(60, SECONDS);
+ changesTestSubscriber.assertNoErrors();
+ changesTestSubscriber.assertValue(Changes.newInstance(TestItem.CONTENT_URI));
+ }
}
View
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);
+ }
+}
View
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);
+ }
+ }
+ }
+}
View
15 ...orefresh/storio/test_without_rxjava/contentresolver/DefaultStorIOContentResolverTest.java
@@ -59,6 +59,21 @@ public void instantiateGetListOfObjects() {
@SuppressWarnings("unchecked")
@Test
+ public void instantiateGetObject() {
+ DefaultStorIOContentResolver.builder()
+ .contentResolver(mock(ContentResolver.class))
+ .build()
+ .get()
+ .object(Object.class)
+ .withQuery(Query.builder()
+ .uri(mock(Uri.class))
+ .build())
+ .withGetResolver(mock(GetResolver.class))
+ .prepare();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
public void instantiatePutObject() {
DefaultStorIOContentResolver.builder()
.contentResolver(mock(ContentResolver.class))

0 comments on commit b3a3df2

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