From e832ae4e83b7487ce9b5df73242afa45b8ff47c9 Mon Sep 17 00:00:00 2001
From: Dmitry Nikitin <nikitin.da.90@gmail.com>
Date: Sun, 20 Dec 2015 03:43:00 +0500
Subject: [PATCH] Add ability to set different uris for insert, update and
 delete

---
 .../AnnotatedFieldValidationTest.java              |  65 +++++++
 .../introspection/AnnotationProcessorStub.java     |  78 ++++++++
 .../processor/StorIOContentResolverProcessor.java  |  61 +++++-
 .../generate/DeleteResolverGenerator.java          |   7 +-
 .../processor/generate/PutResolverGenerator.java   |  14 +-
 .../processor/ContentResolverProcessorStub.java    |  73 ++++++++
 .../annotations/processor/UriValidationTest.java   |  61 ++++++
 .../generate/DeleteResolverGeneratorTest.java      | 124 ++++++++++++-
 .../generate/PutResolverGeneratorTest.java         | 206 ++++++++++++++++++++-
 .../annotations/StorIOContentResolverType.java     |  29 ++-
 10 files changed, 693 insertions(+), 25 deletions(-)
 create mode 100644 storio-common-annotations-processor/src/test/java/com/pushtorefresh/storio/common/annotations/processor/introspection/AnnotatedFieldValidationTest.java
 create mode 100644 storio-common-annotations-processor/src/test/java/com/pushtorefresh/storio/common/annotations/processor/introspection/AnnotationProcessorStub.java
 create mode 100644 storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/ContentResolverProcessorStub.java
 create mode 100644 storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/UriValidationTest.java

diff --git a/storio-common-annotations-processor/src/test/java/com/pushtorefresh/storio/common/annotations/processor/introspection/AnnotatedFieldValidationTest.java b/storio-common-annotations-processor/src/test/java/com/pushtorefresh/storio/common/annotations/processor/introspection/AnnotatedFieldValidationTest.java
new file mode 100644
index 0000000..11aaae2
--- /dev/null
+++ b/storio-common-annotations-processor/src/test/java/com/pushtorefresh/storio/common/annotations/processor/introspection/AnnotatedFieldValidationTest.java
@@ -0,0 +1,65 @@
+package com.pushtorefresh.storio.common.annotations.processor.introspection;
+
+import com.pushtorefresh.storio.common.annotations.processor.ProcessingException;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.lang.model.element.Modifier;
+
+import static javax.lang.model.element.ElementKind.METHOD;
+import static org.mockito.Mockito.when;
+
+public class AnnotatedFieldValidationTest {
+
+    @Rule
+    public ExpectedException expectedException = ExpectedException.none();
+
+    @Test
+    public void failIfEnclosingElementIsNotClass() {
+        AnnotationProcessorStub stub = AnnotationProcessorStub.newInstance();
+        when(stub.enclosingElement.getKind()).thenReturn(METHOD);
+
+        expectedException.expect(ProcessingException.class);
+        expectedException.expectMessage("Please apply TestClassAnnotation to fields of class: TestField");
+        stub.processor.validateAnnotatedField(stub.field);
+    }
+
+    @Test
+    public void failIfThereIsNoAnnotation() {
+        AnnotationProcessorStub stub = AnnotationProcessorStub.newInstance();
+        when(stub.enclosingElement.getAnnotation(AnnotationProcessorStub.TestClassAnnotation.class)).thenReturn(null);
+
+        expectedException.expect(ProcessingException.class);
+        expectedException.expectMessage("Please annotate class TestClass with TestClassAnnotation");
+        stub.processor.validateAnnotatedField(stub.field);
+    }
+
+    @Test
+    public void failIfFieldPrivate() {
+        AnnotationProcessorStub stub = AnnotationProcessorStub.newInstance();
+        Set<Modifier> modifiers = new HashSet<Modifier>();
+        modifiers.add(Modifier.PRIVATE);
+        when(stub.field.getModifiers()).thenReturn(modifiers);
+
+        expectedException.expect(ProcessingException.class);
+        expectedException.expectMessage("TestFieldAnnotation can not be applied to private field: TestField");
+        stub.processor.validateAnnotatedField(stub.field);
+    }
+
+    @Test
+    public void failIfFieldFinal() {
+        AnnotationProcessorStub stub = AnnotationProcessorStub.newInstance();
+        Set<Modifier> modifiers = new HashSet<Modifier>();
+        modifiers.add(Modifier.FINAL);
+        when(stub.field.getModifiers()).thenReturn(modifiers);
+
+        expectedException.expect(ProcessingException.class);
+        expectedException.expectMessage("TestFieldAnnotation can not be applied to final field: TestField");
+        stub.processor.validateAnnotatedField(stub.field);
+    }
+}
diff --git a/storio-common-annotations-processor/src/test/java/com/pushtorefresh/storio/common/annotations/processor/introspection/AnnotationProcessorStub.java b/storio-common-annotations-processor/src/test/java/com/pushtorefresh/storio/common/annotations/processor/introspection/AnnotationProcessorStub.java
new file mode 100644
index 0000000..6ae5a21
--- /dev/null
+++ b/storio-common-annotations-processor/src/test/java/com/pushtorefresh/storio/common/annotations/processor/introspection/AnnotationProcessorStub.java
@@ -0,0 +1,78 @@
+package com.pushtorefresh.storio.common.annotations.processor.introspection;
+
+import com.pushtorefresh.storio.common.annotations.processor.StorIOAnnotationsProcessor;
+
+import org.jetbrains.annotations.NotNull;
+import org.mockito.Mockito;
+
+import java.lang.annotation.Annotation;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.Name;
+
+import static javax.lang.model.element.ElementKind.CLASS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class AnnotationProcessorStub {
+
+    final TestStorIOAnnotationsProcessor processor;
+    final Element field;
+    final Element enclosingElement;
+    final TestClassAnnotation classAnnotation;
+    final TestFieldAnnotation fieldAnnotation;
+
+    @NotNull
+    static AnnotationProcessorStub newInstance() {
+        return new AnnotationProcessorStub();
+    }
+
+    public AnnotationProcessorStub() {
+        processor = mock(TestStorIOAnnotationsProcessor.class, Mockito.CALLS_REAL_METHODS);
+
+        field = mock(Element.class);
+
+        Name name = mock(Name.class);
+        when(name.toString()).thenReturn("TestField");
+        when(field.getSimpleName()).thenReturn(name);
+
+        enclosingElement = mock(Element.class);
+        Name className = mock(Name.class);
+        when(className.toString()).thenReturn("TestClass");
+        when(enclosingElement.getSimpleName()).thenReturn(className);
+        when(enclosingElement.getKind()).thenReturn(CLASS);
+
+        when(field.getEnclosingElement()).thenReturn(enclosingElement);
+
+        classAnnotation = mock(TestClassAnnotation.class);
+        fieldAnnotation = mock(TestFieldAnnotation.class);
+
+        when(enclosingElement.getAnnotation(TestClassAnnotation.class)).thenReturn(classAnnotation);
+    }
+
+    protected static abstract class TestStorIOAnnotationsProcessor extends StorIOAnnotationsProcessor {
+
+        @Override
+        public void validateAnnotatedField(@NotNull Element annotatedField) {
+            super.validateAnnotatedField(annotatedField);
+        }
+
+        @NotNull
+        @Override
+        public Class<? extends Annotation> getTypeAnnotationClass() {
+            return TestClassAnnotation.class;
+        }
+
+        @NotNull
+        @Override
+        protected Class<? extends Annotation> getColumnAnnotationClass() {
+            return TestFieldAnnotation.class;
+        }
+    }
+
+    static abstract class TestClassAnnotation implements Annotation {
+    }
+
+    static abstract class TestFieldAnnotation implements Annotation {
+    }
+}
diff --git a/storio-content-resolver-annotations-processor/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/processor/StorIOContentResolverProcessor.java b/storio-content-resolver-annotations-processor/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/processor/StorIOContentResolverProcessor.java
index 0272059..21c7a05 100644
--- a/storio-content-resolver-annotations-processor/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/processor/StorIOContentResolverProcessor.java
+++ b/storio-content-resolver-annotations-processor/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/processor/StorIOContentResolverProcessor.java
@@ -14,9 +14,13 @@
 import com.pushtorefresh.storio.contentresolver.annotations.processor.introspection.StorIOContentResolverTypeMeta;
 
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -65,14 +69,14 @@
     protected StorIOContentResolverTypeMeta processAnnotatedClass(@NotNull TypeElement classElement, @NotNull Elements elementUtils) {
         final StorIOContentResolverType storIOContentResolverType = classElement.getAnnotation(StorIOContentResolverType.class);
 
-        final String uri = storIOContentResolverType.uri();
+        final String commonUri = storIOContentResolverType.uri();
 
-        if (uri == null || uri.length() == 0) {
-            throw new ProcessingException(
-                    classElement,
-                    "Uri of " + classElement.getQualifiedName() + " annotated with " + StorIOContentResolverType.class.getSimpleName() + " is null or empty"
-            );
-        }
+        final Map<String, String> urisForOperations = new HashMap<String, String>(3);
+        urisForOperations.put("insert", storIOContentResolverType.insertUri());
+        urisForOperations.put("update", storIOContentResolverType.updateUri());
+        urisForOperations.put("delete", storIOContentResolverType.deleteUri());
+
+        validateUris(classElement, commonUri, urisForOperations);
 
         final String simpleName = classElement.getSimpleName().toString();
         final String packageName = elementUtils.getPackageOf(classElement).getQualifiedName().toString();
@@ -80,6 +84,49 @@ protected StorIOContentResolverTypeMeta processAnnotatedClass(@NotNull TypeEleme
         return new StorIOContentResolverTypeMeta(simpleName, packageName, storIOContentResolverType);
     }
 
+
+    /**
+     * Verifies that uris are valid.
+     *
+     * @param classElement type element
+     * @param commonUri nullable default uri for all operations
+     * @param operationUriMap non-null map where
+     *                              key - operation name,
+     *                              value - specific uri for this operation
+     */
+    protected void validateUris(
+            @NotNull TypeElement classElement,
+            @Nullable String commonUri,
+            @NotNull Map<String, String> operationUriMap) {
+
+        if(!validateUri(commonUri)) {
+            final List<String> operationsWithInvalidUris = new ArrayList<String>(operationUriMap.size());
+            for (Map.Entry<String, String> entry : operationUriMap.entrySet()) {
+                if (!validateUri(entry.getValue())) {
+                    operationsWithInvalidUris.add(entry.getKey());
+                }
+            }
+            if (!operationsWithInvalidUris.isEmpty()) {
+                String message = "Uri of " + classElement.getQualifiedName()
+                        + " annotated with " + getTypeAnnotationClass().getSimpleName() + " is null or empty";
+
+                if (operationsWithInvalidUris.size() < operationUriMap.size()) {
+                    message += " for operation " + operationsWithInvalidUris.get(0);
+                }
+                // Else (there is no any uris) - do not specify operation,
+                // because commonUri is default and straightforward way.
+
+                throw new ProcessingException(classElement, message);
+            }
+
+            // It will be okay if uris for all operations were specified separately.
+        }
+    }
+
+    private boolean validateUri(@Nullable String uri) {
+        return uri != null && uri.length() > 0;
+    }
+
     /**
      * Processes fields annotated with {@link StorIOContentResolverColumn}
      *
diff --git a/storio-content-resolver-annotations-processor/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/DeleteResolverGenerator.java b/storio-content-resolver-annotations-processor/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/DeleteResolverGenerator.java
index eada6d1..1182c48 100644
--- a/storio-content-resolver-annotations-processor/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/DeleteResolverGenerator.java
+++ b/storio-content-resolver-annotations-processor/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/DeleteResolverGenerator.java
@@ -41,6 +41,11 @@ public JavaFile generateJavaFile(@NotNull final StorIOContentResolverTypeMeta st
     private MethodSpec createMapToDeleteQueryMethodSpec(@NotNull final StorIOContentResolverTypeMeta storIOContentResolverTypeMeta, @NotNull final ClassName storIOContentResolverTypeClassName) {
         final Map<String, String> where = QueryGenerator.createWhere(storIOContentResolverTypeMeta, "object");
 
+        String deleteUri = storIOContentResolverTypeMeta.storIOType.deleteUri();
+        if (deleteUri == null || deleteUri.length() == 0) {
+            deleteUri = storIOContentResolverTypeMeta.storIOType.uri();
+        }
+
         return MethodSpec.methodBuilder("mapToDeleteQuery")
                 .addJavadoc("{@inheritDoc}\n")
                 .addAnnotation(Override.class)
@@ -55,7 +60,7 @@ private MethodSpec createMapToDeleteQueryMethodSpec(@NotNull final StorIOContent
                                 INDENT + ".where($S)\n" +
                                 INDENT + ".whereArgs($L)\n" +
                                 INDENT + ".build();\n",
-                        storIOContentResolverTypeMeta.storIOType.uri(),
+                        deleteUri,
                         where.get(QueryGenerator.WHERE_CLAUSE),
                         where.get(QueryGenerator.WHERE_ARGS))
                 .build();
diff --git a/storio-content-resolver-annotations-processor/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/PutResolverGenerator.java b/storio-content-resolver-annotations-processor/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/PutResolverGenerator.java
index 32ce556..d9087a9 100644
--- a/storio-content-resolver-annotations-processor/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/PutResolverGenerator.java
+++ b/storio-content-resolver-annotations-processor/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/PutResolverGenerator.java
@@ -43,6 +43,11 @@ public JavaFile generateJavaFile(@NotNull final StorIOContentResolverTypeMeta st
 
     @NotNull
     private MethodSpec createMapToInsertQueryMethodSpec(@NotNull final StorIOContentResolverTypeMeta storIOContentResolverTypeMeta, @NotNull final ClassName storIOContentResolverClassName) {
+        String insertUri = storIOContentResolverTypeMeta.storIOType.insertUri();
+        if (insertUri == null || insertUri.length() == 0) {
+            insertUri = storIOContentResolverTypeMeta.storIOType.uri();
+        }
+
         return MethodSpec.methodBuilder("mapToInsertQuery")
                 .addJavadoc("{@inheritDoc}\n")
                 .addAnnotation(Override.class)
@@ -55,7 +60,7 @@ private MethodSpec createMapToInsertQueryMethodSpec(@NotNull final StorIOContent
                 .addCode("return InsertQuery.builder()\n" +
                                 INDENT + ".uri($S)\n" +
                                 INDENT + ".build();\n",
-                        storIOContentResolverTypeMeta.storIOType.uri())
+                        insertUri)
                 .build();
     }
 
@@ -63,6 +68,11 @@ private MethodSpec createMapToInsertQueryMethodSpec(@NotNull final StorIOContent
     private MethodSpec createMapToUpdateQueryMethodSpec(@NotNull final StorIOContentResolverTypeMeta storIOContentResolverTypeMeta, @NotNull final ClassName storIOContentResolverClassName) {
         final Map<String, String> where = QueryGenerator.createWhere(storIOContentResolverTypeMeta, "object");
 
+        String updateUri = storIOContentResolverTypeMeta.storIOType.updateUri();
+        if (updateUri == null || updateUri.length() == 0) {
+            updateUri = storIOContentResolverTypeMeta.storIOType.uri();
+        }
+
         return MethodSpec.methodBuilder("mapToUpdateQuery")
                 .addJavadoc("{@inheritDoc}\n")
                 .addAnnotation(Override.class)
@@ -77,7 +87,7 @@ private MethodSpec createMapToUpdateQueryMethodSpec(@NotNull final StorIOContent
                                 INDENT + ".where($S)\n" +
                                 INDENT + ".whereArgs($L)\n" +
                                 INDENT + ".build();\n",
-                        storIOContentResolverTypeMeta.storIOType.uri(),
+                        updateUri,
                         where.get(QueryGenerator.WHERE_CLAUSE),
                         where.get(QueryGenerator.WHERE_ARGS))
                 .build();
diff --git a/storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/ContentResolverProcessorStub.java b/storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/ContentResolverProcessorStub.java
new file mode 100644
index 0000000..91bd654
--- /dev/null
+++ b/storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/ContentResolverProcessorStub.java
@@ -0,0 +1,73 @@
+package com.pushtorefresh.storio.contentresolver.annotations.processor;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.mockito.Mockito;
+
+import java.lang.annotation.Annotation;
+import java.util.Map;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.Name;
+import javax.lang.model.element.TypeElement;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ContentResolverProcessorStub {
+
+    final TestStorIOContentResolverProcessor processor;
+    final TypeElement classElement;
+    final TestClassAnnotation classAnnotation;
+
+    @NotNull
+    static ContentResolverProcessorStub newInstance() {
+        return new ContentResolverProcessorStub();
+    }
+
+    public ContentResolverProcessorStub() {
+        processor = mock(TestStorIOContentResolverProcessor.class, Mockito.CALLS_REAL_METHODS);
+        classAnnotation = mock(TestClassAnnotation.class);
+
+        classElement = mock(TypeElement.class);
+        Name qualifiedName = mock(Name.class);
+        when(qualifiedName.toString()).thenReturn("ClassElementName");
+        when(classElement.getQualifiedName()).thenReturn(qualifiedName);
+    }
+
+    protected static abstract class TestStorIOContentResolverProcessor extends StorIOContentResolverProcessor {
+
+        // region Public Morozov
+        @Override
+        public void validateAnnotatedField(@NotNull Element annotatedField) {
+            super.validateAnnotatedField(annotatedField);
+        }
+
+        public void validateUris(
+                @NotNull TypeElement classElement,
+                @Nullable String commonUri,
+                @NotNull Map operationUriMap) {
+            //noinspection unchecked
+            super.validateUris(classElement, commonUri, operationUriMap);
+        }
+        // endregion
+
+        @NotNull
+        @Override
+        public Class<? extends Annotation> getTypeAnnotationClass() {
+            return TestClassAnnotation.class;
+        }
+
+        @NotNull
+        @Override
+        protected Class<? extends Annotation> getColumnAnnotationClass() {
+            return TestFieldAnnotation.class;
+        }
+    }
+
+    static abstract class TestClassAnnotation implements Annotation {
+    }
+
+    static abstract class TestFieldAnnotation implements Annotation {
+    }
+}
diff --git a/storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/UriValidationTest.java b/storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/UriValidationTest.java
new file mode 100644
index 0000000..1f59350
--- /dev/null
+++ b/storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/UriValidationTest.java
@@ -0,0 +1,61 @@
+package com.pushtorefresh.storio.contentresolver.annotations.processor;
+
+import com.pushtorefresh.storio.common.annotations.processor.ProcessingException;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class UriValidationTest {
+
+    @Rule
+    public ExpectedException expectedException = ExpectedException.none();
+
+    @Test
+    public void successIfCommonNameExist() {
+        ContentResolverProcessorStub stub = ContentResolverProcessorStub.newInstance();
+        stub.processor.validateUris(stub.classElement, "content://testUri", Collections.emptyMap());
+    }
+
+    @Test
+    public void successIfNamesForOperationsExist() {
+        ContentResolverProcessorStub stub = ContentResolverProcessorStub.newInstance();
+        stub.processor.validateUris(stub.classElement, "", Collections.singletonMap("operation", "content://testUri"));
+    }
+
+    @Test
+    public void failIfNameForOperationIsEmpty() {
+        ContentResolverProcessorStub stub = ContentResolverProcessorStub.newInstance();
+
+        Map<String, String> operationNameMap = new HashMap<String, String>(3);
+        operationNameMap.put("insert", "content://insertUri");
+        operationNameMap.put("update", "");
+        operationNameMap.put("delete", "content://deleteUri");
+
+        expectedException.expect(ProcessingException.class);
+        expectedException.expectMessage("Uri of ClassElementName annotated " +
+                "with TestClassAnnotation is null or empty for operation update");
+
+        stub.processor.validateUris(stub.classElement, "", operationNameMap);
+    }
+
+    @Test
+    public void failIfAllNamesAreEmpty() {
+        ContentResolverProcessorStub stub = ContentResolverProcessorStub.newInstance();
+
+        Map<String, String> operationNameMap = new HashMap<String, String>(3);
+        operationNameMap.put("insert", "");
+        operationNameMap.put("update", "");
+        operationNameMap.put("delete", "");
+
+        expectedException.expect(ProcessingException.class);
+        expectedException.expectMessage("Uri of ClassElementName annotated " +
+                "with TestClassAnnotation is null or empty"); // Without operation marker
+
+        stub.processor.validateUris(stub.classElement, "", operationNameMap);
+    }
+}
diff --git a/storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/DeleteResolverGeneratorTest.java b/storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/DeleteResolverGeneratorTest.java
index a99ae3d..ebbc6f5 100644
--- a/storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/DeleteResolverGeneratorTest.java
+++ b/storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/DeleteResolverGeneratorTest.java
@@ -6,6 +6,7 @@
 import com.pushtorefresh.storio.contentresolver.annotations.processor.introspection.StorIOContentResolverTypeMeta;
 import com.squareup.javapoet.JavaFile;
 
+import org.jetbrains.annotations.NotNull;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -16,11 +17,9 @@
 
 public class DeleteResolverGeneratorTest {
 
-    @Test
-    public void generateJavaFile() throws IOException {
-        final StorIOContentResolverType storIOContentResolverType = mock(StorIOContentResolverType.class);
-
-        when(storIOContentResolverType.uri()).thenReturn("content://test");
+    @NotNull
+    private String generateJavaFile(
+            @NotNull StorIOContentResolverType storIOContentResolverType) throws IOException {
 
         final StorIOContentResolverTypeMeta storIOContentResolverTypeMeta = new StorIOContentResolverTypeMeta(
                 "TestItem",
@@ -58,7 +57,17 @@ public void generateJavaFile() throws IOException {
         final StringBuilder out = new StringBuilder();
         javaFile.writeTo(out);
 
-        assertThat(out.toString()).isEqualTo("package com.test;\n" +
+        return out.toString();
+    }
+
+    @Test
+    public void generateJavaFileWithCommonUri() throws IOException {
+        final StorIOContentResolverType storIOContentResolverType = mock(StorIOContentResolverType.class);
+
+        when(storIOContentResolverType.uri()).thenReturn("content://test");
+
+        String javaFileAsString = generateJavaFile(storIOContentResolverType);
+        assertThat(javaFileAsString).isEqualTo("package com.test;\n" +
                 "\n" +
                 "import android.support.annotation.NonNull;\n" +
                 "import com.pushtorefresh.storio.contentresolver.operations.delete.DefaultDeleteResolver;\n" +
@@ -83,4 +92,107 @@ public void generateJavaFile() throws IOException {
                 "    }\n" +
                 "}\n");
     }
+
+    @Test
+    public void generateJavaFileWithOperationSpecificUri() throws IOException {
+        final StorIOContentResolverType storIOContentResolverType = mock(StorIOContentResolverType.class);
+
+        when(storIOContentResolverType.deleteUri()).thenReturn("content://delete_test");
+
+        String javaFileAsString = generateJavaFile(storIOContentResolverType);
+        assertThat(javaFileAsString).isEqualTo("package com.test;\n" +
+                "\n" +
+                "import android.support.annotation.NonNull;\n" +
+                "import com.pushtorefresh.storio.contentresolver.operations.delete.DefaultDeleteResolver;\n" +
+                "import com.pushtorefresh.storio.contentresolver.queries.DeleteQuery;\n" +
+                "import java.lang.Override;\n" +
+                "\n" +
+                "/**\n" +
+                " * Generated resolver for Delete Operation\n" +
+                " */\n" +
+                "public class TestItemStorIOContentResolverDeleteResolver extends DefaultDeleteResolver<TestItem> {\n" +
+                "    /**\n" +
+                "     * {@inheritDoc}\n" +
+                "     */\n" +
+                "    @Override\n" +
+                "    @NonNull\n" +
+                "    protected DeleteQuery mapToDeleteQuery(@NonNull TestItem object) {\n" +
+                "        return DeleteQuery.builder()\n" +
+                "            .uri(\"content://delete_test\")\n" +   // Operation specific
+                "            .where(null)\n" +
+                "            .whereArgs(null)\n" +
+                "            .build();\n" +
+                "    }\n" +
+                "}\n");
+    }
+
+    @Test
+    public void operationSpecificUriShouldHaveHigherPriority() throws IOException {
+        final StorIOContentResolverType storIOContentResolverType = mock(StorIOContentResolverType.class);
+
+        when(storIOContentResolverType.uri()).thenReturn("content://test");
+        when(storIOContentResolverType.deleteUri()).thenReturn("content://delete_test");
+
+        String javaFileAsString = generateJavaFile(storIOContentResolverType);
+        assertThat(javaFileAsString).isEqualTo("package com.test;\n" +
+                "\n" +
+                "import android.support.annotation.NonNull;\n" +
+                "import com.pushtorefresh.storio.contentresolver.operations.delete.DefaultDeleteResolver;\n" +
+                "import com.pushtorefresh.storio.contentresolver.queries.DeleteQuery;\n" +
+                "import java.lang.Override;\n" +
+                "\n" +
+                "/**\n" +
+                " * Generated resolver for Delete Operation\n" +
+                " */\n" +
+                "public class TestItemStorIOContentResolverDeleteResolver extends DefaultDeleteResolver<TestItem> {\n" +
+                "    /**\n" +
+                "     * {@inheritDoc}\n" +
+                "     */\n" +
+                "    @Override\n" +
+                "    @NonNull\n" +
+                "    protected DeleteQuery mapToDeleteQuery(@NonNull TestItem object) {\n" +
+                "        return DeleteQuery.builder()\n" +
+                "            .uri(\"content://delete_test\")\n" +   // Operation specific
+                "            .where(null)\n" +
+                "            .whereArgs(null)\n" +
+                "            .build();\n" +
+                "    }\n" +
+                "}\n");
+    }
+
+    @Test
+    public void shouldUseCommonUriIfSpecifiedOnlyForAnotherOperations() throws IOException {
+        final StorIOContentResolverType storIOContentResolverType = mock(StorIOContentResolverType.class);
+
+        when(storIOContentResolverType.uri()).thenReturn("content://test");
+        when(storIOContentResolverType.insertUri()).thenReturn("content://insert_test");
+        when(storIOContentResolverType.updateUri()).thenReturn("content://update_test");
+        // There is no explicit uri for delete operation
+
+        String javaFileAsString = generateJavaFile(storIOContentResolverType);
+        assertThat(javaFileAsString).isEqualTo("package com.test;\n" +
+                "\n" +
+                "import android.support.annotation.NonNull;\n" +
+                "import com.pushtorefresh.storio.contentresolver.operations.delete.DefaultDeleteResolver;\n" +
+                "import com.pushtorefresh.storio.contentresolver.queries.DeleteQuery;\n" +
+                "import java.lang.Override;\n" +
+                "\n" +
+                "/**\n" +
+                " * Generated resolver for Delete Operation\n" +
+                " */\n" +
+                "public class TestItemStorIOContentResolverDeleteResolver extends DefaultDeleteResolver<TestItem> {\n" +
+                "    /**\n" +
+                "     * {@inheritDoc}\n" +
+                "     */\n" +
+                "    @Override\n" +
+                "    @NonNull\n" +
+                "    protected DeleteQuery mapToDeleteQuery(@NonNull TestItem object) {\n" +
+                "        return DeleteQuery.builder()\n" +
+                "            .uri(\"content://test\")\n" +   // Common uri
+                "            .where(null)\n" +
+                "            .whereArgs(null)\n" +
+                "            .build();\n" +
+                "    }\n" +
+                "}\n");
+    }
 }
diff --git a/storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/PutResolverGeneratorTest.java b/storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/PutResolverGeneratorTest.java
index 8a1cd43..7e78e92 100644
--- a/storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/PutResolverGeneratorTest.java
+++ b/storio-content-resolver-annotations-processor/src/test/java/com/pushtorefresh/storio/contentresolver/annotations/processor/generate/PutResolverGeneratorTest.java
@@ -6,6 +6,7 @@
 import com.pushtorefresh.storio.contentresolver.annotations.processor.introspection.StorIOContentResolverTypeMeta;
 import com.squareup.javapoet.JavaFile;
 
+import org.jetbrains.annotations.NotNull;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -17,14 +18,10 @@
 public class PutResolverGeneratorTest {
 
     @SuppressWarnings("ConstantConditions")
-    @Test
-    public void generateJavaFile() throws IOException {
+    @NotNull
+    private String generateJavaFile(@NotNull StorIOContentResolverType storIOContentResolverType) throws IOException {
         final PutResolverGenerator putResolverGenerator = new PutResolverGenerator();
 
-        final StorIOContentResolverType storIOContentResolverType = mock(StorIOContentResolverType.class);
-
-        when(storIOContentResolverType.uri()).thenReturn("content://test");
-
         final StorIOContentResolverTypeMeta storIOContentResolverTypeMeta = new StorIOContentResolverTypeMeta("TestItem", "com.test", storIOContentResolverType);
 
         final StorIOContentResolverColumn storIOContentResolverColumn1 = mock(StorIOContentResolverColumn.class);
@@ -55,7 +52,17 @@ public void generateJavaFile() throws IOException {
         final StringBuilder out = new StringBuilder();
         javaFile.writeTo(out);
 
-        assertThat(out.toString()).isEqualTo("package com.test;\n" +
+        return out.toString();
+    }
+
+    @Test
+    public void generateJavaFileWithCommonUri() throws IOException {
+        final StorIOContentResolverType storIOContentResolverType = mock(StorIOContentResolverType.class);
+
+        when(storIOContentResolverType.uri()).thenReturn("content://test");
+
+        String javaFileAsString = generateJavaFile(storIOContentResolverType);
+        assertThat(javaFileAsString).isEqualTo("package com.test;\n" +
                 "\n" +
                 "import android.content.ContentValues;\n" +
                 "import android.support.annotation.NonNull;\n" +
@@ -107,4 +114,189 @@ public void generateJavaFile() throws IOException {
                 "    }\n" +
                 "}\n");
     }
+
+    @Test
+    public void generateJavaFileWithOperationSpecificUri() throws IOException {
+        final StorIOContentResolverType storIOContentResolverType = mock(StorIOContentResolverType.class);
+
+        when(storIOContentResolverType.insertUri()).thenReturn("content://insert_test");
+        when(storIOContentResolverType.updateUri()).thenReturn("content://update_test");
+
+        String javaFileAsString = generateJavaFile(storIOContentResolverType);
+        assertThat(javaFileAsString).isEqualTo("package com.test;\n" +
+                "\n" +
+                "import android.content.ContentValues;\n" +
+                "import android.support.annotation.NonNull;\n" +
+                "import com.pushtorefresh.storio.contentresolver.operations.put.DefaultPutResolver;\n" +
+                "import com.pushtorefresh.storio.contentresolver.queries.InsertQuery;\n" +
+                "import com.pushtorefresh.storio.contentresolver.queries.UpdateQuery;\n" +
+                "import java.lang.Override;\n" +
+                "\n" +
+                "/**\n" +
+                " * Generated resolver for Put Operation\n" +
+                " */\n" +
+                "public class TestItemStorIOContentResolverPutResolver extends DefaultPutResolver<TestItem> {\n" +
+                "    /**\n" +
+                "     * {@inheritDoc}\n" +
+                "     */\n" +
+                "    @Override\n" +
+                "    @NonNull\n" +
+                "    protected InsertQuery mapToInsertQuery(@NonNull TestItem object) {\n" +
+                "        return InsertQuery.builder()\n" +
+                "            .uri(\"content://insert_test\")\n" +   // Operation specific
+                "            .build();\n" +
+                "    }\n" +
+                "\n" +
+                "    /**\n" +
+                "     * {@inheritDoc}\n" +
+                "     */\n" +
+                "    @Override\n" +
+                "    @NonNull\n" +
+                "    protected UpdateQuery mapToUpdateQuery(@NonNull TestItem object) {\n" +
+                "        return UpdateQuery.builder()\n" +
+                "            .uri(\"content://update_test\")\n" +   // Operation specific
+                "            .where(\"column1 = ?\")\n" +
+                "            .whereArgs(object.column1Field)\n" +
+                "            .build();\n" +
+                "    }\n" +
+                "\n" +
+                "    /**\n" +
+                "     * {@inheritDoc}\n" +
+                "     */\n" +
+                "    @Override\n" +
+                "    @NonNull\n" +
+                "    public ContentValues mapToContentValues(@NonNull TestItem object) {\n" +
+                "        ContentValues contentValues = new ContentValues(2);\n" +
+                "\n" +
+                "        contentValues.put(\"column1\", object.column1Field);\n" +
+                "        contentValues.put(\"column2\", object.column2Field);\n" +
+                "\n" +
+                "        return contentValues;\n" +
+                "    }\n" +
+                "}\n");
+    }
+
+    @Test
+    public void operationSpecificUriShouldHaveHigherPriority() throws IOException {
+        final StorIOContentResolverType storIOContentResolverType = mock(StorIOContentResolverType.class);
+
+        when(storIOContentResolverType.uri()).thenReturn("content://test");
+        when(storIOContentResolverType.insertUri()).thenReturn("content://insert_test");
+        when(storIOContentResolverType.updateUri()).thenReturn("content://update_test");
+
+        String javaFileAsString = generateJavaFile(storIOContentResolverType);
+        assertThat(javaFileAsString).isEqualTo("package com.test;\n" +
+                "\n" +
+                "import android.content.ContentValues;\n" +
+                "import android.support.annotation.NonNull;\n" +
+                "import com.pushtorefresh.storio.contentresolver.operations.put.DefaultPutResolver;\n" +
+                "import com.pushtorefresh.storio.contentresolver.queries.InsertQuery;\n" +
+                "import com.pushtorefresh.storio.contentresolver.queries.UpdateQuery;\n" +
+                "import java.lang.Override;\n" +
+                "\n" +
+                "/**\n" +
+                " * Generated resolver for Put Operation\n" +
+                " */\n" +
+                "public class TestItemStorIOContentResolverPutResolver extends DefaultPutResolver<TestItem> {\n" +
+                "    /**\n" +
+                "     * {@inheritDoc}\n" +
+                "     */\n" +
+                "    @Override\n" +
+                "    @NonNull\n" +
+                "    protected InsertQuery mapToInsertQuery(@NonNull TestItem object) {\n" +
+                "        return InsertQuery.builder()\n" +
+                "            .uri(\"content://insert_test\")\n" +   // Operation specific
+                "            .build();\n" +
+                "    }\n" +
+                "\n" +
+                "    /**\n" +
+                "     * {@inheritDoc}\n" +
+                "     */\n" +
+                "    @Override\n" +
+                "    @NonNull\n" +
+                "    protected UpdateQuery mapToUpdateQuery(@NonNull TestItem object) {\n" +
+                "        return UpdateQuery.builder()\n" +
+                "            .uri(\"content://update_test\")\n" +   // Operation specific
+                "            .where(\"column1 = ?\")\n" +
+                "            .whereArgs(object.column1Field)\n" +
+                "            .build();\n" +
+                "    }\n" +
+                "\n" +
+                "    /**\n" +
+                "     * {@inheritDoc}\n" +
+                "     */\n" +
+                "    @Override\n" +
+                "    @NonNull\n" +
+                "    public ContentValues mapToContentValues(@NonNull TestItem object) {\n" +
+                "        ContentValues contentValues = new ContentValues(2);\n" +
+                "\n" +
+                "        contentValues.put(\"column1\", object.column1Field);\n" +
+                "        contentValues.put(\"column2\", object.column2Field);\n" +
+                "\n" +
+                "        return contentValues;\n" +
+                "    }\n" +
+                "}\n");
+    }
+
+    @Test
+    public void shouldUseCommonUriIfSpecifiedOnlyForAnotherOperations() throws IOException {
+        final StorIOContentResolverType storIOContentResolverType = mock(StorIOContentResolverType.class);
+
+        when(storIOContentResolverType.uri()).thenReturn("content://test");
+        when(storIOContentResolverType.deleteUri()).thenReturn("content://delete_test");
+        // There is no explicit uri for insert and update operations
+
+        String javaFileAsString = generateJavaFile(storIOContentResolverType);
+        assertThat(javaFileAsString).isEqualTo("package com.test;\n" +
+                "\n" +
+                "import android.content.ContentValues;\n" +
+                "import android.support.annotation.NonNull;\n" +
+                "import com.pushtorefresh.storio.contentresolver.operations.put.DefaultPutResolver;\n" +
+                "import com.pushtorefresh.storio.contentresolver.queries.InsertQuery;\n" +
+                "import com.pushtorefresh.storio.contentresolver.queries.UpdateQuery;\n" +
+                "import java.lang.Override;\n" +
+                "\n" +
+                "/**\n" +
+                " * Generated resolver for Put Operation\n" +
+                " */\n" +
+                "public class TestItemStorIOContentResolverPutResolver extends DefaultPutResolver<TestItem> {\n" +
+                "    /**\n" +
+                "     * {@inheritDoc}\n" +
+                "     */\n" +
+                "    @Override\n" +
+                "    @NonNull\n" +
+                "    protected InsertQuery mapToInsertQuery(@NonNull TestItem object) {\n" +
+                "        return InsertQuery.builder()\n" +
+                "            .uri(\"content://test\")\n" +   // Common uri
+                "            .build();\n" +
+                "    }\n" +
+                "\n" +
+                "    /**\n" +
+                "     * {@inheritDoc}\n" +
+                "     */\n" +
+                "    @Override\n" +
+                "    @NonNull\n" +
+                "    protected UpdateQuery mapToUpdateQuery(@NonNull TestItem object) {\n" +
+                "        return UpdateQuery.builder()\n" +
+                "            .uri(\"content://test\")\n" +   // Common uri
+                "            .where(\"column1 = ?\")\n" +
+                "            .whereArgs(object.column1Field)\n" +
+                "            .build();\n" +
+                "    }\n" +
+                "\n" +
+                "    /**\n" +
+                "     * {@inheritDoc}\n" +
+                "     */\n" +
+                "    @Override\n" +
+                "    @NonNull\n" +
+                "    public ContentValues mapToContentValues(@NonNull TestItem object) {\n" +
+                "        ContentValues contentValues = new ContentValues(2);\n" +
+                "\n" +
+                "        contentValues.put(\"column1\", object.column1Field);\n" +
+                "        contentValues.put(\"column2\", object.column2Field);\n" +
+                "\n" +
+                "        return contentValues;\n" +
+                "    }\n" +
+                "}\n");
+    }
 }
diff --git a/storio-content-resolver-annotations/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/StorIOContentResolverType.java b/storio-content-resolver-annotations/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/StorIOContentResolverType.java
index 392edf1..2c1eaa2 100644
--- a/storio-content-resolver-annotations/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/StorIOContentResolverType.java
+++ b/storio-content-resolver-annotations/src/main/java/com/pushtorefresh/storio/contentresolver/annotations/StorIOContentResolverType.java
@@ -14,9 +14,34 @@
 public @interface StorIOContentResolverType {
 
     /**
-     * Required: Specifies uri
+     * Optional: Specifies uri for all operations
+     * Should be set as default if there is no uri for some particular operation
      *
      * @return uri
+     * @see #insertUri()
+     * @see #updateUri()
+     * @see #deleteUri()
      */
-    String uri();
+    String uri() default "";
+
+    /**
+     * Optional: Specifies uri for insert operation
+     *
+     * @return uri
+     */
+    String insertUri() default "";
+
+    /**
+     * Optional: Specifies uri for update operation
+     *
+     * @return uri
+     */
+    String updateUri() default "";
+
+    /**
+     * Optional: Specifies uri for delete operation
+     *
+     * @return uri
+     */
+    String deleteUri() default "";
 }
