Preface
Instructions in this guide also refer to the Template Testing Companion for testing templates during development.
Getting Started
The first thing you want to do is set up a build for your templates, because creating and publishing a template involves several steps:
-
Create the directory structure and files for the template
-
Add VERSION, DESCRIPTION, and README files
-
Package the template structure into a zip, filtering out .retain,
VERSION, andDESCRIPTIONfiles -
Publish the zip file to a repository by copying the manifest file and zip files to a file or http(s) URL.
Fortunately this is very easy because you can use Skeletal to set a project up for you! Simply run
skeletal create lazybones-project my-lzb-templates
and you’ll get top-level my-lzb-templates directory which contains a simple Gradle build file, gradle wrapper, a README.md, and a templates directory into which you put your templates.
my-lzb-templates |-- gradle | `-- wrapper | |-- gradle-wrapper.jar | `-- gradle-wrapper.properties |-- templates (1) |-- build.gradle |-- gradlew |-- gradlew.bat `-- README.md
| 1 | Directory for project templates |
The build.gradle file applies the Skeletal Gradle Plugin which provides the packaging and publishing commands and some properties needed for publishing.
plugins {
id "net.codebuilders.lazybones-templates" version "1.6.2" (1)
}
lazybones {
packageExclude "**/*.swp", "**/*.swo", "**/.gradle" (2)
templateOwner = "<owner of templates in this project>" (3)
}
| 1 | Apply the Skeletal Gradle Plugin |
| 2 | Ant-style pattern for excluding files |
| 3 | Owner used for published templates |
The next step is to create the template.
Creating a Template
Lazybones templates are simple zip files containing a directory structure and a bunch of files. How you create that zip file is up to you, but we’re going to use the build that was created for us. It handles both the packaging and publishing of templates, so we don’t have to worry about the details.
To get a project layout to begin with, we will use the gradle init command
to create a simple Java application with Spock
tests and a Gradle build. We are using a template naming
convention of app type-language-test type-build tool to differentiate
between templates, but you can use whatever convention that best suites your needs.
Create a new directory templates/java-gradle.
from a shell in this directory:
gradle init
-
Select type of project to generate:
application -
Select implementation language:
Java -
Split functionality across multiple subprojects?:
no -
Select build script DSL:
Groovy -
Generate build using new APIs and behavior:
no -
Select test framework:
Spock -
Project name:
java-gradle -
Source package:
org.example
|-- app (2) | |-- src | | |-- main | | | |-- java | | | | `-- org | | | | `-- example | | | | `-- App.java (3) | | | `-- resources | | `-- test | | |-- groovy | | | `-- org | | | `-- example | | | `-- AppTest.groovy (4) | | `-- resources | `-- build.gradle (5) |-- gradle | `-- wrapper | |-- gradle-wrapper.jar | `-- gradle-wrapper.properties |-- .gitattributes |-- .gitignore |-- gradlew |-- gradlew.bat `-- settings.gradle (1)
Gradle multi-project build layout with:
| 1 | settings file with sub-projects listed |
| 2 | app sub-project |
| 3 | sample application |
| 4 | sample test |
| 5 | app sub-project build file |
In java-gradle create these files. See below for information on
their contents and also reference our sample in lazybones-templates/templates/java-gradle
since they may be more complete than the basic content shown here.
-
README.md - a text file that contains information about the template.
-
VERSION - a text file containing the current version number of the template.
-
DESCRIPTION - a text file containing the description of the template
Adding an empty .retain file in a template allows us to include empty
directories in both a git repository and the template zip. The build simply
excludes .retain files when packaging the template while maintaining the
directory structure. Since the .retain files can be empty, a simple
touch src/main/java/.retain is sufficient.
Add empty .retain files as shown here:
-
app/src/main/resources/.retain
-
app/src/test/resources/.retain
The app/build.gradle file is part of this template project and contains:
plugins { (1)
id 'groovy'
id 'application'
}
repositories {
mavenCentral() (2)
}
dependencies { (3)
testImplementation 'org.codehaus.groovy:groovy:3.0.10'
testImplementation 'org.spockframework:spock-core:2.1-groovy-3.0'
testImplementation 'junit:junit:4.13.2'
implementation 'com.google.guava:guava:31.0.1-jre'
}
application {
mainClass = 'org.example.App' (4)
}
tasks.named('test') {
// Use JUnit Platform for unit tests.
useJUnitPlatform()
}
| 1 | Apply the Groovy and the Java application plugins |
| 2 | Use the Maven Central repository for dependencies |
| 3 | Dependencies |
| 4 | Specify the main class |
The VERSION file is required by the build, because that’s how the build
knows what the current version of the template is. Just put any version string
into the file:
0.1.0
No quotes. No markup. Just the version text. Note that the build excludes this file from the template zip as the version is included in the zip file’s name.
The DESCRIPTION file is required by the publishing for the description shown
during the skeletal list command. Just put a description string into the file:
A simple Java application template
As with the VERSION file, no quotes or markup, just text.
Finally, README.md contains some information about the template. This is
displayed immediately after a new project is created from the template, so it
should offer some guidance on what the template provides and what steps to
take next with the new project. Add this to the file:
Simple Java Spock Gradle Application Project -------------------------------------------- You have just created a simple Java application. There is a standard project structure for source code and tests. Simply add your source files to `app/src/main/java`, your test cases to `app/src/test/groovy` and see below for running your application. == Using the project: 1. Add any dependencies to build.gradle. 2. Add logic to App.java == Building the Extension - Build archives for distribution: ``` ./gradlew assemble ``` - Build an install directory with a runnable project unpacked: ``` ./gradlew installDist ```
Although the README is not required, you really should include one. It doesn’t
have to be Markdown either or have a file extension. We just happen to like the
Markdown syntax and the way that GitHub handles files with an md extension.
We didn’t include it here, but we often include a graphical view of the
directory structure in the initial README.md created using the tree command.
This makes it easy for the user of the template to see where everything is
without hunting through the directories.
tree -a --dirsfirst --charset nwildner
You can see the full README.md in java-gradle/README.md.
We could simply leave the template as it is, but wouldn’t it be great if the user could set the group ID and version for the project at creation time? That would mean parameterizing the group and version in the build file. Not a problem: we can add a post-install script.
Creating a Post-install Script
Post-install scripts are executed immediately after a template is unpacked into
the new project directory and just before the README is displayed. They are
straight Groovy scripts with access to just the core Groovy classes, plus
Groovy’s SimpleTemplateEngine and Apache Commons IO
(for making file manipulation easier).
Every script has access to the following properties:
-
projectDir- aFileinstance representing the root directory of the new project. Treat this as read-only. -
fileEncoding- the encoding used by your template files. You can set this at the start of the script. Defaults to UTF-8. -
lazybonesVersion- a string representing the version of Skeletal the user is running. -
lazybonesMajorVersion- a string representing the first number in the version string, e.g. "1" for "1.2.3". -
lazybonesMinorVersion- a string representing the second number in the version string, e.g. "2" for "1.2.3".
The lazybones* properties have been retained for compatability with existing
Lazybones templates but now refer to the corresponding Skeletal versions.
The script also has access to all the public and protected methods and properties
defined in the LazybonesScript
class. Of particular interest are the ask() and processTemplates() methods.
ask() allows the script to request input from a user, such as 'y' or 'n' for
whether to include a particular feature or not. Even better, the user can
provide the input on the command line, bypassing the input requests all together.
processTemplates() makes it easy to parameterize any of the files in your
template using Groovy syntax. It basically runs the source file through
Groovy’s SimpleTemplateEngine to produce the resulting file. So if we want
to allow the user to specify the project’s group ID and version at install
time, we modify build.gradle slightly:
plugins {
id 'groovy'
id 'application'
}
group = "${project_group}" (1)
version = "${project_version}" (2)
repositories {
mavenCentral()
}
...
| 1 | project_group variable |
| 2 | project_version variable |
and then add a post-install script, lazybones.groovy, in the root of the template:
Map props = [:] (1)
props.project_group = ask("Define value for 'group' [org.example]: ", "org.example", "group") (2)
props.project_version = ask("Define value for 'version' [0.1.0]: ", "0.1.0", "version") (3)
processTemplates('app/build.gradle', props) (4)
| 1 | Property Map to store properties used to process template files |
| 2 | Set project_group using ask() |
| 3 | Set project_version using ask() |
| 4 | Use the template engine to process app/build.gradle using stored properties |
The first parameter to ask() is the user prompt message. The second is a default value to use if the user hits enter without providing a value. The third is a property name in the script binding that if provided on the command line will be used instead of prompting the user for one.
To try the template, see installing the template to cache.
Passing parameters to the script binding looks like -P<param>=<value>
From a directory to create the test project in, create the test project:
skeletal create java-gradle 0.1.0 my-java-app -Pgroup=net.codebuilders -Pversion=1.0-SNAPSHOT
If you provide them all, you get non-interactive creation of projects from templates.
Since we ran the build file through processTemplates, If you look in your
new my-java-app/app/build.gradle you should see group and version updated:
...
group = "net.codebuilders"
version = "1.0-SNAPSHOT"
...
Before continuing cleanup the cache and test project.
Another useful method available to post-install scripts is transformText().
It’s common for scripts to convert strings between camel case (for class names
perhaps), lower-case hyphenated (for directory names), and other forms. The
transformText() method allows you to do just that:
import uk.co.cacoethes.util.NameType
def className = "MyClass"
def directoryForClass = transformText(className, from: NameType.CAMEL_CASE, to: NameType.HYPHENATED)
new File(directoryForClass).mkdirs()
The from and to arguments are both required and must be one of the NameType
enum values: CAMEL_CASE ("MyClass"), PROPERTY ("myClass"),
HYPHENATED ("my-class"), or NATURAL ("My Class")
We will use this later in the final script to guess the project class name from the project directory and the project name from that prior to the ask() prompts.
Before we get back to the lazybones.groovy script let’s take care of a
directory issue. When we created the project files from gradle init we used
a package org.example which added those as org/example subdirectories
under src/main/java and src/test/groovy and where the App.java and
AppTest.groovy are respectively. We’re going to use the script to create new
package directories at creation, so we can remove them from our template by
moving the two classes up to src/main/java and src/test/groovy and deleting
the org/example directories.
Edit App.java and change the package and class name like shown:
package ${project_package}; (1)
public class ${project_class_name} { (2)
public String getGreeting() {
return "Hello World!";
}
public static void main(String[] args) {
System.out.println(new ${project_class_name}().getGreeting()); (2)
}
}
| 1 | project_package variable |
| 2 | project_class_name variable |
We like to use the Spock naming convention of *Spec instead of *Test, so we will
rename AppTest.groovy to AppSpec.groovy and then edit as shown:
package ${project_package}
import spock.lang.Specification
class ${project_class_name}Spec extends Specification {
def "application has a greeting"() {
setup:
def app = new ${project_class_name}()
when:
def result = app.greeting
then:
result != null
}
}
Now that the project package and class name are variables, we need to edit
build.gradle and use variables for the main class. We will also add
settings for the jar archive basename and application name which is the
command that gets ran. These would have both defaulted to app since that
is the subproject we are working in.
...
jar {
archiveBaseName = '${project_name}' (1)
}
application {
mainClass = '${project_package}.${project_class_name}' (2)
applicationName = '${project_name}' (1)
}
...
| 1 | project_name variable |
| 2 | project_package and project_class_name variables |
The same for settings.gradle:
rootProject.name = '${project_name}' (1)
| 1 | project_name variable |
Now edit the lazybones.groovy like this:
import uk.co.cacoethes.util.NameType
import org.apache.commons.io.FileUtils
Map props = [:]
(1)
if (projectDir.name =~ /\-/) {
props.project_class_name = transformText(projectDir.name, from: NameType.HYPHENATED, to: NameType.CAMEL_CASE)
} else {
props.project_class_name = transformText(projectDir.name, from: NameType.PROPERTY, to: NameType.CAMEL_CASE)
}
(2)
props.project_name = transformText(props.project_class_name, from: NameType.CAMEL_CASE, to: NameType.HYPHENATED)
(3)
props.project_group = ask("Define value for 'group' [org.example]: ", "org.example", "group")
props.project_name = ask("Define value for 'artifactId' [" + props.project_name + "]: ", props.project_name , "artifactId")
props.project_version = ask("Define value for 'version' [0.1.0]: ", "0.1.0", "version")
props.project_package = ask("Define value for 'package' [" + props.project_group + "]: ", props.project_group, "package")
props.project_class_name = ask("Define value for 'className' [" + props.project_class_name + "]: ", props.project_class_name, "className").capitalize()
props.project_property_name = transformText(props.project_class_name, from: NameType.CAMEL_CASE, to: NameType.PROPERTY)
props.project_capitalized_name = props.project_property_name.capitalize()
String packagePath = props.project_package.replace('.' as char, '/' as char)
props.package_path = packagePath
(4)
processTemplates('README.md', props)
processTemplates('app/build.gradle', props)
processTemplates('settings.gradle', props)
processTemplates('gradle.properties', props)
processTemplates('app/src/main/java/*.java', props)
processTemplates('app/src/test/groovy/*.groovy', props)
File mainSources = new File(projectDir, 'app/src/main/java')
File testSources = new File(projectDir, 'app/src/test/groovy')
File mainSourcesPath = new File(mainSources, packagePath)
mainSourcesPath.mkdirs()
File testSourcesPath = new File(testSources, packagePath)
testSourcesPath.mkdirs()
def renameFile = { File from, String path ->
if (from.file) {
File to = new File(path)
to.parentFile.mkdirs()
FileUtils.moveFile(from, to)
}
}
(5)
mainSources.eachFile { File file ->
renameFile(file, mainSourcesPath.absolutePath + '/' + file.name)
}
testSources.eachFile { File file ->
renameFile(file, testSourcesPath.absolutePath + '/' + props.project_capitalized_name + file.name)
}
renameFile(new File(mainSourcesPath, 'App.java'), mainSourcesPath.absolutePath + '/' + props.project_class_name + ".java")
renameFile(new File(testSourcesPath, 'AppSpec.java'), testSourcesPath.absolutePath + '/' + props.project_class_name + "Spec" + ".groovy")
| 1 | In the first if/else statement we make an educated guess about the project class
name based on the directory given to create and use transformText() to make
it CAMEL_CASE. |
| 2 | Then we use the class name and transformText() again to make a HYPHENATED
project name. |
| 3 | Then we use these guesses as defaults when asking the user for their values next. This pattern continues until we have all the information we need. |
| 4 | Then we use processTemplates() on all the files that have variables to replace. |
| 5 | Finally, we rename our sources to move them into the package directory structure and then rename the application class and test class. |
Install into Cache and Test
This is now covered in the Template Testing Companion document. To test, install the template again, create the test project per the instructions, build and run the distribution and then cleanup the cache and test project.
Once the template is ready, it’s time to publish it.
Packaging, Installing and Publishing
There are three options to use publishing a template, each of which can be accomplished with a simple task provided by the Skeletal Gradle Plugin:
-
packaging - zipping up the template directory (no manifest created)
-
installing - putting the template package directly into the local Skeletal template cache
-
publishing - making the template package and a manifest file to place in a simple URL repository.
Publishing takes care of the packaging and creates a manifest file in one task. The first time a template is used from a repository it is also added the cache so the install step isn’t required unless you don’t plan on using a remote repository or unless you are doing template development testing.
The relevant Gradle tasks are:
-
packageTemplate<Name> -
packageAllTemplates -
installTemplate<Name> -
installAllTemplates -
publishTemplate<Name> -
publishAllTemplates
The packaging tasks aren’t often used by themselves, so we’ll skip over those
right now. But installing the templates in your local cache is important so
that you can easily test them before publication. You can do this on a
per-template basis, or simply install all the templates from your templates
directory.
If you want to execute a task for a particular template, the <Name> in
the above tasks is derived from the name of the template, which comes from
the directory name. In our case, the template name is java-gradle.
To use this name in the Gradle tasks, we simply camel-case it:
JavaGradle. Of course, this means your directories should use
hyphenated notation rather than camel-case.
If the rules for converting between camel-case and hyphenated forms don’t suit your template name, for example if you separate numbers with hyphens ('javaee-7'), then you can use hyphens in the task name:
./gradlew packageTemplate-javaee-7
Once you’re happy with the template, you can publish it for a simple URL
repository. To do that, you have to configure the build. If you have a look at
my-lzb-templates/build.gradle, you’ll see this section:
lazybones {
...
templateOwner = "Skeletal Project"
}
templateOwner is used in the manifest file as the template owner or creator.
This owner is used for all templates published from this lazybones project.
To publish the template and create the manifest file:
./gradlew publishTemplateJavaGradle
This will create the zip archive and a skeletal-manifest.txt file. This manifest
is a simple CSV formatted text file with an entry for each template published.
name,version,owner,description java-gradle-template,1.0,Skeletal Project,A simple Java Spock Gradle project template
See the Skeletal Gradle Plugin for instructions on applying the plugin to your Gradle build, task usage, and advanced configuration.
Fine-tuning the Packaging
The packaging process is by default rather dumb. It will include all files and directories in the target template directory except for a few hard-coded exceptions (the DESCRIPTION, VERSION, and .retain files for example). That leaves a lot of scope for accidentally including temporary files in the package! To help you avoid that, the plugin allows you to specify a set of extra exclusions using Apache Ant-style patterns.
lazybones {
packageExclude "**/*.swp", ".gradle", "build"
....
}
These exclusions apply to all templates. If you want template-specific exclusions, then use the following syntax:
lazybones {
template("java-gradle") { // Template (directory) name
packageExclude "**/*.swp", ".settings"
}
}
Note that the template-specific settings completely override the global ones, so if you want the global ones to apply you will need to repeat them in the template-specific list.
Another potential issue when packaging templates is with file and directory permissions. Lazybones attempts to retain the permissions it finds in the template directory, but these may not be correct on Windows. To compensate for that, the plugin allows you to specify file permissions in the template configuration:
lazybones {
fileMode "755", "gradlew", "**/*.sh"
}
The first argument is the Unix-style permission as a string (such as "600", "755" and so on), and the rest are a list of Ant-style patterns representing the files and directories that the permission string should apply to. You can have multiple fileMode() entries, although ideally you should only have one per file mode.
As with package exclusions, you can also specify file modes on a per-template basis:
lazybones {
template("simple-java") {
fileMode "600", "secret.properties"
fileMode "755, "gradlew", "**/*.sh"
}
}
Again, the template-specific settings replace the global ones for that particular template.
That’s it for the getting started guide. You’ve created a template, tested it, and finally published it for a URL repository. For the rest of the guide we’ll look at the template creation in more detail.
Simple URL Repositories
In a break from the original Lazybones project and their use of Bintray for
repositories, Skeletal uses what we call a Simple URL Repository which can be
any file: or http(s): URL location that contains templates and a
skelatal-manifest.txt file that your computer has read-access to.
To create a repository just copy the manifest file and project template
archives to a local directory, file share, or webserver and use file: or http(s) type repository URLs to access them. Configuring Skeletal to
use additional repositories is covered in the Application Users Guide.
Template Engines
The processTemplates() method available to post-install scripts allows you to
generate files based on templates. By default, any files that match the pattern
passed to processTemplates() are treated as Groovy templates that can be
processed by SimpleTemplateEngine
and those source files are replaced by the processed versions. That’s not the
end of the story though.
Skeletal allows you to use any template engine that implements Groovy’s
TemplateEngine,
meaning that your source templates could be Mustache, Apache Velocity, or anything else. Of course, not every template engine has a Groovy implementation, but it’s
often trivial to create an adapter TemplateEngine implementation.
For the following examples, we’ll use our Groovy Handlebars Engine implementation of Handlebars.java.
The first step to using an alternative template engine is to include the implementation JAR in the post-install script for your project template. Lazybones uses Groovy’s @Grab annotation for that:
@Grab(group="net.codebuilders", module="groovy-handlebars-engine", version="0.3.0-M1")
import uk.co.cacoethes.handlebars.HandlebarsTemplateEngine
registerDefaultEngine new HandlebarsTemplateEngine()
The Handlebars engine JAR is in Maven Central that @Grab automatically searches. If you have the JAR hosted elsewhere, you’ll need to use @GrabResolver.
As of Skeletal 0.19.0, Groovy Handlebars Engine is an included library, so @Grab is not required as it is already on the classpath. Other alternative template engines will still require it.
Once you have the JAR on the script’s classpath, you can register the engine. There are several ways to do this depending on what you want to do. The above example uses registerDefaultEngine() to make the Handlebars template engine the default, which means that any files handled by processTemplates will be treated as Handlbars templates rather than Groovy ones.
What if you want to use different engines for different templates? Or perhaps you prefer to give the source templates a suffix that identifies them as such? In these cases, you can use registerEngine():
import uk.co.cacoethes.handlebars.HandlebarsTemplateEngine
registerEngine "hbs", new HandlebarsTemplateEngine()
processTemplates "**/*.groovy", [foo: "bar"]
This method registers a template engine against a specific suffix. If any files match the processTemplates() pattern with the addition of the registered suffix, Lazybones will use the corresponding template engine for that file.
So let’s say your template project has a src/main/groovy/org/example/App.groovy.hbs file. The App.groovy part matches the pattern and hbs is a registered extension. So that file will be processed by the Handlebars template engine, creating a src/main/groovy/org/example/App.groovy file in the target project. Here’s a summary of how source template files are processed:
| Filename | Resulting file | Processing |
|---|---|---|
App.groovy |
App.groovy |
Registered default template engine |
App.groovy.gtpl |
App.groovy |
Groovy template engine |
App.groovy.hbs |
App.groovy |
Handlebars template engine |
Skeletal automatically registers the Groovy template engine against the suffix gtpl. Also note that you should not include the template suffix in your file pattern. If you try:
processTemplates "**/*.hbs", [foo: "bar"]
then Skeletal will in fact use the default template engine for any source file that has a hbs suffix. It’s better to use the */ pattern instead:
processTemplates "**/*", [foo: "bar"]
Then any files ending with hbs will be processed with the Handlebars template engine. This does raise a problem: the pattern above will match non-template files too, and Skeletal will process those files with the default template engine.
If you do want to take this approach, then you can disable the default template engine:
import uk.co.cacoethes.handlebars.HandlebarsTemplateEngine
registerEngine "hbs", new HandlebarsTemplateEngine()
clearDefaultEngine()
processTemplates "**/*", [foo: "bar"]
This will ensure that only source files with a registered template suffix get processed. All other files are left untouched.
Subtemplates
It’s very easy to add subtemplate support to your project templates. The key points to understand are:
-
Subtemplates are similar to project templates but packaged inside a project template zip.
-
A subtemplate can be included in multiple project templates.
-
Subtemplates only take effect when the user runs the
skeletal generatecommand.
Let’s say you want to add a subtemplate for generating Spock test classes in a
project created from the java-gradle template we introduced
earlier. Your starting point is to create a new directory for the subtemplate:
templates/subtmpl-spock-spec
Note that although the subtemplate will be going inside the java-gradle
template, its directory is at the same level as templates/java-gradle.
The key is to give the directory name as 'subtmpl-' prefix, as this is what
tells the build that it’s a subtemplate, resulting in
subtmpl-spock-spec being excluded from the *AllTemplates tasks.
The contents of a subtemplate source directory look a little like a normal project template, except you are unlikely to include as many files and the README is unnecessary. In this case, we want:
-
VERSION - the file containing the current version of the subtemplate
-
lazybones.groovy - the post-install script
-
Spec.groovy.gtpl - the template source file for test classes
Each of these files behaves in the same way as in a project template, but there are a few slight differences. Consider the template source file for tests:
package ${pkg}
import spock.lang.Specification
class ${cls} extends Specification {
def "application has a greeting"() {
given: // setup
def app = new ${parentClassName}()
when: // stimulus
def result = app.greeting
then: // response
result != null
}
}
This references several properties: pkg, cls, and parentClassName. Where
do these parameters come from? We need to look into the post-install script
lazybones.groovy, to find out:
import org.apache.commons.io.FileUtils
import org.apache.commons.io.FilenameUtils
import static org.apache.commons.io.FilenameUtils.concat
Map props = [:]
// Pass in parameters from the project template
String parentPackage = parentParams.package (2) (3)
props.parentClassName = parentParams.className (2) (3)
props.pkg = ask("Define value 'package' [" + parentPackage + "]: ", parentPackage, "package") (1)
props.cls = ask("Define value for 'class' name: [SimpleSpec]", "SimpleSpec", "class").capitalize() (1)
processTemplates("Spec.groovy", props)
String pkgPath = props.pkg.replace('.' as char, '/' as char)
String filename = props.cls.capitalize() + ".groovy"
File destFile = new File(projectDir, concat(concat("app/src/test/groovy", pkgPath), filename))
destFile.parentFile.mkdirs()
FileUtils.moveFile(new File(templateDir, "Spec.groovy"), destFile) (4)
println "Created new Spock Test ${FilenameUtils.normalize(destFile.path)}"
| 1 | As you can see, the pkg and cls properties are mapped from the return
values of two ask() calls. This is standard post-install script behavior. |
| 2 | The interesting properties, parentPackage and parentClassName, are mapped from
something new: the parentParams map. This contains any named parameters used
by the parent project template, i.e. java-gradle in this case.
Because of this, parentParams only exists for subtemplates. |
| 3 | Only parentClassName is used in the template so it is in the prop map where
parentPackage is a property only used within the script. |
| 4 | Another novel aspect of the post-install script is the reference to a
templateDir property in addition to projectDir. This is because
subtemplates are not unpacked directly in the project directory. Instead,
Lazybones unpacks them into the project’s .lazybones directory. templateDir
points to the location of the unpacked subtemplate, whereas projectDir still
points to the root directory of the project created from
java-gradle. So your subtemplate post-install script will
typically want to copy or move files from templateDir to projectDir. The
Commons IO classes that all post-install scripts have access to are ideal for
this. |
With all the subtemplates files in place, all you need to do is tell the build
that the simple-java project template should include the entity subtemplate.
So open up the build file and add this line to the lazybones block:
lazybones {
...
template "java-gradle" includes "spock-spec"
}
Note how the name of the subtemplate excludes the 'subtmpl-' prefix. Now when
you package the simple-java project template, the entity subtemplate will be
included in it, ready for use with Lazybones' generate command.
If you want to include multiple subtemplates, just pass extra arguments to includes():
lazybones {
...
template "java-gradle" includes "spock-spec", "controller", "repository"
}
There is one final option available to template authors. What if you want to package the spock-test, controller, and repository template files into a single subtemplate package? How would the user be able to specify which type of class they want to generate? The answer is through template qualifiers.
Let’s say you have an 'artifact' subtemplate that includes Spec.groovy.gtpl, Controller.groovy.gtpl, etc. The user can run the generate command like this to determine which artifact type to use:
skeletal generate artifact::controller
The :: separates the subtemplate name, 'artifact', from the qualifier,
'controller'. In your post-install script, you can access the qualifiers through
a tmplQualifiers property:
def artifactTemplate
if (tmplQualifiers) {
artifactTemplate = tmplQualifiers[0].capitalize() + ".groovy.gtpl"
}
else {
artifactTemplate = ask("Which type of artifact do you want to generate? ", null, "type")
}
// ... process the corresponding template file.
The user can even pass extra qualifiers simply by separating them with ::
skeletal generate artifact::controller::org.example::Book
This is why tmplQualifiers is a list. It retains the order that the qualifiers are specified on the command line.
Note qualifiers should not be used for general parameterization such as packages and class names. Think carefully before supporting more than a single qualifier.
Post Install Script In-depth
The lazybones.groovy post install script is a generic groovy script with a few extra
helper methods:
-
ask(String message, defaultValue = null)- asks the user a question and returns their answer, ordefaultValueif no answer is provided -
ask(String message, defaultValue, String propertyName)- works similarly to theask()above, but allows grabbing variables from the command line as well based on thepropertyName. -
processTemplates(String filePattern, Map substitutionVariables)- use ant pattern matching to find files and filter their contents in place using Groovy’sSimpleTemplateEngine. -
hasFeature(String featureName)- checks if the script has access to a feature,hasFeature("ask")orhasFeature("processTemplates")would both return true
You can get a complete list of the available methods from the LazybonesScript class.
Here is a very simple example lazybones.groovy script that asks the user for
a couple of values and uses those to populate properties in the template’s build
file:
def props = [:]
props["groupId"] = ask("What is the group ID for this project?")
props["version"] = ask("What is the project's initial version?", "0.1", "version")
processTemplates("*.gradle", props)
processTemplates("pom.xml", props)
The main Gradle build file might then look like this:
plugins {
id 'groovy'
id 'application'
}
<% if (group) { %> (2)
group = "${project_group}"
<% } %>
version = "${project_version}" (1)
| 1 | The ${} expressions are executed as Groovy expressions, and they have access
to any variables in the property map passed to processTemplates(). |
| 2 | Scriptlets, i.e. code inside <% %> delimiters, allow for more complex logic. |