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.cache.FileTemplateLoader 19import freemarker.cache.MultiTemplateLoader 20import freemarker.cache.TemplateLoader 21import freemarker.template.Configuration 22import freemarker.template.DefaultObjectWrapper 23import freemarker.template.Template 24import org.gradle.api.GradleException 25import org.gradle.api.file.FileVisitDetails 26import org.gradle.api.tasks.InputDirectory 27import org.gradle.api.tasks.OutputDirectory 28import org.gradle.api.tasks.SourceTask 29import org.gradle.api.tasks.TaskAction 30 31 32class ApplyTemplates extends SourceTask { 33 /** 34 * Freemarker context object 35 */ 36 def Configuration cfg = new freemarker.template.Configuration() 37 38 /** 39 * The root directory for output files. All output file paths 40 * are assumed to be relative to this root. 41 */ 42 @OutputDirectory 43 public outputDir = project.projectDir 44 45 /** 46 * Include directory. The templates in this directory will not be 47 * processed directly, but will be accessible to other templates 48 * via the <#include> directive. 49 */ 50 def include = project.file("$project.projectDir/templates/include") 51 52 /** 53 * List of file extensions that indicate a file to be processed, rather 54 * than simply copied. 55 */ 56 def extensionsToProcess = ['ftl'] 57 58 /** 59 * List of file extensions that should be completely ignored by this 60 * task. File extensions that appear in neither this list nor the list 61 * specified by {@link #extensionsToProcess} are copied into the destination 62 * without processing. 63 */ 64 def extensionsToIgnore = ['ftli'] 65 66 /** 67 * A String -> String closure that transforms a (relative) input path into a 68 * (relative) output path. This closure is responsible for any alterations to 69 * the output path, including pathname substitution and extension removal. 70 */ 71 Closure<String> filenameTransform 72 73 /** 74 * The hash which will be passed to the freemarker template engine. This hash 75 * is used by the freemarker script as input data. 76 * The hash should contain a key named "meta". The template processor will add 77 * processing data to this key. 78 */ 79 def parameters 80 81 /** 82 * The main action for this task. Visits each file in the source directories and 83 * either processes, copies, or ignores it. The action taken for each file depends 84 * on the contents of {@link #extensionsToProcess} and {@link #extensionsToIgnore}. 85 */ 86 @TaskAction 87 def applyTemplate() { 88 // Create a list of Freemarker template loaders based on the 89 // source tree(s) of this task. The loader list establishes a virtual 90 // file system for freemarker templates; the template language can 91 // load files, and each load request will have its path resolved 92 // against this set of loaders. 93 println "Gathering template load locations:" 94 def List loaders = [] 95 source.asFileTrees.each { 96 src -> 97 println " ${src.dir}" 98 loaders.add(0, new FileTemplateLoader(project.file(src.dir))) 99 } 100 101 // Add the include path(s) to the list of loaders. 102 println "Gathering template include locations:" 103 include = project.fileTree(include) 104 include.asFileTrees.each { 105 inc -> 106 println " ${inc.dir}" 107 loaders.add(0, new FileTemplateLoader(project.file(inc.dir))) 108 } 109 // Add the loaders to the freemarker config 110 cfg.setTemplateLoader(new MultiTemplateLoader(loaders.toArray(new TemplateLoader[1]))) 111 112 // Set the wrapper that will be used to convert the template parameters hash into 113 // the internal freemarker data model. The default wrapper is capable of handling a 114 // mix of POJOs/POGOs and XML nodes, so we'll use that. 115 cfg.setObjectWrapper(new DefaultObjectWrapper()) 116 117 // This is very much like setting the target SDK level in Android. 118 cfg.setIncompatibleEnhancements("2.3.20") 119 120 // Add an implicit <#include 'common.ftl' to the top of every file. 121 // TODO: should probably be a parameter instead of hardcoded like this. 122 cfg.addAutoInclude('common.ftl') 123 124 // Visit every file in the source tree(s) 125 def processTree = source.getAsFileTree() 126 processTree.visit { 127 FileVisitDetails input -> 128 def inputFile = input.getRelativePath().toString() 129 def outputFile = input.getRelativePath().getFile(project.file(outputDir)) 130 // Get the input and output files, and make sure the output path exists 131 def renamedOutput = filenameTransform(outputFile.toString()) 132 outputFile = project.file(renamedOutput) 133 134 if (input.directory){ 135 // create the output directory. This probably will have already been 136 // created as part of processing the files *in* the directory, but 137 // do it here anyway to support empty directories. 138 outputFile.mkdirs() 139 } else { 140 // We may or may not see the directory before we see the files 141 // in that directory, so create it here 142 outputFile.parentFile.mkdirs() 143 144 // Check the input file extension against the process/ignore list 145 def extension = "NONE" 146 def extensionPattern = ~/.*\.(\w*)$/ 147 def extensionMatch = extensionPattern.matcher(inputFile) 148 if (extensionMatch.matches()) { 149 extension = extensionMatch[0][1] 150 } 151 // If the extension is in the process list, put the input through freemarker 152 if (extensionsToProcess.contains(extension)){ 153 print '[freemarker] PROCESS: ' 154 println "$inputFile -> $outputFile" 155 156 try { 157 def Template tpl = this.cfg.getTemplate(inputFile) 158 def FileWriter out = new FileWriter(outputFile) 159 160 // Add the output file path to parameters.meta so that the freemarker 161 // script can access it. 162 parameters.meta.put("outputFile", "${outputFile}") 163 tpl.process(parameters, out) 164 } catch (e) { 165 println e.message 166 throw new GradleException("Error processing ${inputFile}: ${e.message}") 167 } 168 } else if (!extensionsToIgnore.contains(extension)) { 169 // if it's not processed and not ignored, then it must be copied. 170 print '[freemarker] COPY: ' 171 println "$inputFile -> $outputFile" 172 input.copyTo(outputFile); 173 } 174 } 175 } 176 } 177} 178