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 */ 16 package com.example.android.samples.build 17 18 import freemarker.ext.dom.NodeModel 19 import groovy.transform.Canonical 20 import org.gradle.api.GradleException 21 import org.gradle.api.Project 22 import 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 34 class 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