blob: ca545132aed1ca1016991473eb88c362d359e78f [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.databinding.tool.writer
import android.databinding.tool.ext.L
import android.databinding.tool.ext.N
import android.databinding.tool.ext.S
import android.databinding.tool.ext.T
import android.databinding.tool.ext.W
import android.databinding.tool.ext.classSpec
import android.databinding.tool.ext.constructorSpec
import android.databinding.tool.ext.fieldSpec
import android.databinding.tool.ext.javaFile
import android.databinding.tool.ext.methodSpec
import android.databinding.tool.ext.parameterSpec
import android.databinding.tool.store.GenClassInfoLog
import android.databinding.tool.writer.ViewBinder.RootNode
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.CodeBlock
import com.squareup.javapoet.NameAllocator
import com.squareup.javapoet.ParameterSpec
import com.squareup.javapoet.TypeName.BOOLEAN
import javax.lang.model.element.Modifier.FINAL
import javax.lang.model.element.Modifier.PRIVATE
import javax.lang.model.element.Modifier.PUBLIC
import javax.lang.model.element.Modifier.STATIC
fun ViewBinder.toJavaFile(useLegacyAnnotations: Boolean = false) =
JavaFileGenerator(this, useLegacyAnnotations).create()
fun ViewBinder.generatedClassInfo() = GenClassInfoLog.GenClass(
qName = generatedTypeName.toString(),
modulePackage = generatedTypeName.packageName(),
variables = emptyMap(),
implementations = emptySet()
)
private class JavaFileGenerator(
private val binder: ViewBinder,
private val useLegacyAnnotations: Boolean
) {
private val annotationPackage =
if (useLegacyAnnotations) "android.support.annotation" else "androidx.annotation"
private val nonNull = ClassName.get(annotationPackage, "NonNull")
private val nullable = ClassName.get(annotationPackage, "Nullable")
private val fieldNames = NameAllocator().apply {
// Since the binding names are used in public fields, allocate those first.
binder.bindings.forEach { binding ->
newName(binding.name, binding)
}
}
private val rootFieldName = fieldNames.newName("rootView")
fun create() = javaFile(binder.generatedTypeName.packageName(), typeSpec()) {
addFileComment("Generated by view binder compiler. Do not edit!")
}
private fun typeSpec() = classSpec(binder.generatedTypeName) {
addModifiers(PUBLIC, FINAL)
val viewBindingPackage = if (useLegacyAnnotations) "android" else "androidx"
addSuperinterface(ClassName.get("$viewBindingPackage.viewbinding", "ViewBinding"))
// TODO elide the separate root field if the root tag has an ID (and isn't a binder)
addField(rootViewField())
addFields(bindingFields())
addMethod(constructor())
addMethod(rootViewGetter())
if (binder.rootNode is RootNode.Merge) {
addMethod(mergeInflate())
} else {
addMethod(oneParamInflate())
addMethod(threeParamInflate())
}
addMethod(bind())
}
private fun rootViewField() = fieldSpec(rootFieldName, binder.rootNode.type) {
addModifiers(PRIVATE, FINAL)
addAnnotation(nonNull)
}
private fun bindingFields() = binder.bindings.map { binding ->
fieldSpec(binding.name, binding.type) {
addModifiers(PUBLIC, FINAL)
// TODO addJavadoc when types were normalized to View due to different declarations.
if (binding.isRequired) {
addAnnotation(nonNull)
} else {
addJavadoc(
renderConfigurationJavadoc(
binding.presentConfigurations,
binding.absentConfigurations
)
)
addAnnotation(nullable)
}
}
}
private fun constructor() = constructorSpec {
addModifiers(PRIVATE)
addParameter(parameterSpec(binder.rootNode.type, rootFieldName) {
addAnnotation(nonNull)
})
addStatement("this.$rootFieldName = $rootFieldName")
binder.bindings.forEach { binding ->
val name = fieldNames.get(binding)
addParameter(parameterSpec(binding.type, name) {
addAnnotation(if (binding.isRequired) nonNull else nullable)
})
addStatement("this.$1N = $1N", name)
}
}
private fun rootViewGetter() = methodSpec("getRoot") {
// TODO addJavadoc about this being the parent if the root tag was <merge> ...right?
addAnnotation(Override::class.java)
addAnnotation(nonNull)
addModifiers(PUBLIC)
returns(binder.rootNode.type)
addStatement("return $rootFieldName")
}
private fun oneParamInflate() = methodSpec("inflate") {
// TODO addJavadoc
addModifiers(PUBLIC, STATIC)
addAnnotation(nonNull)
returns(binder.generatedTypeName)
val inflaterParam = parameterSpec(ANDROID_LAYOUT_INFLATER, "inflater") {
addAnnotation(nonNull)
}
addParameter(inflaterParam)
addStatement("return inflate($N, null, false)", inflaterParam)
}
private fun threeParamInflate() = methodSpec("inflate") {
// TODO addJavadoc
addModifiers(PUBLIC, STATIC)
addAnnotation(nonNull)
returns(binder.generatedTypeName)
val inflaterParam = parameterSpec(ANDROID_LAYOUT_INFLATER, "inflater") {
addAnnotation(nonNull)
}
val parentParam = parameterSpec(ANDROID_VIEW_GROUP, "parent") {
addAnnotation(nullable)
}
val attachToParentParam = parameterSpec(BOOLEAN, "attachToParent")
addParameter(inflaterParam)
addParameter(parentParam)
addParameter(attachToParentParam)
addStatement("$T root = $N.inflate($L, $N, false)",
ANDROID_VIEW, inflaterParam, binder.layoutReference.asCode(), parentParam)
beginControlFlow("if ($N)", attachToParentParam)
addStatement("$N.addView(root)", parentParam)
endControlFlow()
addStatement("return bind(root)")
}
private fun mergeInflate() = methodSpec("inflate") {
// TODO addJavadoc
addModifiers(PUBLIC, STATIC)
addAnnotation(nonNull)
returns(binder.generatedTypeName)
val inflaterParam = parameterSpec(ANDROID_LAYOUT_INFLATER, "inflater") {
addAnnotation(nonNull)
}
val parentParam = parameterSpec(ANDROID_VIEW_GROUP, "parent") {
addAnnotation(nonNull)
}
addParameter(inflaterParam)
addParameter(parentParam)
beginControlFlow("if ($N == null)", parentParam)
addStatement("throw new $T($S)", NullPointerException::class.java, parentParam.name)
endControlFlow()
addStatement("$N.inflate($L, $N)",
inflaterParam, binder.layoutReference.asCode(), parentParam)
addStatement("return bind($N)", parentParam)
}
private fun bind() = methodSpec("bind") {
// TODO addJavadoc
addModifiers(PUBLIC, STATIC)
addAnnotation(nonNull)
returns(binder.generatedTypeName)
// We use a dedicated name allocator here because we want the public parameter name to take
// precedence over any view with a matching ID which is only used as a local.
val localNames = NameAllocator()
val rootParam = parameterSpec(ANDROID_VIEW, localNames.newName("rootView")) {
addAnnotation(nonNull)
}
addParameter(rootParam)
val rootBinding = (binder.rootNode as? RootNode.Binding)?.binding
val nonRootBindings = binder.bindings.filter { it !== rootBinding }
if (nonRootBindings.isEmpty()) {
// Without any bindings that invoke findViewById, an erroneously-null rootView can be
// propagated into the binding instance and its non-null getRoot() method. Synthesize
// an explicit null check to prevent this.
beginControlFlow("if ($N == null)", rootParam)
addStatement("throw new $T($S)", NullPointerException::class.java, rootParam.name)
endControlFlow()
addCode("\n")
}
/** Non-null when error-handling is being generated. */
val id: String?
if (nonRootBindings.any { it.isRequired }) {
addComment("The body of this method is generated in a way you would not otherwise write.")
addComment("This is done to optimize the compiled bytecode for size and performance.")
id = localNames.newName("id")
addStatement("int $id")
// By using a named block and break statements, the generated code compiles to bytecode
// which optimizes for the common case of all required views being present. It also allows
// de-duplicating the exception handling code to save bytecode size.
beginControlFlow("missingId:")
} else {
id = null
}
val constructorParams = mutableListOf<CodeBlock>()
constructorParams += rootParam.asViewReference(binder.rootNode.type)
binder.bindings.forEach { binding ->
val viewName = localNames.newName(binding.name)
val viewType = when (binding.form) {
ViewBinding.Form.View -> binding.type
ViewBinding.Form.Binder -> ANDROID_VIEW
}
val viewInitializer = if (binding === rootBinding) {
// If this corresponds to the root binding, we can re-use the input View argument.
rootParam.asViewReference(viewType)
} else if (binding.isRequired) {
// Place the id value first into the local in case it's needed for error handling.
addStatement("$id = $L", binding.id.asCode())
CodeBlock.of("$N.findViewById($id)", rootParam)
} else {
CodeBlock.of("$N.findViewById($L)", rootParam, binding.id.asCode())
}
addStatement("$T $viewName = $L", viewType, viewInitializer)
if (binding.isRequired && binding !== rootBinding) {
beginControlFlow("if ($viewName == null)")
addStatement("break missingId")
endControlFlow()
}
val constructorParam = when (binding.form) {
ViewBinding.Form.View -> viewName
ViewBinding.Form.Binder -> {
val binderName = localNames.newName("binding_${binding.name}")
if (binding.isRequired) {
addStatement("$1T $binderName = $1T.bind($viewName)", binding.type)
} else {
addStatement("""
$1T $binderName = $viewName != null
? $1T.bind($viewName)
: null
""".trimIndent(), binding.type)
}
binderName
}
}
constructorParams += CodeBlock.of(L, constructorParam)
addCode("\n")
}
addStatement("return new $T($L)", binder.generatedTypeName,
CodeBlock.join(constructorParams, ",$W"))
if (id != null) {
endControlFlow()
val missingId = localNames.newName("missingId")
addStatement(
"$T $missingId = $N.getResources().getResourceName($id)",
String::class.java,
rootParam
)
// String.concat(String) produces less bytecode than '+' (StringBuilder).
addStatement(
"throw new $T($S.concat($missingId))",
NullPointerException::class.java,
"Missing required view with ID: "
)
}
}
/** Return a [CodeBlock] reference to [this] as [viewType], emitting a cast if needed. */
private fun ParameterSpec.asViewReference(viewType: ClassName): CodeBlock {
return if (viewType != ANDROID_VIEW) {
CodeBlock.of("($T) $N", viewType, this)
} else {
CodeBlock.of(N, this)
}
}
/** Return the storage type for the view backing a [RootNode]. */
private val RootNode.type get() = when (this) {
is RootNode.Merge -> ANDROID_VIEW
is RootNode.View -> type
is RootNode.Binding -> binding.type
}
}