Skip to content

Creating Project Templates with Cookiecutter

Published on
  • Python

Can you create a new Java project from scratch, manually, without copy & pasting? I don't. And probably you shouldn't as this is a pure waste of time considering how repetitive the base code and the directory structure is.


Spring team created for this reason Spring Initializr and it's web representation start.spring.io. All the other major frameworks copied this idea thanks to which it is a no brainer anymore to create a new Spring, Quarkus, Micronaut, Axon or Vaadin project (I am sure there is more).

But not every framework, not every platform offers such tool. Creating AWS Lambda, or Google Function with Java starts with copy & pasting code from documentation or sample projects. Even Maven - the most popular build tool for Java - does not offer simple mvn init command to start a fresh, non-opinionated project from scratch. Yes, I know there are archetypes but I don't think they are very user-friendly.

There are quite a few project generator frameworks - tools to create your own project templates. The most popular one is probably Yeoman, used by JHipster. Today I came across Cookiecutter, used internally by AWS SAM CLI and decided to give it a try to create a simplified "missing mvn init" with Cookiecutter.

Cookiecutter

CookieCutter

Cookiecutter is a cross-platform tool to create projects from project templates, for any programming language. You can also mix different languages as all it actually cares is processing files and filtering placeholders. It is built with Python, so you must have Python installed on your system to create projects from Cookiecutter templates. You probably should also have some Python knowledge but don't stress too much about it - I have very little and managed to develop what I needed.

Package management in Python is a mess, so it's the best if you consult installation page in Cookiecutter documentation.

I've installed it on MacOS Monterey with this simple command:

code
$ pip install cookiecutter

Once Cookiecutter is installed, you can create project from local or remote templates:

code
$ cookiecutter local-template # from local directory
$ cookiecutter https://github.com/user/repo # from git repository

Cookiecutter project template

Project template must contain following elements:

  • empty __init__.py file
  • cookiecutter.json with a list of parameters that user provides to generate a project together with default values:
json
{
  "groupId": "com.example",
  "artifactId": "demo",
  "javaVersion": "11"
}
  • generated project directory, which name can be also filtered. In our case it will be a directory named \{\{ cookiecutter.artifactId \}\} - exactly like that, with curly brackets in the name. Cookiecutter during project generation will replace this placeholder with an actual value.

During project generation, Cookiecutter replaces all placeholders like \{\{ cookiecutter.property-name \}\} in all files in the \{\{ cookiecutter.artifactId \}\} directory with a value provided by a user.

In our case complete project structure looks like this:

.
├── __init__.py
├── cookiecutter.json
└── \{\{cookiecutter.artifactId\}\}
    ├── pom.xml
    └── src
        └── main
            └── java
                └── demo
                    └── Main.java

Where pom.xml contains:

xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>\{\{ cookiecutter.groupId \}\}</groupId>
	<artifactId>\{\{ cookiecutter.artifactId \}\}</artifactId>
	<version>1.0</version>
	<packaging>jar</packaging>
	<properties>
		<maven.compiler.source>\{\{ cookiecutter.javaVersion \}\}</maven.compiler.source>
		<maven.compiler.target>\{\{ cookiecutter.javaVersion \}\}</maven.compiler.target>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>
</project>

And Main.java:

java
package demo;

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello world");
    }
}

Generating project from the template

Now if we move one directory up from my-template, we can create a new project:

$ cookiecutter my-template

We will be prompted to provide values for:

groupId [com.example]: 
artifactId [demo]: 
javaVersion [11]:

and the project will be generated 🎉

But we can do better.

  • instead of generating demo Java package, the template should create a package that corresponds to the groupId and the artifactId.
  • it would be nice if generated project had Maven Wrapper installed
  • it would be nice if right after project generation, all the dependencies be installed

For that, we will use private variables and Hooks - simple and handy way to generate project a bit more dynamically.

Private Variables

cookiecutter.json can contain private variables that user won't be required to fill. In our case we can use it to compute the project root package based on the groupId and the artifactId:

json
{
  "groupId": "com.example",
  "artifactId": "demo",
  "javaVersion": "11",
  "__package": "\{\{ cookiecutter.groupId + '.' + cookiecutter.artifactId \}\}"
}

Cookiecutter Hooks

Hooks can run either before or after project is generated and can be written either with Bash or Python. These scripts get also filtered with Cookiecutter parameters from cookiecutter.json.

Our hook will address all the issues listed above. Since we are going to create the directory structure dynamically, we can remove src/main/java/demo directory and move Main.java file to the root of the \{\{ cookiecutter.artifactId \}\} directory. Instead of hardcoding demo pacakge in Main.java, we can use now \{\{ cookiecutter.__package \}\} variable:

java
package \{\{cookiecutter.__package\}\};

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello world");
    }
}

Then:

  1. Create a directory hooks in your project template directory.

  2. Create a file hooks/post_gen_project.py

python
import os

# converts groupId like com.example and artifact id like demo into a string com/example/hello 
directory = '\{\{ cookiecutter.__package.replace('.','/') \}\}'

srcDir = 'src/main/java/' + directory

# create typical maven directory structure
os.makedirs(srcDir, exist_ok=True)

# move Main.java to Maven sources
os.rename("Main.java", srcDir + '/Main.java')

# generate Maven wrapper
os.system('mvn wrapper:wrapper')

# download dependencies
os.system('./mvnw verify')

Conditionals

It may and likely will happen that some pieces of projects are created or not depending on the values provided by the user. We can extend cookiecutter.json to ask if project should include JUnit, and if it does then include the dependency in pom.xml and create src/test/java directory.

json
{
  "groupId": "com.example",
  "artifactId": "demo",
  "javaVersion": "11",
  "junit": [
    "yes",
    "no"
  ],
  "__package": "\{\{ cookiecutter.groupId + '.' + cookiecutter.artifactId \}\}"
}

Cookiecutter uses Jinja2 templating system. Let's update pom.xml with:

xml
{% if cookiecutter.junit == 'yes'%}
<dependencies>
	<dependency>
		<groupId>org.junit.jupiter</groupId>
		<artifactId>junit-jupiter</artifactId>
		<version>5.9.0</version>
		<scope>test</scope>
	</dependency>
</dependencies>
		{% endif %}

Our hook, now must create src/test/java directory depending on the value of cookiecutter.junit variable:

python
if '\{\{ cookiecutter.junit \}\}' == 'yes':
	os.makedirs(testDir, exist_ok=True)

Try it out!

The following code is pushed to https://github.com/maciejwalkowiak/cookiecutter-maven-template repository. You can try it yourself by executing:

$ cookiecutter https://github.com/maciejwalkowiak/cookiecutter-maven-template

And just to make it clear - this project is not meant to be "the missing mvn init" - at this stage it does not offer much more than regular mvn archetype:generate, but if you think it is an idea worth pursuing - PRs are welcome!

Conclusion

Cookiecutter is an interesting, simple but powerful option for generating project templates. From the user point of view - it is quite rough. It misses bells and whistles, animations, interactive CLI comboboxes that you may be familiar with from tools like Yeoman. But it does the job. I like concept of hooks, thanks to which it is very easy to code more advanced use cases. The drawback - purely from personal point of view - it is written in Python, which I am not a fan of. But that's just a matter of taste.

Let's stay in touch and follow me on Twitter: @maciejwalkowiak

Subscribe to RSS feed