From b3a3df2c7d73caf9a083de3a84c1708c9846fd5f Mon Sep 17 00:00:00 2001
From: Ilya Zorin <geraltencore@gmail.com>
Date: Mon, 23 Nov 2015 00:18:30 +0300
Subject: [PATCH] Implement option to get one object instead of list for
 executeAsBlocking for ContentResolver

---
 .../operations/get/PreparedGet.java                |  12 ++
 .../operations/get/PreparedGetObject.java          | 185 +++++++++++++++++++++
 .../design/GetOperationDesignTest.java             |  13 ++
 .../integration/GetOperationTest.java              |  64 +++++++
 .../operations/get/GetObjectStub.java              | 134 +++++++++++++++
 .../operations/get/PreparedGetObjectTest.java      | 146 ++++++++++++++++
 .../DefaultStorIOContentResolverTest.java          |  15 ++
 7 files changed, 569 insertions(+)
 create mode 100644 storio-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGetObject.java
 create mode 100644 storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/operations/get/GetObjectStub.java
 create mode 100644 storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGetObjectTest.java

diff --git a/storio-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGet.java b/storio-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGet.java
index 72b2373..e75d46b 100644
--- a/storio-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGet.java
+++ b/storio-content-resolver/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.
diff --git a/storio-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGetObject.java b/storio-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGetObject.java
new file mode 100644
index 0000000..2845699
--- /dev/null
+++ b/storio-content-resolver/src/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
+            );
+        }
+    }
+}
diff --git a/storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/design/GetOperationDesignTest.java b/storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/design/GetOperationDesignTest.java
index 9e88f06..4ea3a0f 100644
--- a/storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/design/GetOperationDesignTest.java
+++ b/storio-content-resolver/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();
+    }
 }
diff --git a/storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/integration/GetOperationTest.java b/storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/integration/GetOperationTest.java
index 6128101..935bc45 100644
--- a/storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/integration/GetOperationTest.java
+++ b/storio-content-resolver/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));
+    }
 }
diff --git a/storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/operations/get/GetObjectStub.java b/storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/operations/get/GetObjectStub.java
new file mode 100644
index 0000000..a2ba6e1
--- /dev/null
+++ b/storio-content-resolver/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);
+    }
+}
diff --git a/storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGetObjectTest.java b/storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/operations/get/PreparedGetObjectTest.java
new file mode 100644
index 0000000..c16da27
--- /dev/null
+++ b/storio-content-resolver/src/test/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);
+            }
+        }
+    }
+}
diff --git a/storio-test-without-rxjava/src/test/java/com/pushtorefresh/storio/test_without_rxjava/contentresolver/DefaultStorIOContentResolverTest.java b/storio-test-without-rxjava/src/test/java/com/pushtorefresh/storio/test_without_rxjava/contentresolver/DefaultStorIOContentResolverTest.java
index 7885427..d0c499c 100644
--- a/storio-test-without-rxjava/src/test/java/com/pushtorefresh/storio/test_without_rxjava/contentresolver/DefaultStorIOContentResolverTest.java
+++ b/storio-test-without-rxjava/src/test/java/com/pushtorefresh/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))
