Skip to content

Commit

Permalink
Support for Kotlin properties (#776)
Browse files Browse the repository at this point in the history
* Initial support for Kotlin properties in sqlite variant

* Finish tests for Kotlin properties support in sqlite variant

* Add Kotlin properties support to content resolver variant
  • Loading branch information
geralt-encore authored Apr 18, 2017
1 parent da7a1f5 commit 6d92d70
Show file tree
Hide file tree
Showing 59 changed files with 1,834 additions and 72 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.pushtorefresh.storio.common.annotations.processor

fun String.startsWithIs(): Boolean = this.startsWith("is") && this.length > 2
&& Character.isUpperCase(this[2])
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.pushtorefresh.storio.common.annotations.processor.introspection.StorI
import javax.annotation.processing.*
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ElementKind.*
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier.*
Expand All @@ -30,6 +31,10 @@ abstract class StorIOAnnotationsProcessor<TypeMeta : StorIOTypeMeta<*, *>, out C
private lateinit var typeUtils: Types
protected lateinit var messager: Messager

// cashing getters and setters for private fields to avoid second pass since we already
// have result after the validation step
protected val accessorsMap = mutableMapOf<String, Pair<String, String>>()

/**
* Processes class annotations.
*
Expand Down Expand Up @@ -76,7 +81,7 @@ abstract class StorIOAnnotationsProcessor<TypeMeta : StorIOTypeMeta<*, *>, out C
/**
* Checks that element annotated with [StorIOColumnMeta] satisfies all required conditions.
*
* @param annotatedElement an annotated field
* @param annotatedElement an annotated field or method
*/
@Throws(SkipNotAnnotatedClassWithAnnotatedParentException::class)
protected fun validateAnnotatedFieldOrMethod(annotatedElement: Element) {
Expand All @@ -99,7 +104,13 @@ abstract class StorIOAnnotationsProcessor<TypeMeta : StorIOTypeMeta<*, *>, out C
}

if (PRIVATE in annotatedElement.modifiers) {
throw ProcessingException(annotatedElement, "${columnAnnotationClass.simpleName} can not be applied to private field or method: ${annotatedElement.simpleName}")
if (annotatedElement.kind == FIELD) {
if (!findGetterAndSetterForPrivateField(annotatedElement)) {
throw ProcessingException(annotatedElement, "${columnAnnotationClass.simpleName} can not be applied to private field without corresponding getter and setter or private method: ${annotatedElement.simpleName}")
}
} else {
throw ProcessingException(annotatedElement, "${columnAnnotationClass.simpleName} can not be applied to private field without corresponding getter and setter or private method: ${annotatedElement.simpleName}")
}
}

if (annotatedElement.kind == FIELD && FINAL in annotatedElement.modifiers) {
Expand Down Expand Up @@ -141,6 +152,53 @@ abstract class StorIOAnnotationsProcessor<TypeMeta : StorIOTypeMeta<*, *>, out C
}
}

/**
* Checks that field is accessible via corresponding getter and setter.
* Cashes names of elements getter and setter into [accessorsMap].
*
* @param annotatedElement an annotated field
*/
protected fun findGetterAndSetterForPrivateField(annotatedElement: Element): Boolean {
val name = annotatedElement.simpleName.toString()
var getter: String? = null
var setter: String? = null
annotatedElement.enclosingElement.enclosedElements.forEach { element ->
if (element.kind == ElementKind.METHOD) {
val method = element as ExecutableElement
val methodName = method.simpleName.toString()
// check if it is a valid getter
if ((methodName == String.format("get%s", name.capitalize())
|| methodName == String.format("is%s", name.capitalize())
// Special case for properties which name starts with is.
// Kotlin will generate getter with the same name instead of isIsProperty.
|| methodName == name && name.startsWithIs())
&& !method.modifiers.contains(PRIVATE)
&& !method.modifiers.contains(STATIC)
&& method.parameters.isEmpty()
&& method.returnType == annotatedElement.asType()) {
getter = methodName
}
// check if it is a valid setter
if ((methodName == String.format("set%s", name.capitalize())
// Special case for properties which name starts with is.
// Kotlin will generate setter with setProperty name instead of setIsProperty.
|| name.startsWithIs() && methodName == String.format("set%s", name.substring(2, name.length)))
&& !method.modifiers.contains(PRIVATE)
&& !method.modifiers.contains(STATIC)
&& method.parameters.size == 1
&& method.parameters[0].asType() == annotatedElement.asType()) {
setter = methodName
}
}
}
if (getter == null || setter == null) {
return false
} else {
accessorsMap += name to (getter!! to setter!!)
return true
}
}

@Synchronized override fun init(processingEnv: ProcessingEnvironment) {
super.init(processingEnv)
filer = processingEnv.filer
Expand Down Expand Up @@ -245,4 +303,4 @@ abstract class StorIOAnnotationsProcessor<TypeMeta : StorIOTypeMeta<*, *>, out C
protected abstract fun createDeleteResolver(): Generator<TypeMeta>

protected abstract fun createMapping(): Generator<TypeMeta>
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ open class StorIOColumnMeta<out ColumnAnnotation : Annotation>(
val element: Element,
val elementName: String,
val javaType: JavaType,
val storIOColumn: ColumnAnnotation) {
val storIOColumn: ColumnAnnotation,
val getter: String? = null,
val setter: String? = null) {

val isMethod: Boolean
get() = element.kind == ElementKind.METHOD
Expand All @@ -21,6 +23,16 @@ open class StorIOColumnMeta<out ColumnAnnotation : Annotation>(
else -> elementName
}

val needAccessors: Boolean
get() = getter != null && setter != null

val contextAwareName: String
get() = when {
isMethod -> "$elementName()"
needAccessors -> "$getter()"
else -> elementName
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
Expand All @@ -32,6 +44,8 @@ open class StorIOColumnMeta<out ColumnAnnotation : Annotation>(
if (elementName != other.elementName) return false
if (javaType != other.javaType) return false
if (storIOColumn != other.storIOColumn) return false
if (getter != other.getter) return false
if (setter != other.setter) return false

return true
}
Expand All @@ -42,13 +56,15 @@ open class StorIOColumnMeta<out ColumnAnnotation : Annotation>(
result = 31 * result + elementName.hashCode()
result = 31 * result + javaType.hashCode()
result = 31 * result + storIOColumn.hashCode()
result = 31 * result + (getter?.hashCode() ?: 0)
result = 31 * result + (setter?.hashCode() ?: 0)
return result
}

override fun toString() = "StorIOColumnMeta(enclosingElement=$enclosingElement, element=$element, elementName='$elementName', javaType=$javaType, storIOColumn=$storIOColumn)"
override fun toString(): String = "StorIOColumnMeta(enclosingElement=$enclosingElement, element=$element, elementName='$elementName', javaType=$javaType, storIOColumn=$storIOColumn, getter='$getter', setter='$setter')"

private fun decapitalize(str: String) = when {
str.length > 1 -> Character.toLowerCase(str[0]) + str.substring(1)
else -> str.toLowerCase()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,8 @@ class StorIOColumnMetaTest {
@Test
fun toStringValidation() {
// given
val columnMeta = StorIOColumnMeta(elementMock, elementMock, "TEST", javaType, annotationMock)
val expectedString = "StorIOColumnMeta(enclosingElement=$elementMock," +
" element=$elementMock, elementName='TEST', javaType=" + javaType +
", storIOColumn=" + annotationMock + ')'
val columnMeta = StorIOColumnMeta(elementMock, elementMock, "TEST", javaType, annotationMock, "getter", "setter")
val expectedString = "StorIOColumnMeta(enclosingElement=$elementMock, element=$elementMock, elementName='TEST', javaType=$javaType, storIOColumn=$annotationMock, getter='getter', setter='setter')"

// when
val toString = columnMeta.toString()
Expand Down Expand Up @@ -112,4 +110,4 @@ class StorIOColumnMetaTest {
assertThat(realName).isEqualTo("iso")
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,25 @@ public void shouldNotCompileIfAnnotatedFieldInsideNotAnnotatedClass() {
}

@Test
public void shouldNotCompileIfAnnotatedFieldIsPrivate() {
JavaFileObject model = JavaFileObjects.forResource("PrivateField.java");
public void shouldNotCompileIfAnnotatedFieldIsPrivateAndDoesNotHaveAccessors() {
JavaFileObject model = JavaFileObjects.forResource("PrivateFieldWithoutAccessors.java");

assert_().about(javaSource())
.that(model)
.processedWith(new StorIOContentResolverProcessor())
.failsToCompile()
.withErrorContaining("StorIOContentResolverColumn can not be applied to private field or method: id");
.withErrorContaining("StorIOContentResolverColumn can not be applied to private field without corresponding getter and setter or private method: id");
}

@Test
public void shouldNotCompileIfAnnotatedMethodIsPrivate() {
JavaFileObject model = JavaFileObjects.forResource("PrivateMethod.java");

assert_().about(javaSource())
.that(model)
.processedWith(new StorIOContentResolverProcessor())
.failsToCompile()
.withErrorContaining("StorIOContentResolverColumn can not be applied to private field without corresponding getter and setter or private method: id");
}

@Test
Expand Down Expand Up @@ -496,4 +507,96 @@ public void shouldCompileWithMethodsReturningBoxedTypesAndMarkedAsIgnoreNullAndF
.generatesSources(generatedTypeMapping, generatedDeleteResolver, generatedGetResolver, generatedPutResolver);
}

@Test
public void shouldNotCompileIfAnnotatedFieldIsPrivateAndDoesNotHaveSetter() {
JavaFileObject model = JavaFileObjects.forResource("PrivateFieldWithoutSetter.java");

assert_().about(javaSource())
.that(model)
.processedWith(new StorIOContentResolverProcessor())
.failsToCompile()
.withErrorContaining("StorIOContentResolverColumn can not be applied to private field without corresponding getter and setter or private method: id");
}

@Test
public void shouldNotCompileIfAnnotatedFieldIsPrivateAndDoesNotHaveGetter() {
JavaFileObject model = JavaFileObjects.forResource("PrivateFieldWithoutGetter.java");

assert_().about(javaSource())
.that(model)
.processedWith(new StorIOContentResolverProcessor())
.failsToCompile()
.withErrorContaining("StorIOContentResolverColumn can not be applied to private field without corresponding getter and setter or private method: id");
}

@Test
public void shouldCompileIfAnnotatedFieldIsPrivateAndHasIsGetter() {
JavaFileObject model = JavaFileObjects.forResource("PrivateFieldWithIsGetter.java");

assert_().about(javaSource())
.that(model)
.processedWith(new StorIOContentResolverProcessor())
.compilesWithoutError();
}

@Test
public void shouldCompileIfAnnotatedFieldIsPrivateAndHasNameStartingWithIs() {
JavaFileObject model = JavaFileObjects.forResource("PrivateFieldWithNameStartingWithIs.java");

assert_().about(javaSource())
.that(model)
.processedWith(new StorIOContentResolverProcessor())
.compilesWithoutError();
}

@Test
public void shouldCompileWithPrivatePrimitiveFieldsWithCorrepsondingAccessors() {
JavaFileObject model = JavaFileObjects.forResource("PrimitivePrivateFields.java");

JavaFileObject generatedTypeMapping = JavaFileObjects.forResource("PrimitivePrivateFieldsContentResolverTypeMapping.java");
JavaFileObject generatedDeleteResolver = JavaFileObjects.forResource("PrimitivePrivateFieldsStorIOContentResolverDeleteResolver.java");
JavaFileObject generatedGetResolver = JavaFileObjects.forResource("PrimitivePrivateFieldsStorIOContentResolverGetResolver.java");
JavaFileObject generatedPutResolver = JavaFileObjects.forResource("PrimitivePrivateFieldsStorIOContentResolverPutResolver.java");

assert_().about(javaSource())
.that(model)
.processedWith(new StorIOContentResolverProcessor())
.compilesWithoutError()
.and()
.generatesSources(generatedTypeMapping, generatedDeleteResolver, generatedGetResolver, generatedPutResolver);
}

@Test
public void shouldCompileWithPrivateBoxedTypesFieldsWithCorrespondingAccessors() {
JavaFileObject model = JavaFileObjects.forResource("BoxedTypesPrivateFields.java");

JavaFileObject generatedTypeMapping = JavaFileObjects.forResource("BoxedTypesPrivateFieldsContentResolverTypeMapping.java");
JavaFileObject generatedDeleteResolver = JavaFileObjects.forResource("BoxedTypesPrivateFieldsStorIOContentResolverDeleteResolver.java");
JavaFileObject generatedGetResolver = JavaFileObjects.forResource("BoxedTypesPrivateFieldsStorIOContentResolverGetResolver.java");
JavaFileObject generatedPutResolver = JavaFileObjects.forResource("BoxedTypesPrivateFieldsStorIOContentResolverPutResolver.java");

assert_().about(javaSource())
.that(model)
.processedWith(new StorIOContentResolverProcessor())
.compilesWithoutError()
.and()
.generatesSources(generatedTypeMapping, generatedDeleteResolver, generatedGetResolver, generatedPutResolver);
}

@Test
public void shouldCompileWithPrivateBoxedTypesFieldsWithCorresondingAccessorsAndMarkedAsIgnoreNull() {
JavaFileObject model = JavaFileObjects.forResource("BoxedTypesPrivateFieldsIgnoreNull.java");

JavaFileObject generatedTypeMapping = JavaFileObjects.forResource("BoxedTypesPrivateFieldsIgnoreNullContentResolverTypeMapping.java");
JavaFileObject generatedDeleteResolver = JavaFileObjects.forResource("BoxedTypesPrivateFieldsIgnoreNullStorIOContentResolverDeleteResolver.java");
JavaFileObject generatedGetResolver = JavaFileObjects.forResource("BoxedTypesPrivateFieldsIgnoreNullStorIOContentResolverGetResolver.java");
JavaFileObject generatedPutResolver = JavaFileObjects.forResource("BoxedTypesPrivateFieldsIgnoreNullStorIOContentResolverPutResolver.java");

assert_().about(javaSource())
.that(model)
.processedWith(new StorIOContentResolverProcessor())
.compilesWithoutError()
.and()
.generatesSources(generatedTypeMapping, generatedDeleteResolver, generatedGetResolver, generatedPutResolver);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.pushtorefresh.storio.contentresolver.annotations;

@StorIOContentResolverType(uri = "content://uri")
public class BoxedTypesPrivateFields {

@StorIOContentResolverColumn(name = "field1")
private Boolean field1;

@StorIOContentResolverColumn(name = "field2")
private Short field2;

@StorIOContentResolverColumn(name = "field3")
private Integer field3;

@StorIOContentResolverColumn(name = "field4", key = true)
private Long field4;

@StorIOContentResolverColumn(name = "field5")
private Float field5;

@StorIOContentResolverColumn(name = "field6")
private Double field6;

public Boolean getField1() {
return field1;
}

public void setField1(Boolean field1) {
this.field1 = field1;
}

public Short getField2() {
return field2;
}

public void setField2(Short field2) {
this.field2 = field2;
}

public Integer getField3() {
return field3;
}

public void setField3(Integer field3) {
this.field3 = field3;
}

public Long getField4() {
return field4;
}

public void setField4(Long field4) {
this.field4 = field4;
}

public Float getField5() {
return field5;
}

public void setField5(Float field5) {
this.field5 = field5;
}

public Double getField6() {
return field6;
}

public void setField6(Double field6) {
this.field6 = field6;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.pushtorefresh.storio.contentresolver.annotations;

import com.pushtorefresh.storio.contentresolver.ContentResolverTypeMapping;

/**
* Generated mapping with collection of resolvers
*/
public class BoxedTypesPrivateFieldsContentResolverTypeMapping extends ContentResolverTypeMapping<BoxedTypesPrivateFields> {
public BoxedTypesPrivateFieldsContentResolverTypeMapping() {
super(new BoxedTypesPrivateFieldsStorIOContentResolverPutResolver(),
new BoxedTypesPrivateFieldsStorIOContentResolverGetResolver(),
new BoxedTypesPrivateFieldsStorIOContentResolverDeleteResolver());
}
}
Loading

0 comments on commit 6d92d70

Please sign in to comment.