Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Kotlin properties #776

Merged
merged 3 commits into from
Apr 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😐

|| 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