@@ -8,12 +8,11 @@ import com.thinkinglogic.builder.annotation.Mutable
88import com.thinkinglogic.builder.annotation.NullableType
99import org.jetbrains.annotations.NotNull
1010import java.io.File
11+ import java.util.*
1112import java.util.stream.Collectors
1213import javax.annotation.processing.*
1314import javax.lang.model.SourceVersion
14- import javax.lang.model.element.Element
15- import javax.lang.model.element.TypeElement
16- import javax.lang.model.element.VariableElement
15+ import javax.lang.model.element.*
1716import javax.lang.model.type.ArrayType
1817import javax.lang.model.type.DeclaredType
1918import javax.lang.model.type.PrimitiveType
@@ -60,30 +59,36 @@ class BuilderProcessor : AbstractProcessor() {
6059 val sourceRootFile = File (generatedSourcesRoot)
6160 sourceRootFile.mkdir()
6261
63- annotatedElements.forEach { annotatedClass ->
64- if (annotatedClass !is TypeElement ) {
65- annotatedClass.errorMessage { " Invalid element type, expected a class" }
66- return @forEach
62+ annotatedElements.forEach { annotatedElement ->
63+ when (annotatedElement.kind) {
64+ ElementKind .CLASS -> writeBuilderForClass(annotatedElement as TypeElement , sourceRootFile)
65+ ElementKind .CONSTRUCTOR -> writeBuilderForConstructor(annotatedElement as ExecutableElement , sourceRootFile)
66+ else -> annotatedElement.errorMessage { " Invalid element type, expected a class or constructor" }
6767 }
68-
69- writeBuilder(annotatedClass, sourceRootFile)
7068 }
7169
7270 return false
7371 }
7472
75- /* *
76- * Writes the source code to create a builder for [classToBuild] within the [sourceRoot] directory.
77- */
78- private fun writeBuilder (classToBuild : TypeElement , sourceRoot : File ) {
73+ /* * Invokes [writeBuilder] to create a builder for the given [classElement]. */
74+ private fun writeBuilderForClass (classElement : TypeElement , sourceRootFile : File ) {
75+ writeBuilder(classElement, classElement.fieldsForBuilder(), sourceRootFile)
76+ }
77+
78+ /* * Invokes [writeBuilder] to create a builder for the given [constructor]. */
79+ private fun writeBuilderForConstructor (constructor : ExecutableElement , sourceRootFile : File ) {
80+ writeBuilder(constructor .enclosingElement as TypeElement , constructor .parameters, sourceRootFile)
81+ }
82+
83+ /* * Writes the source code to create a builder for [classToBuild] within the [sourceRoot] directory. */
84+ private fun writeBuilder (classToBuild : TypeElement , fields : List <VariableElement >, sourceRoot : File ) {
7985 val packageName = processingEnv.elementUtils.getPackageOf(classToBuild).toString()
8086 val builderClassName = " ${classToBuild.simpleName} Builder"
8187
8288 processingEnv.noteMessage { " Writing $packageName .$builderClassName " }
8389
8490 val builderSpec = TypeSpec .classBuilder(builderClassName)
8591 val builderClass = ClassName (packageName, builderClassName)
86- val fields = classToBuild.fieldsForBuilder()
8792
8893 fields.forEach { field ->
8994 processingEnv.noteMessage { " Adding field: $field " }
@@ -100,9 +105,7 @@ class BuilderProcessor : AbstractProcessor() {
100105 .writeTo(sourceRoot)
101106 }
102107
103- /* *
104- * Returns all fields in this type that also appear as a constructor parameter.
105- */
108+ /* * Returns all fields in this type that also appear as a constructor parameter. */
106109 private fun TypeElement.fieldsForBuilder (): List <VariableElement > {
107110 val allMembers = processingEnv.elementUtils.getAllMembers(this )
108111 val fields = fieldsIn(allMembers)
@@ -114,9 +117,7 @@ class BuilderProcessor : AbstractProcessor() {
114117 return fields.filter { constructorParamNames.contains(it.simpleName.toString()) }
115118 }
116119
117- /* *
118- * Creates a 'build()' function that will invoke a constructor for [returnType], passing [fields] as arguments and returning the new instance.
119- */
120+ /* * Creates a 'build()' function that will invoke a constructor for [returnType], passing [fields] as arguments and returning the new instance. */
120121 private fun createBuildFunction (fields : List <Element >, returnType : TypeElement ): FunSpec {
121122 val code = StringBuilder (" $CHECK_REQUIRED_FIELDS_FUNCTION_NAME ()" )
122123 code.appendln().append(" return ${returnType.simpleName} (" )
@@ -139,13 +140,10 @@ class BuilderProcessor : AbstractProcessor() {
139140 .build()
140141 }
141142
142- /* *
143- * Creates a function that will invoke [check] to confirm that each required field is populated.
144- */
143+ /* * Creates a function that will invoke [check] to confirm that each required field is populated. */
145144 private fun createCheckRequiredFieldsFunction (fields : List <Element >): FunSpec {
146145 val code = StringBuilder ()
147- fields
148- .filterNot { it.isNullable() }
146+ fields.filterNot { it.isNullable() }
149147 .forEach { field ->
150148 code.append(" check(${field.simpleName} != null, {\" ${field.simpleName} must not be null\" })" ).appendln()
151149 }
@@ -156,20 +154,16 @@ class BuilderProcessor : AbstractProcessor() {
156154 .build()
157155 }
158156
159- /* *
160- * Creates a property for the field identified by this element.
161- */
157+ /* * Creates a property for the field identified by this element. */
162158 private fun Element.asProperty (): PropertySpec =
163159 PropertySpec .varBuilder(simpleName.toString(), asKotlinTypeName().asNullable(), KModifier .PRIVATE )
164- .initializer(" ${ defaultValue()} " )
160+ .initializer(defaultValue())
165161 .build()
166162
167- /* *
168- * Returns the correct default value for this element - the value of any [DefaultValue] annotation, or "null".
169- */
163+ /* * Returns the correct default value for this element - the value of any [DefaultValue] annotation, or "null". */
170164 private fun Element.defaultValue (): String {
171165 return if (hasAnnotation(DefaultValue ::class .java)) {
172- val default = this .getAnnotation (DefaultValue ::class .java).value
166+ val default = this .findAnnotation (DefaultValue ::class .java).value
173167 // make sure that strings are wrapped in quotes
174168 return if (asType().toString() == " java.lang.String" && ! default.startsWith(" \" " )) {
175169 " \" $default \" "
@@ -181,9 +175,7 @@ class BuilderProcessor : AbstractProcessor() {
181175 }
182176 }
183177
184- /* *
185- * Creates a function that sets the property identified by this element, and returns the [builder].
186- */
178+ /* * Creates a function that sets the property identified by this element, and returns the [builder]. */
187179 private fun Element.asSetterFunctionReturning (builder : ClassName ): FunSpec {
188180 val fieldType = asKotlinTypeName()
189181 val parameterClass = if (isNullable()) {
@@ -206,27 +198,17 @@ class BuilderProcessor : AbstractProcessor() {
206198 var typeName = asType().asKotlinTypeName()
207199 if (typeName is ParameterizedTypeName ) {
208200 if (hasAnnotation(NullableType ::class .java)
209- && verify (typeName.typeArguments.isNotEmpty(), " NullableType annotation should not be applied to a property without type arguments!" )) {
201+ && assert (typeName.typeArguments.isNotEmpty(), " NullableType annotation should not be applied to a property without type arguments!" )) {
210202 typeName = typeName.withNullableType()
211203 }
212204 if (hasAnnotation(Mutable ::class .java)
213- && verify (MUTABLE_COLLECTIONS .containsKey(typeName.rawType), " Mutable annotation should not be applied to non-mutable collections!" )) {
205+ && assert (MUTABLE_COLLECTIONS .containsKey(typeName.rawType), " Mutable annotation should not be applied to non-mutable collections!" )) {
214206 typeName = typeName.asMutableCollection()
215207 }
216208 }
217209 return typeName
218210 }
219211
220- /* *
221- * Returns the given [fact], logging an error message if it is not true.
222- */
223- private fun Element.verify (fact : Boolean , message : String ): Boolean {
224- if (! fact) {
225- this .errorMessage { message }
226- }
227- return fact
228- }
229-
230212 /* *
231213 * Converts this type to one containing nullable elements.
232214 *
@@ -251,12 +233,14 @@ class BuilderProcessor : AbstractProcessor() {
251233 val mutable = MUTABLE_COLLECTIONS [rawType]!!
252234 .parameterizedBy(* this .typeArguments.toTypedArray())
253235 .annotated(this .annotations)
254- return if (nullable) { mutable.asNullable() } else { mutable }
236+ return if (nullable) {
237+ mutable.asNullable()
238+ } else {
239+ mutable
240+ }
255241 }
256242
257- /* *
258- * Converts this TypeMirror to a [TypeName], ensuring that java types such as [java.lang.String] are converted to their Kotlin equivalent.
259- */
243+ /* * Converts this TypeMirror to a [TypeName], ensuring that java types such as [java.lang.String] are converted to their Kotlin equivalent. */
260244 private fun TypeMirror.asKotlinTypeName (): TypeName {
261245 return when (this ) {
262246 is PrimitiveType -> processingEnv.typeUtils.boxedClass(this as PrimitiveType ? ).asKotlinClassName()
@@ -279,9 +263,7 @@ class BuilderProcessor : AbstractProcessor() {
279263 }
280264 }
281265
282- /* *
283- * Converts this element to a [ClassName], ensuring that java types such as [java.lang.String] are converted to their Kotlin equivalent.
284- */
266+ /* * Converts this element to a [ClassName], ensuring that java types such as [java.lang.String] are converted to their Kotlin equivalent. */
285267 private fun TypeElement.asKotlinClassName (): ClassName {
286268 val className = asClassName()
287269 return try {
@@ -293,22 +275,79 @@ class BuilderProcessor : AbstractProcessor() {
293275 }
294276 }
295277
278+ /* * Returns the [TypeElement] represented by this [TypeMirror]. */
296279 private fun TypeMirror.asTypeElement () = processingEnv.typeUtils.asElement(this ) as TypeElement
297280
281+ /* * Returns true as long as this [Element] is not a [PrimitiveType] and does not have the [NotNull] annotation. */
298282 private fun Element.isNullable (): Boolean {
299283 if (this .asType() is PrimitiveType ) {
300284 return false
301285 }
302286 return ! hasAnnotation(NotNull ::class .java)
303287 }
304288
305- private fun Element.hasAnnotation (annotationClass : Class <* >): Boolean {
289+ /* *
290+ * Returns true if this element has the specified [annotation], or if the parent class has a matching constructor parameter with the annotation.
291+ * (This is necessary because builder annotations can be applied to both fields and constructor parameters - and constructor parameters take precedence.
292+ * Rather than require clients to specify, for instance, `@field:NullableType`, this method also checks for annotations of constructor parameters
293+ * when this element is a field).
294+ */
295+ private fun Element.hasAnnotation (annotation : Class <* >): Boolean {
296+ return hasAnnotationDirectly(annotation) || hasAnnotationViaConstructorParameter(annotation)
297+ }
298+
299+ /* * Return true if this element has the specified [annotation]. */
300+ private fun Element.hasAnnotationDirectly (annotation : Class <* >): Boolean {
306301 return this .annotationMirrors
307302 .map { it.annotationType.toString() }
308303 .toSet()
309- .contains(annotationClass.name)
304+ .contains(annotation.name)
305+ }
306+
307+ /* * Return true if there is a constructor parameter with the same name as this element that has the specified [annotation]. */
308+ private fun Element.hasAnnotationViaConstructorParameter (annotation : Class <* >): Boolean {
309+ val parameterAnnotations = getConstructorParameter()?.annotationMirrors ? : listOf ()
310+ return parameterAnnotations
311+ .map { it.annotationType.toString() }
312+ .toSet()
313+ .contains(annotation.name)
314+ }
315+
316+ /* * Returns the first constructor parameter with the same name as this element, if any such exists. */
317+ private fun Element.getConstructorParameter (): VariableElement ? {
318+ val enclosingElement = this .enclosingElement
319+ return if (enclosingElement is TypeElement ) {
320+ val allMembers = processingEnv.elementUtils.getAllMembers(enclosingElement)
321+ constructorsIn(allMembers)
322+ .flatMap { it.parameters }
323+ .firstOrNull { it.simpleName == this .simpleName }
324+ } else {
325+ null
326+ }
327+ }
328+
329+ /* *
330+ * Returns the given annotation, retrieved from this element directly, or from the corresponding constructor parameter.
331+ *
332+ * @throws NullPointerException if no such annotation can be found - use [hasAnnotation] before calling this method.
333+ */
334+ private fun <A : Annotation > Element.findAnnotation (annotation : Class <A >): A {
335+ return if (hasAnnotationDirectly(annotation)) {
336+ getAnnotation(annotation)
337+ } else {
338+ getConstructorParameter()!! .getAnnotation(annotation)
339+ }
340+ }
341+
342+ /* * Returns the given [assertion], logging an error message if it is not true. */
343+ private fun Element.assert (assertion : Boolean , message : String ): Boolean {
344+ if (! assertion) {
345+ this .errorMessage { message }
346+ }
347+ return assertion
310348 }
311349
350+ /* * Prints an error message using this element as a position hint. */
312351 private fun Element.errorMessage (message : () -> String ) {
313352 processingEnv.messager.printMessage(ERROR , message(), this )
314353 }
0 commit comments