This guide captures the conventions and non-obvious patterns used across this codebase. It focuses on project-specific and uncommon conventions rather than generic Java best practices.
- Multi-module Java project:
cliis built with Maven (traditionalpom.xml).- IntelliJ plugin uses Gradle Kotlin DSL (
build.gradle.kts) and theorg.jetbrains.intellijGradle plugin.
- Gradle IntelliJ plugin configuration is declared in Kotlin DSL. Set JVM compatibility in
tasks.withType<JavaCompile>and Kotlin target viaKotlinCompile.kotlinOptions.jvmTarget. - Use environment variables for secret values in Gradle tasks (e.g.
System.getenv("PUBLISH_TOKEN"),CERTIFICATE_CHAIN,PRIVATE_KEY).
- Use
javax.inject.Inject+javax.inject.Singletonfor constructor injection and singleton scopes. - Central DI entrypoint:
DIContext.getInstance()used to fetch singletons in places where constructor injection is not available (tests, builders). - Constructors are used to declare dependencies; fields for injected dependencies are
private final.
- Builders implement an interface (e.g.
ProjectGeneratorRequest.Params) and return that interface from setters for fluent chaining. - Public getters on builders lazily derive values from multiple inputs (e.g.
magicArg,projectName,githubRepository) rather than requiring callers to populate every field. - Builders may embed CLI metadata as annotations on fields (see Command Line Parser section).
Example pattern:
- setX(...) returns
ProjectGeneratorRequest.Paramsso callers can chain. - getX() returns either explicitly-set value or an inferred default computed from other fields.
- Use a custom
CommandLineParserannotation set on builder fields:@CommandLineParser.Alias("x")for short flags@CommandLineParser.Help("...")to document flags@CommandLineParser.PositionalArg(n)for positional args
- These annotations are placed on private fields rather than on methods.
- Template placeholders use Mustache-like double-brace tokens:
{{ foo }}(e.g.{{ packagePath }},{{ mainClass }}). - Replacement occurs for many text file types (explicit list in code): .java, .properties, .xml, .gradle, .json, .yml, .yaml, .md, .adoc.
- File names are also processed for placeholder replacements (not only file contents).
- Placeholder substitution is performed by sequential String#replace calls (simple token replacement, not a template engine).
packagePathplaceholder is expected to be a/-separated path (derived frompackageName.replace(".", "/")).
- Use Apache Commons IO
FileUtilsfor copying, moving, reading/writing files and directories. - When creating project directories:
- Create with
mkdirs()and then verify existence; throwIOExceptionif creation failed.
- Create with
- When iterating template files:
- Copy directories with
FileUtils.copyDirectory(...)and files withFileUtils.copyFileToDirectory(...).
- Copy directories with
- When processing files recursively:
- Recurse into directories, process files, then apply rename/move operations (so moved/renamed entries are returned by helper methods).
- Project extensions are applied by a
ProjectDirectoryExtensionMerger(merge extension directories into project directory). Treat extensions as additive overlays on a template.
- Many values are computed from inputs in the following precedence order:
- explicit value set on builder
- derived from
githubRepositoryif provided (e.g.groupId→com.github.[user]) - derived from
magicArgwhen it containspackage.Classform - fallbacks such as
my-app,My App,com.example.myapp
- Use helper
StringUtilsfor:- camelCase ↔ lower-case-with-separator conversions
ucFirst,ucWords,lowerCaseWithSeparatorToCamelCaseisValidJavaClassName,countCharInstances
Naming conventions used by helpers:
- artifactId/project-name → lower case with
-separator - class names → CamelCase; package names → dot-separated lowercase
- GitHub repo operations:
- Creating repository via REST
POST https://api.github.com/user/reposusingHttpURLConnectionand a JSON body ({"name": "...", "private": true/false}). - Use a token from
GithubTokenServiceinAuthorization: Bearer <token>.
- Creating repository via REST
- Git operations use JGit:
- Initialize repo with
Git.init().setDirectory(localPath).call(). - Manage
originremote via JGitRemoteConfigandURIish, checking for existing URIs and adding if absent. - Commit with
git.add().addFilepattern(".").call()andgit.commit().setMessage(...).call(). - Push with
git.push().setRemote("origin").setCredentialsProvider(new UsernamePasswordCredentialsProvider(token, "")). (token passed as username and empty password)
- Initialize repo with
- When dealing with releases for private repos:
- Create a "-releases" repository, populate a README template and initialize/publish it.
- In workflows, replace
${{ secrets.GITHUB_TOKEN }}with${{ secrets.JDEPLOY_RELEASES_TOKEN }}and insert atarget_repositoryfield under the same indentation level—indentation must be preserved. Use scanner-based method to compute indentation of a line.
- Use
private static finalconstants for repeated external strings (e.g.GITHUB_URL,JDEPLOY_TOKEN_SECRET_NAME,GITHUB_API_URL). - Prefer
String.valueOf(...)when adding potentially null values to templates to avoid NPEs in replacement code.
- Validate early: throw checked
ExceptionorIOExceptionfor invalid inputs like missing template directory or existing project directory. - Wrap IO exceptions into unchecked
RuntimeExceptionin helper methods where appropriate (e.g. workflow file modification). - Methods interacting with external services (HTTP, Git) propagate checked exceptions to caller for higher-level handling/tests.
- Use JUnit 5 (
@BeforeEach,@AfterEach,Assumptions,@Disabled). - Create temporary directories with
Files.createTempDirectory(...)and delete them in@AfterEachviaFileUtils.deleteDirectory. - Tests use
DIContextto obtain instances; tests also useMavenBuilderto build generated projects in integration-style tests. - Tests may introspect private fields using reflection to assert builder-derived defaults (helper method sets
setAccessible(true)). - Use
Assumptions.assumeTrue(getJavaVersion() >= 17, "...")when a test requires a certain Java runtime. - Use
@DisabledOnOs/ OS conditions for platform-specific tests where needed.
- javax.inject for DI (lighter-weight than Spring-style annotations).
- JGit (org.eclipse.jgit) for programmatic git operations — remote management via
RemoteConfigandURIish. - Direct use of
HttpURLConnectionfor simple GitHub REST calls (no heavy HTTP client). - Use of
org.apache.commons.io.FileUtilsandca.weblite.tools.io.IOUtilfor file I/O helpers. - Project templates handled as filesystem directories with placeholders; no templating engine is used—simple string replacement suffices for current needs.
- Configure the IntelliJ plugin block with:
version.set("..."),type.set("IC"), andplugins.set(listOf("git4idea")).
- Use
patchPluginXmlto setsinceBuild/untilBuild. - Use
signPluginandpublishPlugintasks, sourcing credentials from environment variables (avoid hard-coding secrets). - Keep Kotlin and Java target jvm versions consistent (17 in this project).
- Console output used for progress info in CLI and initializers (e.g.,
System.out.printlnfor successful repository creation). - Tests include both unit and integration-style tests that run external build tools (Maven) where appropriate.
- When searching/processing text files to insert YAML fields, preserve existing indentation by computing indent of the target line and inserting new lines with the same indent.
If you want, I can:
- Convert these conventions into a checklist for PR reviews.
- Produce linting rules / code templates (e.g., a builder skeleton) matching these patterns.