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