1 /*
<lambda>null2 * Copyright 2018 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
17 package androidx.navigation.safeargs.gradle
18
19 import androidx.navigation.safe.args.generator.ErrorMessage
20 import androidx.navigation.safe.args.generator.SafeArgsGenerator
21 import com.google.gson.Gson
22 import com.google.gson.reflect.TypeToken
23 import java.io.File
24 import javax.inject.Inject
25 import org.gradle.api.DefaultTask
26 import org.gradle.api.GradleException
27 import org.gradle.api.file.ConfigurableFileCollection
28 import org.gradle.api.file.DirectoryProperty
29 import org.gradle.api.file.ProjectLayout
30 import org.gradle.api.provider.Property
31 import org.gradle.api.tasks.CacheableTask
32 import org.gradle.api.tasks.Input
33 import org.gradle.api.tasks.InputFiles
34 import org.gradle.api.tasks.OutputDirectory
35 import org.gradle.api.tasks.PathSensitive
36 import org.gradle.api.tasks.PathSensitivity
37 import org.gradle.api.tasks.TaskAction
38 import org.gradle.work.ChangeType
39 import org.gradle.work.Incremental
40 import org.gradle.work.InputChanges
41
42 private const val MAPPING_FILE = "file_mappings.json"
43
44 @CacheableTask
45 abstract class ArgumentsGenerationTask
46 @Inject
47 constructor(private val projectLayout: ProjectLayout) : DefaultTask() {
48 @get:Input abstract val rFilePackage: Property<String>
49
50 @get:Input abstract val applicationId: Property<String>
51
52 @get:Input abstract val useAndroidX: Property<Boolean>
53
54 @get:Input abstract val generateKotlin: Property<Boolean>
55
56 @get:OutputDirectory abstract val outputDir: DirectoryProperty
57
58 @get:PathSensitive(PathSensitivity.RELATIVE)
59 @get:Incremental
60 @get:InputFiles
61 abstract val navigationFiles: ConfigurableFileCollection
62
63 @get:OutputDirectory abstract val incrementalFolder: DirectoryProperty
64
65 private fun generateArgs(navFiles: Collection<File>, out: File) =
66 navFiles
67 .map { file ->
68 val output =
69 SafeArgsGenerator(
70 rFilePackage = rFilePackage.get(),
71 applicationId = applicationId.orNull ?: "",
72 navigationXml = file,
73 outputDir = out,
74 useAndroidX = useAndroidX.get(),
75 generateKotlin = generateKotlin.get()
76 )
77 .generate()
78 Mapping(
79 file.relativeTo(projectLayout.projectDirectory.asFile).path,
80 output.fileNames
81 ) to output.errors
82 }
83 .unzip()
84 .let { (mappings, errorLists) -> mappings to errorLists.flatten() }
85
86 private fun writeMappings(mappings: List<Mapping>) {
87 File(incrementalFolder.asFile.get(), MAPPING_FILE).writer().use {
88 Gson().toJson(mappings, it)
89 }
90 }
91
92 private fun readMappings(): List<Mapping> {
93 val type = object : TypeToken<List<Mapping>>() {}.type
94 val mappingsFile = File(incrementalFolder.asFile.get(), MAPPING_FILE)
95 return if (mappingsFile.exists()) {
96 mappingsFile.reader().use { Gson().fromJson(it, type) }
97 } else {
98 emptyList()
99 }
100 }
101
102 @TaskAction
103 internal fun taskAction(inputs: InputChanges) {
104 if (inputs.isIncremental) {
105 doIncrementalTaskAction(inputs)
106 } else {
107 logger.info("Unable do incremental execution: full task run")
108 doFullTaskAction()
109 }
110 }
111
112 private fun doFullTaskAction() {
113 val outputDirFile = outputDir.asFile.get()
114 if (outputDirFile.exists() && !outputDirFile.deleteRecursively()) {
115 logger.warn("Failed to clear directory for navigation arguments")
116 }
117 if (!outputDirFile.exists() && !outputDirFile.mkdirs()) {
118 throw GradleException("Failed to create directory for navigation arguments")
119 }
120 val (mappings, errors) = generateArgs(navigationFiles.files, outputDirFile)
121 writeMappings(mappings)
122 failIfErrors(errors)
123 }
124
125 private fun doIncrementalTaskAction(inputs: InputChanges) {
126 val modifiedFiles = mutableSetOf<File>()
127 val removedFiles = mutableSetOf<File>()
128 inputs.getFileChanges(navigationFiles).forEach { change ->
129 if (change.changeType == ChangeType.MODIFIED || change.changeType == ChangeType.ADDED) {
130 modifiedFiles.add(change.file)
131 } else if (change.changeType == ChangeType.REMOVED) {
132 removedFiles.add(change.file)
133 }
134 }
135
136 val oldMapping = readMappings()
137 val (newMapping, errors) = generateArgs(modifiedFiles, outputDir.asFile.get())
138 val newJavaFiles = newMapping.flatMap { it.javaFiles }.toSet()
139 val changedInputs = removedFiles + modifiedFiles
140 val (modified, unmodified) =
141 oldMapping.partition {
142 File(projectLayout.projectDirectory.asFile, it.navFile) in changedInputs
143 }
144 modified
145 .flatMap { it.javaFiles }
146 .filter { name -> name !in newJavaFiles }
147 .forEach { javaName ->
148 val fileExtension =
149 if (generateKotlin.get()) {
150 ".kt"
151 } else {
152 ".java"
153 }
154 val fileName = "${javaName.replace('.', File.separatorChar)}$fileExtension"
155 val file = File(outputDir.asFile.get(), fileName)
156 if (file.exists()) {
157 file.delete()
158 }
159 }
160 writeMappings(unmodified + newMapping)
161 failIfErrors(errors)
162 }
163
164 private fun failIfErrors(errors: List<ErrorMessage>) {
165 if (errors.isNotEmpty()) {
166 val errString = errors.joinToString("\n") { it.toClickableText() }
167 throw GradleException(
168 "androidx.navigation.safeargs plugin failed.\n " +
169 "Following errors found: \n$errString"
170 )
171 }
172 }
173 }
174
ErrorMessagenull175 private fun ErrorMessage.toClickableText() =
176 "$path:$line:$column " + "(${File(path).name}:$line): \n" + "error: $message"
177
178 private data class Mapping(val navFile: String, val javaFiles: List<String>)
179