1/*
2* Copyright 2013 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*/
16package com.example.android.samples.build
17
18import freemarker.ext.dom.NodeModel
19import groovy.transform.Canonical
20import org.gradle.api.GradleException
21import org.gradle.api.Project
22import org.gradle.api.file.FileTree
23
24/**
25 * Gradle extension that holds properties for sample generation.
26 *
27 * The sample generator needs a number of properties whose values can be
28 * inferred by convention from a smaller number of initial properties.
29 * This class defines fields for the initial properties, and getter
30 * methods for the inferred properties. It also defines a small number
31 * of convenience methods for setting up template-generation tasks.
32 */
33@Canonical
34class SampleGenProperties {
35    /**
36     * The Gradle project that this extension is being applied to.
37     */
38    Project project
39
40    /**
41     *  Directory where the top-level sample project lives
42     */
43    def targetProjectPath
44
45    /**
46     * Relative path to samples/common directory
47     */
48    def pathToSamplesCommon
49
50    /**
51     * Relative path to build directory (platform/developers/build)
52     */
53    def pathToBuild
54
55    /**
56     * Java package name for the root package of this sample.
57     */
58    String targetSamplePackage
59
60    /**
61     *
62     * @return The path to the sample project (as opposed to the top-level project, which
63     *         what is that even for anyway?)
64     */
65    String targetSamplePath() {
66        return "${targetProjectPath}/${targetSampleModule()}"
67    }
68
69
70
71    /**
72     *
73     * @return The path that contains common files -- can be cleaned without harming
74     *         the sample
75     */
76    String targetCommonPath() {
77        return "${targetSamplePath()}/src/common/java/com/example/android/common"
78    }
79
80    /**
81     *
82     * @return The path that contains template files -- can be cleaned without harming
83     *         the sample
84     */
85    String targetTemplatePath() {
86        return "${targetSamplePath()}/src/template"
87    }
88
89    /**
90     * The name of this sample (and also of the corresponding .iml file)
91     */
92    String targetSampleName() {
93        return project.file(targetProjectPath).getName()
94    }
95
96    /**
97     * The name of the main module in the sample project
98     */
99    String targetSampleModule() {
100        return "Application"
101    }
102
103    /**
104     * The path to the template parameters file
105     */
106    String templateXml() {
107        return "${targetProjectPath}/template-params.xml"
108    }
109
110    /**
111     * Transforms a package name into a java-style OS dependent path
112     * @param pkg cccc
113     * @return The java-style path to the package's code
114     */
115    String packageAsPath(String pkg) {
116        return pkg.replaceAll(/\./, File.separator)
117    }
118
119    /**
120     * Transforms a path into a java-style package name
121     * @param path The java-style path to the package's code
122     * @return Name of the package to transform
123     */
124    String pathAsPackage(String path) {
125        return path.replaceAll(File.separator, /\./)
126    }
127
128    /**
129     * Returns the path to the common/build/templates directory
130     */
131    String templatesRoot() {
132        return "${targetProjectPath}/${pathToBuild}/templates"
133    }
134
135
136    /**
137     * Returns the path to common/src/java
138     */
139    String commonSourceRoot() {
140        return "${targetProjectPath}/${pathToSamplesCommon}/src/java/com/example/android/common"
141    }
142
143    /**
144     * Returns the path to the template include directory
145     */
146    String templatesInclude() {
147        return "${templatesRoot()}/include"
148    }
149
150    /**
151     * Returns the output file that will be generated for a particular
152     * input, by replacing generic pathnames with project-specific pathnames
153     * and dropping the .ftl extension from freemarker files.
154     *
155     * @param relativeInputPath Input file as a relative path from the template directory
156     * @return Relative output file path
157     */
158    String getOutputForInput(String relativeInputPath) {
159        String outputPath = relativeInputPath
160        outputPath = outputPath.replaceAll('_PROJECT_', targetSampleName())
161        outputPath = outputPath.replaceAll('_MODULE_', targetSampleModule())
162        outputPath = outputPath.replaceAll('_PACKAGE_', packageAsPath(targetSamplePackage))
163
164        // This is kind of a hack; IntelliJ picks up any and all subdirectories named .idea, so
165        // named them ._IDE_ instead. TODO: remove when generating .idea projects is no longer necessary.
166        outputPath = outputPath.replaceAll('_IDE_', "idea")
167        outputPath = outputPath.replaceAll(/\.ftl$/, '')
168
169        // Any file beginning with a dot won't get picked up, so rename them as necessary here.
170        outputPath = outputPath.replaceAll('gitignore', '.gitignore')
171        return outputPath
172    }
173
174    /**
175     * Returns the tree(s) where the templates to be processed live. The template
176     * input paths that are passed to
177     * {@link SampleGenProperties#getOutputForInput(java.lang.String) getOutputForInput}
178     * are relative to the dir element in each tree.
179     */
180    FileTree[] templates() {
181        def result = []
182        def xmlFile = project.file(templateXml())
183        if (xmlFile.exists()) {
184            def xml = new XmlSlurper().parse(xmlFile)
185            xml.template.each { template ->
186                result.add(project.fileTree(dir: "${templatesRoot()}/${template.@src}"))
187            }
188        } else {
189            result.add(project.fileTree(dir: "${templatesRoot()}/create"))
190        }
191        return result;
192    }
193
194    /**
195     * Path(s) of the common directories to copy over to the sample project.
196     */
197    FileTree[] common() {
198        def result = []
199        def xmlFile = project.file(templateXml())
200        if (xmlFile.exists()) {
201            def xml = new XmlSlurper().parse(xmlFile)
202            xml.common.each { common ->
203                println "Adding common/${common.@src} from ${commonSourceRoot()}"
204                result.add(project.fileTree (
205                        dir: "${commonSourceRoot()}",
206                        include: "${common.@src}/**/*"
207                ))
208            }
209        }
210        return result
211    }
212
213    /**
214     * Returns the hash to supply to the freemarker template processor.
215     * This is loaded from the file specified by {@link SampleGenProperties#templateXml()}
216     * if such a file exists, or synthesized with some default parameters if it does not.
217     * In addition, some data about the current project is added to the "meta" key of the
218     * hash.
219     *
220     * @return The hash to supply to freemarker
221     */
222    Map templateParams() {
223        Map result = new HashMap();
224
225        def xmlFile = project.file(templateXml())
226        if (xmlFile.exists()) {
227            // Parse the xml into Freemarker's DOM structure
228            def params = freemarker.ext.dom.NodeModel.parse(xmlFile)
229
230            // Move to the <sample> node and stuff that in our map
231            def sampleNode = (NodeModel)params.exec(['/sample'])
232            result.put("sample", sampleNode)
233        } else {
234            // Fake data for use on creation
235            result.put("sample", [
236                    name:targetSampleName(),
237                    package:targetSamplePackage,
238                    minSdk:4
239            ])
240        }
241
242        // Extra data that some templates find useful
243        result.put("meta", [
244                root: targetProjectPath,
245                module: targetSampleModule(),
246                common: pathToSamplesCommon,
247                build: pathToBuild,
248        ])
249        return result
250    }
251
252
253
254    /**
255     * Generate default values for properties that can be inferred from an existing
256     * generated project, unless those properties have already been
257     * explicitly specified.
258     */
259    void getRefreshProperties() {
260        if (!this.targetProjectPath) {
261            this.targetProjectPath = project.projectDir
262        }
263        def xmlFile = project.file(templateXml())
264        if (xmlFile.exists()) {
265            println "Template XML: $xmlFile"
266            def xml = new XmlSlurper().parse(xmlFile)
267            this.targetSamplePackage = xml.package.toString()
268            println "Target Package: $targetSamplePackage"
269        }
270    }
271
272    /**
273     * Generate default values for creation properties, unless those properties
274     * have already been explicitly specified. This method will attempt to get
275     * these properties interactively from the user if necessary.
276     */
277    void getCreationProperties() {
278        def calledFrom = project.hasProperty('calledFrom') ? new File(project.calledFrom)
279                : project.projectDir
280        calledFrom = calledFrom.getCanonicalPath()
281        println('\n\n\nReady to create project...')
282
283        if (project.hasProperty('pathToSamplesCommon')) {
284            this.pathToSamplesCommon = project.pathToSamplesCommon
285        } else {
286            throw new GradleException (
287                    'create task requires project property pathToSamplesCommon')
288        }
289
290
291        if (project.hasProperty('pathToBuild')) {
292            this.pathToBuild = project.pathToBuild
293        } else {
294            throw new GradleException ('create task requires project property pathToBuild')
295        }
296
297        if (!this.targetProjectPath) {
298            if (project.hasProperty('out')) {
299                this.targetProjectPath = project.out
300            } else {
301                this.targetProjectPath = System.console().readLine(
302                        "\noutput directory [$calledFrom]:")
303                if (this.targetProjectPath.length() <= 0) {
304                    this.targetProjectPath = calledFrom
305                }
306            }
307        }
308
309        if (!this.targetSamplePackage) {
310            def defaultPackage = "com.example.android." +
311                    this.targetSampleName().toLowerCase()
312            this.targetSamplePackage = System.console().readLine(
313                    "\nsample package name[$defaultPackage]:")
314            if (this.targetSamplePackage.length() <= 0) {
315                this.targetSamplePackage = defaultPackage
316            }
317        }
318    }
319
320}
321