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