NavWriter.kt revision 1503d52153986fdcfe7e744795010708b7410892
1/* 2 * Copyright 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package androidx.navigation.safe.args.generator 18 19import androidx.navigation.safe.args.generator.ext.N 20import androidx.navigation.safe.args.generator.ext.S 21import androidx.navigation.safe.args.generator.ext.T 22import androidx.navigation.safe.args.generator.models.Action 23import androidx.navigation.safe.args.generator.models.Argument 24import androidx.navigation.safe.args.generator.models.Destination 25import androidx.navigation.safe.args.generator.models.accessor 26import com.squareup.javapoet.ClassName 27import com.squareup.javapoet.CodeBlock 28import com.squareup.javapoet.FieldSpec 29import com.squareup.javapoet.JavaFile 30import com.squareup.javapoet.MethodSpec 31import com.squareup.javapoet.TypeSpec 32import javax.lang.model.element.Modifier 33 34private const val NAVIGATION_PACKAGE = "androidx.navigation" 35private val NAV_DIRECTION_CLASSNAME: ClassName = ClassName.get(NAVIGATION_PACKAGE, "NavDirections") 36private val NAV_OPTIONS_CLASSNAME: ClassName = ClassName.get(NAVIGATION_PACKAGE, "NavOptions") 37private val BUNDLE_CLASSNAME: ClassName = ClassName.get("android.os", "Bundle") 38 39private class ClassWithArgsSpecs(val args: List<Argument>) { 40 41 fun fieldSpecs() = args.map { arg -> 42 FieldSpec.builder(arg.type.typeName(), arg.name) 43 .apply { 44 addModifiers(Modifier.PRIVATE) 45 if (arg.isOptional()) { 46 initializer(arg.defaultValue!!.write()) 47 } 48 } 49 .build() 50 } 51 52 fun setters(thisClassName: ClassName) = args.map { (name, type) -> 53 MethodSpec.methodBuilder("set${name.capitalize()}") 54 .addModifiers(Modifier.PUBLIC) 55 .addParameter(type.typeName(), name) 56 .addStatement("this.$N = $N", name, name) 57 .addStatement("return this") 58 .returns(thisClassName) 59 .build() 60 } 61 62 fun constructor() = MethodSpec.constructorBuilder().apply { 63 addModifiers(Modifier.PUBLIC) 64 args.filterNot(Argument::isOptional).forEach { (argName, type) -> 65 addParameter(type.typeName(), argName) 66 addStatement("this.$N = $N", argName, argName) 67 } 68 }.build() 69 70 fun toBundleMethod(name: String) = MethodSpec.methodBuilder(name).apply { 71 addModifiers(Modifier.PUBLIC) 72 returns(BUNDLE_CLASSNAME) 73 val bundleName = "__outBundle" 74 addStatement("$T $N = new $T()", BUNDLE_CLASSNAME, bundleName, BUNDLE_CLASSNAME) 75 args.forEach { (argName, type) -> 76 addStatement("$N.$N($S, $N)", bundleName, type.bundlePutMethod(), argName, argName) 77 } 78 addStatement("return $N", bundleName) 79 }.build() 80 81 fun copyProperties(to: String, from: String) = CodeBlock.builder() 82 .apply { 83 args.forEach { arg -> addStatement("$to.${arg.name} = $from.${arg.name}") } 84 } 85 .build() 86 87 fun getters() = args.map { arg -> 88 MethodSpec.methodBuilder("get${arg.name.capitalize()}") 89 .addModifiers(Modifier.PUBLIC) 90 .addStatement("return $N", arg.name) 91 .returns(arg.type.typeName()) 92 .build() 93 } 94} 95 96fun generateDestinationDirectionsTypeSpec( 97 className: ClassName, 98 destination: Destination): TypeSpec { 99 val actionTypes = destination.actions.map { action -> 100 action to generateDirectionsTypeSpec(action) 101 } 102 103 val getters = actionTypes 104 .map { (action, actionType) -> 105 val constructor = actionType.methodSpecs.find(MethodSpec::isConstructor)!! 106 val params = constructor.parameters.joinToString(", ") { param -> param.name } 107 val actionTypeName = ClassName.get("", actionType.name) 108 MethodSpec.methodBuilder(action.id.name) 109 .addModifiers(Modifier.PUBLIC, Modifier.STATIC) 110 .addParameters(constructor.parameters) 111 .returns(actionTypeName) 112 .addStatement("return new $T($params)", actionTypeName) 113 .build() 114 } 115 116 return TypeSpec.classBuilder(className) 117 .addModifiers(Modifier.PUBLIC) 118 .addTypes(actionTypes.map { (_, actionType) -> actionType }) 119 .addMethods(getters) 120 .build() 121} 122 123fun generateDirectionsTypeSpec(action: Action): TypeSpec { 124 val specs = ClassWithArgsSpecs(action.args) 125 126 val getDestIdMethod = MethodSpec.methodBuilder("getDestinationId") 127 .addModifiers(Modifier.PUBLIC) 128 .returns(Int::class.java) 129 .addStatement("return $N", action.destination.accessor()) 130 .build() 131 132 val getNavOptions = MethodSpec.methodBuilder("getOptions") 133 .returns(NAV_OPTIONS_CLASSNAME) 134 .addModifiers(Modifier.PUBLIC) 135 .addStatement("return null") 136 .build() 137 138 val className = ClassName.get("", action.id.name.capitalize()) 139 return TypeSpec.classBuilder(className) 140 .addSuperinterface(NAV_DIRECTION_CLASSNAME) 141 .addModifiers(Modifier.PUBLIC, Modifier.STATIC) 142 .addFields(specs.fieldSpecs()) 143 .addMethod(specs.constructor()) 144 .addMethods(specs.setters(className)) 145 .addMethod(specs.toBundleMethod("getArguments")) 146 .addMethod(getDestIdMethod) 147 .addMethod(getNavOptions) 148 .build() 149} 150 151internal fun generateArgsJavaFile(destination: Destination): JavaFile { 152 val destName = destination.name 153 ?: throw IllegalStateException("Destination with arguments must have name") 154 val className = ClassName.get(destName.packageName(), "${destName.simpleName()}Args") 155 val args = destination.args 156 val specs = ClassWithArgsSpecs(args) 157 158 val fromBundleMethod = MethodSpec.methodBuilder("fromBundle").apply { 159 addModifiers(Modifier.PUBLIC, Modifier.STATIC) 160 val bundle = "bundle" 161 addParameter(BUNDLE_CLASSNAME, bundle) 162 returns(className) 163 val result = "result" 164 addStatement("$T $N = new $T()", className, result, className) 165 args.forEach { arg -> 166 beginControlFlow("if ($N.containsKey($S))", bundle, arg.name).apply { 167 addStatement("$N.$N = $N.$N($S)", result, arg.name, bundle, 168 arg.type.bundleGetMethod(), arg.name) 169 } 170 if (!arg.isOptional()) { 171 nextControlFlow("else") 172 addStatement("throw new $T($S)", IllegalArgumentException::class.java, 173 "Required argument \"${arg.name}\" is missing and does " + 174 "not have an android:defaultValue") 175 } 176 endControlFlow() 177 } 178 addStatement("return $N", result) 179 }.build() 180 181 val constructor = MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build() 182 183 val copyConstructor = MethodSpec.constructorBuilder() 184 .addModifiers(Modifier.PUBLIC) 185 .addParameter(className, "original") 186 .addCode(specs.copyProperties("this", "original")) 187 .build() 188 189 val buildMethod = MethodSpec.methodBuilder("build") 190 .returns(className) 191 .addStatement("$T result = new $T()", className, className) 192 .addCode(specs.copyProperties("result", "this")) 193 .addStatement("return result") 194 .build() 195 196 val builderClassName = ClassName.get("", "Builder") 197 val builderTypeSpec = TypeSpec.classBuilder("Builder") 198 .addModifiers(Modifier.PUBLIC, Modifier.STATIC) 199 .addFields(specs.fieldSpecs()) 200 .addMethod(copyConstructor) 201 .addMethod(specs.constructor()) 202 .addMethod(buildMethod) 203 .addMethods(specs.setters(builderClassName)) 204 .addMethods(specs.getters()) 205 .build() 206 207 val typeSpec = TypeSpec.classBuilder(className) 208 .addModifiers(Modifier.PUBLIC) 209 .addFields(specs.fieldSpecs()) 210 .addMethod(constructor) 211 .addMethod(fromBundleMethod) 212 .addMethods(specs.getters()) 213 .addMethod(specs.toBundleMethod("toBundle")) 214 .addType(builderTypeSpec) 215 .build() 216 217 return JavaFile.builder(className.packageName(), typeSpec).build() 218} 219 220fun generateDirectionsJavaFile(destination: Destination): JavaFile { 221 val destName = destination.name 222 ?: throw IllegalStateException("Destination with actions must have name") 223 val className = ClassName.get(destName.packageName(), "${destName.simpleName()}Directions") 224 val typeSpec = generateDestinationDirectionsTypeSpec(className, destination) 225 return JavaFile.builder(className.packageName(), typeSpec).build() 226} 227