diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d021614..68ce4a9 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -5,9 +5,13 @@ on:
branches: [ main ]
pull_request:
+permissions:
+ contents: read
+
jobs:
build:
runs-on: ubuntu-latest
+
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -17,6 +21,18 @@ jobs:
with:
java-version: '21'
distribution: 'temurin'
+ cache: maven
- - name: Build with Maven
- run: mvn -B clean verify
\ No newline at end of file
+ - name: Build with Maven (tests + coverage)
+ run: mvn -B -q -ntp clean verify
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: |
+ target/site/jacoco/jacoco.xml
+ target/site/jacoco-it/jacoco.xml
+ flags: codegen-blueprint
+ name: codegen-blueprint
+ fail_ci_if_error: false
\ No newline at end of file
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..6882da4
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,44 @@
+name: CodeQL
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+ schedule:
+ - cron: '18 3 * * 1'
+
+permissions:
+ contents: read
+ security-events: write
+ actions: read
+
+jobs:
+ analyze:
+ name: Analyze (CodeQL)
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v4
+ with:
+ languages: java
+ queries: +security-and-quality
+
+ - name: Build (codegen-blueprint)
+ run: mvn -q -ntp -DskipTests=true clean package
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v4
+ with:
+ category: "/language:java"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 2107c55..9eb1bba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,3 +64,4 @@ buildNumber.properties
generated-sources/
generated-classes/
/HELP.md
+*.iml
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 09364ee..2ad710b 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,7 @@
MIT License
-Copyright (c) 2025 bsayli
+Copyright (c) 2025 blueprint-platform
+Maintained by Barış Saylı (@bsayli)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 8d3a97c..0a53552 100644
--- a/README.md
+++ b/README.md
@@ -1,216 +1,329 @@
-# Codegen Spring Boot Initializr
+# Codegen Blueprint — Profile‑Driven Project Generator with Architecture Options
-
-
-
-
-
+[](https://github.com/blueprint-platform/codegen-blueprint/actions/workflows/build.yml)
+[](https://github.com/blueprint-platform/codegen-blueprint/releases/latest)
+[](https://github.com/blueprint-platform/codegen-blueprint/actions/workflows/codeql.yml)
+[](https://codecov.io/gh/blueprint-platform/codegen-blueprint/tree/refactor/hexagonal-architecture)
+[](https://openjdk.org/projects/jdk/21/)
+[](https://spring.io/projects/spring-boot)
+[](https://maven.apache.org/)
+[](LICENSE)
-
-
- Social preview banner for GitHub and sharing
+
-**A customizable project generator for Spring Boot.**
-Quickly scaffold a new Java application with predefined structure, configuration, and tests — no repetitive setup
-required.
-
---
-## 🚧 Active Development Notice
+## 🧠 Why Codegen Blueprint Exists
-This repository is currently undergoing a full **Hexagonal Architecture refactor**.
+Modern engineering teams don’t struggle to **start** new services —
+they struggle to keep them **architecturally consistent** as they scale.
-➡️ All active development continues on the branch:
+Most generators produce a folder structure and walk away.
+Codegen Blueprint safeguards **architectural integrity**:
-🔗 **https://github.com/blueprint-platform/codegen-blueprint/tree/refactor/hexagonal-architecture**
+* Starts clean — no framework dependencies in the domain
+* Stays clean — structure guides every evolution
+* Prevents silent architecture drift
-The `main` branch currently reflects **the older pre-refactor version** and will be updated once the refactor reaches **1.0.0-RC**.
+Not just scaffolding.
+Not just templates.
----
+> **Executable Architecture — baked into the delivery pipeline.**
+
+
-## 🚀 Problem Statement
+
+
+
+ Who benefits ➜ What the engine delivers ➜ Generated services
+
-Bootstrapping a new Spring Boot project often involves:
+---
-* Manually creating Maven folders
-* Writing boilerplate `pom.xml`
-* Copying `.gitignore`, `application.yml`, and test classes
-* Setting up wrapper scripts
+### 🎯 Who is this for?
-❌ Time wasted on repetitive setup
-❌ Risk of inconsistencies between projects
-❌ Slower onboarding for new developers
+| Role | Benefit |
+| -------------------- | -------------------------------- |
+| Platform Engineering | Org‑wide standardization |
+| Lead Architect | Governance as Code |
+| Developers | Clean architecture from day zero |
+| New Team Members | Instant productivity |
---
-## 💡 Solution
+### 🥇 What makes it different?
-This project automates all of that:
+> **Initializr‑like simplicity** ➜ **Architecture‑first consistency**
-* Generates a **ready-to-run Spring Boot project** with Maven
-* Adds `.gitignore`, `application.yml`, starter class, and test class
-* Supports **custom package and project naming**
-* Ships with **CLI runner** for one-liner project generation
-* Produces a **zip archive** you can immediately extract and use
+| Capability Focus | Spring Initializr & JHipster | Codegen Blueprint |
+| --------------------------------- | ---------------------------- | ----------------- |
+| Generates folder layout | ✔ | ✔ |
+| Opinionated architecture defaults | ⚠️ | **✔** |
+| Domain isolation by design | ❌ | **✔** |
+| Profile‑driven evolution paths | ⚠️ | **✔** |
+| Anti‑drift support (future‑ready) | ❌ | **✔** |
+
+> 🧭 Same starting point — **better long‑term alignment**
---
-## ⚡ Quick Start
+## 📑 Table of Contents
+
+* ⚡ [What is Codegen Blueprint (Today)?](#-what-is-codegen-blueprint-today)
+* 🧭 [1.0.0 Scope & Status](#-100-scope--status)
+* 💡 [Why This Project Matters](#-why-this-project-matters)
+* 🔔 [Sample Code & Greeting Example](#-sample-code--greeting-example)
+* 🔌 [Inbound Adapter](#-inbound-adapter-delivery)
+* ⚙️ [Outbound Adapters & Artifacts](#-outbound-adapters--artifacts)
+* 🧪 [Testing & CI](#-testing--ci)
+* 🔄 [CLI Usage Example](#-cli-usage-example)
+* 🚀 [Vision & Roadmap](#-vision--roadmap-beyond-100)
+* 🤝 [Contributing](#-contributing)
+* ⭐ [Support & Community](#-support--community)
+* 🛡 [License](#-license)
-### 1. Clone the Repository
+---
-```bash
-git clone https://github.com/bsayli/codegen-springboot-initializr.git
-cd codegen-springboot-initializr
-```
+## ⚡ What is Codegen Blueprint (Today)?
-### 2. Build the Project
+A **CLI‑driven**, **architecture‑aware** project generator.
-```bash
-mvn clean install
-```
+📌 Current primary profile: **springboot‑maven‑java**
+(✔ Spring Boot 3.5.x · ✔ Java 21 · ✔ Maven)
-### 3. Run in CLI Mode
+Produces a clean and predictable structure with:
-```bash
-mvn spring-boot:run -Dspring-boot.run.profiles=cli \
- -Dspring-boot.run.arguments="--groupId=com.example --artifactId=demo-app --packageName=com.example.demo --outputDir=./target/generated-projects --overwrite=true"
-```
+* Standardized identifiers (groupId, artifactId, package)
+* Clear boundaries for maintainability
+* Tests ready from day zero
+* No dependency overload
-✅ This generates a new project as a zip archive under the specified output directory (default:
-`./target/generated-projects`):
+### Optional Architecture Layout
+
+📌 Hexagonal is an **opt‑in structured evolution path**.
```
-[OK] Project archive generated at: /.../target/generated-projects/demo-app/demo-app.zip
+domain // business logic only (no Spring)
+application // orchestrates ports
+adapters // inbound & outbound
+bootstrap // configuration & wiring
```
-ℹ️ Tips:
+> “Framework‑free domain — intentional architecture from day zero.”
-* If you don’t provide `--outputDir`, the project will be created under the default path `target/generated-projects`.
-* If the target directory already exists:
+
- * By default, the generator will **fail-fast** with a clear error.
- * Add `--overwrite=true` to **delete and regenerate** the project in the same directory.
+
+
+
+ Flow: Inputs ➜ Use Cases ➜ Domain Rules ➜ Artifacts ➜ Executable Service
+
---
-## 🧑💻 Programmatic Usage
+## 🧭 1.0.0 Scope & Status
-```java
-import java.nio.file.Path;
-import java.util.List;
+### Included in 1.0.0
-import io.github.bsayli.codegen.initializr.projectgeneration.model.Dependency;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.spring.SpringBootJavaProjectMetadata.SpringBootJavaProjectMetadataBuilder;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.BuildTool;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Framework;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Language;
-import io.github.bsayli.codegen.initializr.projectgeneration.service.ProjectGenerationService;
+| Feature | Status |
+| -------------------------------------- | ------------------ |
+| CLI‑based generation | ✔ Production‑ready |
+| Hexagonal architecture layout (opt‑in) | ✔ Available |
+| Spring Boot 3 / Java 21 / Maven | ✔ Supported |
+| Main + test entrypoints | ✔ Provided |
+| Required build + config artifacts | ✔ Generated |
+| Greeting sample (optional sample‑code) | ✔ Included |
+| MIT License | ✔ Open‑source |
-// Assume ProjectGenerationService is injected or obtained from Spring context
-ProjectGenerationService service = /* @Autowired or ApplicationContext.getBean(...) */;
+### Up Next
- var depWeb = new Dependency.DependencyBuilder()
- .groupId("org.springframework.boot")
- .artifactId("spring-boot-starter-web")
- .build();
+| Feature | Status |
+|------------------------------------------------| ---------- |
+| REST inbound adapter | Planned |
+| Hexagonal evolution kit (ports + CQRS) | Planned |
+| Additional profiles (Gradle, Kotlin, Quarkus) | Planned |
+| Foundation libraries (`blueprint-*`) | Planned |
+| Multi‑module services | Planned |
+| Developer UI | Evaluating |
- var depTest = new Dependency.DependencyBuilder()
- .groupId("org.springframework.boot")
- .artifactId("spring-boot-starter-test")
- .scope("test")
- .build();
+> ✔ Deep quality first → expand ecosystem next
- var metadata = new SpringBootJavaProjectMetadataBuilder()
- .springBootVersion("3.5.5")
- .javaVersion("21")
- .groupId("com.example")
- .artifactId("demo-app")
- .name("demo-app")
- .description("Generated by codegen-initializr-core")
- .packageName("com.example.demo")
- .dependencies(List.of(depWeb, depTest))
- .build();
+📌 For more details:
+- [Executable Architecture Scope (1.0.0)](docs/architecture/executable-architecture-scope.md)
- var type = new ProjectType(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA);
+---
- Path zip = service.generateProject(type, metadata);
-System.out.
+## 💡 Why This Project Matters
- println("Archive generated at: "+zip.toAbsolutePath());
-```
+Clean architecture shouldn’t be optional.
+
+You gain:
+
+* ✔ Predictable structure & boundaries
+* ✔ Testability from day zero
+* ✔ Faster onboarding & team scaling
+* ✔ Architectural governance without friction
+
+You avoid:
+
+* ❌ Copy‑paste architecture
+* ❌ Each repo reinventing patterns
+* ❌ Best‑practice rot over time
+* ❌ Architecture drift
+
+📘 Explore design:
+👉 [How to Explore This Project (Hexagonal Architecture Guide)](./docs/guides/how-to-explore-hexagonal-architecture.md)
+
+### Strategic Impact
+
+Architecture becomes **intentional — enforceable — repeatable**.
---
-## 🖼 Demo Output
+## 🔔 Sample Code & Greeting Example
-Example of the generated project structure:
+📌 Minimal but meaningful reference sample:
-```text
-demo-app/
- ├── pom.xml
- ├── .gitignore
- ├── src/
- │ ├── main/java/com/example/demo/DemoAppApplication.java
- │ ├── main/resources/application.yml
- │ ├── test/java/com/example/demo/DemoAppApplicationTests.java
- │ └── gen/java/... (for codegen output)
+* Domain model: **Greeting**
+* Use case: generate greeting text
+* Inbound REST adapter: `/api/v1/sample/greetings/default`
+* Hexagonal structure illustrates **port‑driven design**
+
+Enabled when flags include:
+
+```
+--layout hexagonal \
+--sample-code basic
```
+> Designed as a **teaching reference** and a **quick productivity boost**
+
---
-## 🛠 Tech Stack & Features
+## 🔌 Inbound Adapter (Delivery)
-* 🚀 **Java 21** — modern baseline
-* 🍃 **Spring Boot 3.5**
-* 📦 **Maven 3.9+** — build and dependency management
-* 🧩 **FreeMarker templates** — for generator extensibility
-* 📂 **Automatic directory structure** — `src/main/java`, `src/test/java`, etc.
-* 🧪 **JUnit 5** — generated test classes
+| Adapter | Status |
+| ------- | ---------------- |
+| CLI | ✔ Primary driver |
+| REST | Planned |
---
-## 🧩 Architecture
+## ⚙️ Outbound Adapters & Artifacts
+
+Active profile:
-This project follows a **hexagonal (ports & adapters) architecture**:
+```
+springboot‑maven‑java
+```
-* **Ports** — abstract interfaces like `ProjectBuildGenerator`, `ApplicationYamlGenerator`, `ProjectArchiver`
-* **Adapters** — framework-specific implementations (Spring Boot, Maven, FreeMarker)
-* **Core** — generation service depends only on ports, making it extensible and testable
+Generated artifacts:
-This design allows the generator to evolve independently of specific tools while staying highly testable.
+| Category | Includes |
+| --------------- | -------------------------------------------------------- |
+| Build system | `pom.xml`, Maven Wrapper |
+| Runtime config | `src/main/resources/application.yml` |
+| Source skeleton | Main application & test bootstraps |
+| Sample code | Optional greeting sample (domain + ports + REST adapter) |
+| Git hygiene | `.gitignore` |
+| Docs (minimal) | `README.md` inside generated project |
+
+> Everything required to **build ▸ run ▸ extend** a clean service
---
-## 🧪 Testing
+## 🧪 Testing & CI
-Run the test suite:
+```bash
+mvn verify
+```
+
+* Unit + integration tests ✔
+* JaCoCo coverage ✔
+* CodeQL security ✔
+* Codecov reporting ✔
+
+---
+
+## 🔄 CLI Usage Example
```bash
-mvn test
+java -jar codegen-blueprint-1.0.0.jar \
+ --cli \
+ springboot \
+ --group-id io.github.blueprintplatform.samples \
+ --artifact-id greeting-service \
+ --name "Greeting Service" \
+ --description "Hexagonal greeting sample powered by Blueprint" \
+ --package-name io.github.blueprintplatform.samples.greeting \
+ --layout hexagonal \
+ --sample-code basic \
+ --dependency web \
+ --dependency data_jpa
+```
+
+**Output (simplified)**
+
+```
+greeting-service/
+ ├── pom.xml
+ ├── src/main/java/io/github/blueprintplatform/samples/greeting/GreetingServiceApplication.java
+ ├── src/test/java/io/github/blueprintplatform/samples/greeting/GreetingServiceApplicationTests.java
+ ├── src/main/resources/application.yml
+ └── .gitignore
```
-The generator components (`pom.xml`, `.gitignore`, `application.yml`, layout, archiver) are fully covered with unit &
-integration tests.
+> Hexagonal + sample code = ready‑to‑run REST greeting service
---
-## 📖 Related Work
+## 🚀 Vision & Roadmap (Beyond 1.0.0)
+
+> Best practices should **execute**, not merely be documented.
-This tool is inspired by the need to automate repetitive **Spring Boot project initialization** tasks.
-It works well alongside other repositories
-like [spring-boot-openapi-generics-clients](https://github.com/bsayli/spring-boot-openapi-generics-clients).
+* 🧱 Hexagonal evolution kits (ports / adapters / CQRS)
+* 📈 Observability defaults (tracing / metrics)
+* 🔐 Security (OAuth2 / Keycloak patterns)
+* 🧩 Multi‑module service generation
+* 🎯 Broader ecosystem: Gradle / Kotlin / Quarkus
+* 💻 Developer UI → configure → generate → download
+
+> **Executable Architecture for modern delivery**
---
-## 🛡 License
+## 🤝 Contributing
-MIT
+Discussions:
+[https://github.com/blueprint-platform/codegen-blueprint/discussions](https://github.com/blueprint-platform/codegen-blueprint/discussions)
+
+Issues:
+[https://github.com/blueprint-platform/codegen-blueprint/issues](https://github.com/blueprint-platform/codegen-blueprint/issues)
---
-**Author:** Barış Saylı
-**GitHub:** [bsayli](https://github.com/bsayli)
+## ⭐ Support & Community
+
+If Codegen Blueprint helps you:
+👉 Please star the repo — it really matters.
+
+**Barış Saylı**
+
+GitHub — [bsayli](https://github.com/bsayli)
+LinkedIn — [linkedin.com/in/bsayli](https://www.linkedin.com/in/bsayli)
+Medium — [@baris.sayli](https://medium.com/@baris.sayli)
+
+---
+
+## 🛡 License
+
+Licensed under MIT — free for personal and commercial use.
+See: [LICENSE](LICENSE)
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..75edc97
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,25 @@
+coverage:
+ status:
+ project:
+ default:
+ target: auto
+ threshold: 1%
+ patch:
+ default:
+ target: 70%
+
+comment:
+ layout: "reach, diff, flags, files"
+ behavior: default
+
+ignore:
+ - "target/**"
+ - "**/target/**"
+ - "**/generated/**"
+ - "**/generated-sources/**"
+
+flags:
+ codegen-blueprint:
+ paths:
+ - "src/main/java/"
+ - "src/test/java/"
\ No newline at end of file
diff --git a/docs/architecture/executable-architecture-scope.md b/docs/architecture/executable-architecture-scope.md
new file mode 100644
index 0000000..a107d3d
--- /dev/null
+++ b/docs/architecture/executable-architecture-scope.md
@@ -0,0 +1,265 @@
+# Architecture Enforcement Scope — Codegen Blueprint 1.0.0 GA
+
+> This unified document defines what the **Codegen Blueprint engine enforces today (1.0.0 GA)** and what the **generated project guarantees at output** — a single reference point for architectural truth.
+
+---
+
+## 📚 Table of Contents
+
+* [1 Purpose](#1-purpose)
+* [2 Core Mental Model](#2-core-mental-model)
+* [3 Engine Enforcement Guarantees (1.0.0 GA)](#3-engine-enforcement-guarantees-100-ga)
+ * [3.1 Deterministic Project Layout](#31-deterministic-project-layout)
+ * [3.2 Naming & Identity Enforcement](#32-naming--identity-enforcement)
+ * [3.3 Spring Boot Minimal Runtime Baseline](#33-spring-boot-minimal-runtime-baseline)
+ * [3.4 Test-Ready Project](#34-test-ready-project)
+ * [3.5 Separation of Engine & Templates](#35-separation-of-engine--templates)
+ * [3.6 Profile‑Driven Execution](#36-profile-driven-execution)
+* [4 Generated Project Scope (Output Contract)](#4-generated-project-scope-output-contract)
+* [5 Explicitly Not Enforced (Yet)](#5-explicitly-not-enforced-yet)
+* [6 Intentional Scope Constraints](#6-intentional-scope-constraints)
+* [7 Path Toward Executable Architecture](#7-path-toward-executable-architecture)
+* [8 Review Guidance](#8-review-guidance)
+
+---
+
+## 1️⃣ Purpose
+
+Ensure that:
+
+* README **claims** match **actual engine guarantees**
+* Teams get a **predictable**, **testable**, **clean** project every time
+* Foundations are in place for **strict boundary enforcement** later
+
+> 🧠 If we promise it, we enforce it.
+
+---
+
+## 2️⃣ Core Mental Model
+
+| Concept | Description |
+| -------------- | ---------------------------------------------------- |
+| **Engine** | CLI‑driven generator applying architectural profiles |
+| **Profile** | Defines language + build tool + architecture layout |
+| **Blueprints** | Artifact templates (POM, YAML, sources, docs) |
+
+📌 The engine today:
+
+> Generates clean, production‑viable Spring Boot services — with architecture *prepared* for enforcement.
+
+---
+
+## 3️⃣ Engine Enforcement Guarantees (1.0.0 GA)
+
+These are **strict contracts** validated through automated tests.
+
+### 3.1 Deterministic Project Layout
+
+Generated structure **must** follow:
+
+```
+/
+ ├─ pom.xml
+ ├─ src/main/java//
+ ├─ src/test/java//
+ ├─ src/main/resources/application.yml
+ ├─ .gitignore
+ └─ README.md
+```
+
+Always **single‑module**.
+
+---
+
+### 3.2 Naming & Identity Enforcement
+
+Engine **normalizes + validates**:
+
+* groupId
+* artifactId
+* package name
+* application name
+
+Main class rule:
+
+```
+Application
+```
+
+> ❌ Invalid identifiers → **fail fast**
+
+---
+
+### 3.3 Spring Boot Minimal Runtime Baseline
+
+Project must:
+
+* ✔ compile + run instantly
+* ✔ use explicitly provided dependencies only
+* ✔ bootstrap through SpringApplication.run(...)
+
+📌 No accidental demo code.
+
+---
+
+### 3.4 Test Ready Project
+
+Generated project must:
+
+* contain test execution entrypoint via `@SpringBootTest`
+* pass `mvn verify` right after generation
+
+Testing is not optional.
+
+---
+
+### 3.5 Separation of Engine & Templates
+
+Engine **does not depend on**:
+
+* Spring
+* File system
+* Maven internals
+
+All tech‑specific logic lives in **adapters + profiles**.
+
+> Enables Gradle, Kotlin, Quarkus… with **zero** engine refactor.
+
+---
+
+### 3.6 Profile Driven Execution
+
+```bash
+java -jar codegen-blueprint.jar \
+ --cli \
+ springboot \
+ --group-id com.acme \
+ --artifact-id order-service \
+ --name "Order Service" \
+ --package-name com.acme.order \
+ --layout hexagonal \
+ --dependency web
+```
+
+Profile determines:
+
+* templates
+* structure
+* tech behavior
+
+---
+
+## 4️⃣ Generated Project Scope (Output Contract)
+
+### Standard Profile
+
+```
+springboot-maven-java
+```
+
+### Output requirements
+
+```
+/
+ ├── pom.xml
+ ├── src/main/java//Application.java
+ ├── src/test/java//ApplicationTests.java
+ ├── src/main/resources/application.yml
+ ├── .gitignore
+ └── README.md
+```
+
+### Architecture Option (OPT‑IN)
+
+```bash
+--layout hexagonal
+```
+
+Produces structured boundaries:
+
+```
+├── domain/ # business rules only
+├── application/ # orchestrates ports
+├── adapters/ # inbound + outbound
+└── bootstrap/ # wiring + config
+```
+
+### Sample Code Option (OPT‑IN)
+
+```bash
+--sample-code basic
+```
+
+Provides ready‑to‑run teaching example:
+
+```bash
+GET /api/v1/sample/greetings/default
+→ 200 OK
+{
+ "text": "Hello from hexagonal sample!"
+}
+```
+
+Run instantly:
+
+```bash
+./mvnw spring-boot:run
+```
+
+---
+
+## 5️⃣ Explicitly Not Enforced (Yet)
+
+| Item | Reason |
+| ------------------------- | --------------------------------- |
+| Hexagonal by default | Avoid adoption friction |
+| Policy engine | Requires architectural DSL |
+| ArchUnit rules generation | Depends on next milestone |
+| Org‑wide governance | Future platform-level enforcement |
+
+> Architecture‑aware today → architecture‑policing tomorrow
+
+---
+
+## 6️⃣ Intentional Scope Constraints
+
+* 🚫 No bloated features
+* 🚫 No silent opinionated defaults
+* 🎯 Precision > volume
+* ♻️ Upgrade without core rewrites
+
+> Narrow now → **massively scalable later**
+
+---
+
+## 7️⃣ Path Toward Executable Architecture
+
+| Stage | Capability | Benefit |
+| ----- | ---------------------------- | -------------------------------- |
+| v1.1+ | Layout‑aware hex scaffolding | Real boundaries in code output |
+| v1.2+ | Auto‑architecture tests | Prevent drift at compile/CI time |
+| v1.3+ | Policy DSL | Architecture as CI quality gate |
+| v2.0 | Org profiles | Governance at organization scale |
+
+---
+
+## 8️⃣ Review Guidance
+
+Every change touching architectural behavior must answer:
+
+> ❓ Does this change **claim** enforcement?
+
+If YES → update this document.
+If NO → update README roadmap (only).
+
+---
+
+### Final Statement
+
+**Codegen Blueprint 1.0.0 GA** generates:
+
+* Clean & testable projects
+* Architecture‑aware structure
+* Predictable foundations for future enforcement
+
+> **Executable Architecture begins here.** 🚀
diff --git a/docs/design/social-preview.html b/docs/design/social-preview.html
new file mode 100644
index 0000000..71b8863
--- /dev/null
+++ b/docs/design/social-preview.html
@@ -0,0 +1,437 @@
+
+
+
+
+ Codegen Blueprint – Social Preview
+
+
+
+
+
+
+
+
+
+
+
+
Profile-Driven · Architecture-Aware
+
+
+
+ Codegen Blueprint
+
+
+
+ Profile-driven project generator with architecture options —
+ standardized Spring Boot scaffolding plus hexagonal-ready layout paths,
+ built for scalable reuse across teams.
+
+
+
+
+
+
Standardized, enterprise-ready Spring Boot services from day zero.
+
+
+
+
Hexagonal-ready layout for domain-centric, framework-independent design.
+
+
+
+
Profiles capture your stack — Spring Boot, Maven, Java 21 and beyond.
+
+
+
+
Architecture as a Product — automation that enforces best practices.
+
+
+
+
+
+ Runtime
+ Java 21
+
+
+ Framework
+ Spring Boot 3.5+
+
+
+ Build Tool
+ Maven
+
+
+ Architecture
+ Hexagonal-ready
+
+
+
+
+
+
+
+
+
+
+
+
+
// Architecture-aware scaffolding
+
+
java -jar codegen-blueprint-1.0.0.jar \
+
--cli \
+
springboot \
+
--group-id io.github.blueprintplatform.samples \
+
--artifact-id greeting-service \
+
--name "Greeting Service" \
+
--package-name io.github.blueprintplatform.samples.greeting \
+
+
// Optional flags
+
--layout hexagonal \
+
--sample-code basic \
+
--dependency web \
+
--dependency data_jpa
+
+
// Defaults → maven · java · Java 21 · Spring Boot 3.5.x
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/diagrams/architecture-overview.drawio b/docs/diagrams/architecture-overview.drawio
new file mode 100644
index 0000000..4b92cb0
--- /dev/null
+++ b/docs/diagrams/architecture-overview.drawio
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/diagrams/value-proposition.drawio b/docs/diagrams/value-proposition.drawio
new file mode 100644
index 0000000..3e0b1a2
--- /dev/null
+++ b/docs/diagrams/value-proposition.drawio
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/guides/how-to-explore-hexagonal-architecture.md b/docs/guides/how-to-explore-hexagonal-architecture.md
new file mode 100644
index 0000000..8ef93de
--- /dev/null
+++ b/docs/guides/how-to-explore-hexagonal-architecture.md
@@ -0,0 +1,319 @@
+# 🚀 Codegen-Blueprint — Hexagonal Architecture Deep Dive
+
+Welcome! This guide shows how **Hexagonal Architecture (Ports & Adapters)** is applied to a **real, production-grade project generation engine** — with strict boundaries and full test coverage.
+
+This repository demonstrates **Executable Architecture** in action:
+
+* Architecture rules **enforced by the engine**, not left to individuals
+* Domain remains **pure and framework-agnostic**
+* Technology choices are **plug-replaceable** — without core changes
+
+> Build a scalable ecosystem of services —
+> **without losing architectural consistency over time**.
+
+*Hexagonal Architecture — not just documented, but executed.*
+
+---
+
+## 📚 Table of Contents
+
+* [🧱 Architectural Overview](#-architectural-overview)
+* [🔌 Ports & Adapters](#-ports--adapters)
+ * [💼 Domain → Outbound Ports](#-domain--outbound-ports)
+ * [🧩 Application → Artifact Generation Ports](#-application--artifact-generation-ports)
+ * [🛠️ Technology Adapters](#-technology-adapters)
+* [📦 Profiles: Externalized Architecture Rules](#-profiles-externalized-architecture-rules)
+* [🧱 Source Layout Generation](#-source-layout-generation)
+* [📄 Resource Model — Stronger than “Files”](#-resource-model--stronger-than-files)
+* [🧪 Testing Strategy](#-testing-strategy)
+* [🎯 What You Can Learn Here](#-what-you-can-learn-here)
+* [🎮 Try It — CLI Adapter](#-try-it--cli-adapter)
+* [🔍 Start Here](#-start-here)
+* [⭐ Final Thoughts](#-final-thoughts)
+
+---
+
+## 🧱 Architectural Overview
+
+Codegen Blueprint applies **strict inward dependency flow** — ensuring the **domain stays pure** and fully independent of frameworks:
+
+```
+bootstrap // Spring & runtime wiring only
+↓
+adapter // technology-specific implementations (CLI, File, Templating…)
+↓
+application // orchestration, profiles, generation rules
+↓
+domain // core business rules — no external dependencies
+```
+
+### Key Principles
+
+* **Domain-centric** — business logic remains framework-free
+* **Replaceable adapters** — switch technology with no core changes
+* **Independent testing** — every layer testable on its own
+* **Evolution-ready** — new profiles or stacks plug in without refactor
+
+> Architecture is not a *guideline* here —
+> **it is enforced by design**
+
+---
+
+## 🔌 Ports & Adapters
+
+The engine is **driven by ports (interfaces)** — fully decoupled from frameworks.
+
+---
+
+### 💼 Domain → Outbound Ports
+
+These ports allow the **application layer** to perform external actions
+**without** depending on external technology:
+
+| Port | Responsibility |
+| --------------------- | ----------------------------------------------------- |
+| `ProjectRootPort` | Resolve and prepare the output project directory |
+| `ProjectWriterPort` | Persist generated resources (text / binary / folders) |
+| `ProjectArchiverPort` | Bundle project for delivery (e.g., ZIP packaging) |
+
+> Same domain → multiple tech stacks → zero changes to business rules
+
+---
+
+### 🧩 Application → Artifact Generation Ports
+
+Each artifact in the produced project has a **dedicated generation port**:
+
+| Port | Output artifact |
+| ------------------------------ | ------------------------------------------------------ |
+| `SourceLayoutPort` | Java package structure & source folders |
+| `MainSourceEntrypointPort` | Main application class |
+| `TestSourceEntrypointPort` | Test bootstrap |
+| `ApplicationConfigurationPort` | Runtime configuration (`application.yml`) |
+| `BuildConfigurationPort` | Build descriptor (`pom.xml`) |
+| `BuildToolFilesPort` | Wrapper + tool metadata (`mvnw`, `.mvn/`) |
+| `IgnoreRulesPort` | `.gitignore` + VCS hygiene |
+| `ProjectDocumentationPort` | Generated project README |
+| `SampleCodePort` | Optional greeting sample (domain + ports + REST demo) |
+
+Supporting the pipeline:
+
+| Component | Role |
+| -------------------------- | --------------------------------------------------- |
+| `ProjectArtifactsPort` | Executes artifacts in correct architectural order |
+| `ProjectArtifactsSelector` | Chooses implementation based on selected TechStack |
+
+> Every artifact is intentional → nothing accidental is generated
+
+---
+
+### 🛠️ Technology Adapters
+
+Adapters **implement ports using real world tooling**:
+
+* File system access
+* FreeMarker-based resource templating
+* Maven build metadata
+
+Designed for evolution:
+
+* Gradle
+* Kotlin
+* Quarkus
+* REST delivery
+
+⬆ All can be added **without touching domain or application code**
+
+---
+
+> **Ports define the architecture**
+> **Adapters only enable execution**
+
+---
+
+## 📦 Profiles: Externalized Architecture Rules
+
+Profiles define what artifacts are generated, in what order, and under which architecture rules:
+
+* Template namespace (profile defines rendering folders)
+* Enabled artifacts per stack
+* Strict generation ordering — architecture enforcement
+
+📍 Example — `springboot-maven-java` 1.0.0 pipeline
+
+```
+build-config → build-tool-files → ignore-rules
+→ source-layout → app-config
+→ main-source-entrypoint → test-source-entrypoint
+→ sample-code (optional)
+→ project-documentation
+```
+
+> Profiles ensure **hexagonal evolution** does not require code changes — only configuration.
+
+
+---
+
+## 🧱 Source Layout Generation
+
+`SOURCE_LAYOUT` adapter now generates:
+
+### Standard Layout
+
+```
+src/main/java//
+src/main/resources/
+src/test/java//
+src/test/resources/
+```
+
+### Hexagonal layout (opt-in an evolution path)
+
+```
+src/main/java//
+├─ domain/
+├─ application/
+├─ adapter/
+│ ├─ in/
+│ └─ out/
+└─ bootstrap/
+```
+
+If `--sample-code basic` is enabled:
+
+```
+adapter/in/rest/
+adapter/out/ (future)
+domain/greeting/
+application/greeting/
+```
+
+> Directories are **intentional artifacts** → not side effects.
+
+---
+
+## 📄 Resource Model — Stronger than “Files”
+
+Generated assets are modeled as first-class domain concepts:
+
+| Type | Model | Purpose |
+| --------- | -------------------------- | ------------------------------ |
+| Directory | `GeneratedDirectory` | Ensure structural correctness |
+| Text | `GeneratedTextResource` | Java, YAML, README, etc. |
+| Binary | `GeneratedBinaryResource` | Maven wrapper, future assets |
+
+Capability highlights:
+
+* Template-driven & template-less generation
+* Supports future binary artifacts (zip, images)
+* Perfect fit for multi-artifact pipelines
+
+
+---
+
+## 🧪 Testing Strategy
+
+| Test Type | Validates |
+| ------------------------ | ------------------------------------------------- |
+| **Unit Tests** | Domain rules + adapter logic |
+| **Integration Tests** | Spring wiring + ordered artifact pipeline |
+| **E2E CLI Tests** | Full generation → ZIP structure correctness |
+| **Template Coverage** | Sample code, structure, placeholders, UTF-8 model |
+
+CI includes:
+
+* 🧩 Contract tests for every port + adapter pair
+* 📊 Codecov tracking — full pipeline validation
+* 🔐 CodeQL security scanning
+* ✔ Architectural test gates planned (`ArchUnit`)
+
+### Summary
+
+* Profiles externalize **architecture rules**
+* Layout generation enforces **predictability**
+* Resource model prevents **accidental drift**
+* Tests safeguard **contract integrity**
+
+---
+
+## 🎯 What You Can Learn Here
+
+| Capability You’ll Gain | How This Repo Enables It |
+|---------------------------|-----------------------------------------------------------|
+| Hexagonal architecture | Strict boundaries, port-driven domain isolation |
+| Code generation engines | Profile-driven, ordered artifact pipelines |
+| Enterprise maintainability| Add new stacks w/o modifying core engine |
+| CI-First delivery | Coverage, contract tests, secure pipelines |
+| Architecture automation | Enforce structure from day zero — “Executable Architecture” |
+
+This is a **real production reference**, not a conceptual demo.
+
+---
+
+## 🎮 Try It — CLI Adapter
+
+Here’s the **springboot-maven-java** profile with **hexagonal** layout and **sample greeting** included:
+
+```bash
+java -jar codegen-blueprint-1.0.0.jar \
+ --cli \
+ springboot \
+ --group-id io.github.blueprintplatform.samples \
+ --artifact-id greeting-service \
+ --name "Greeting Service" \
+ --description "Hexagonal greeting sample powered by Blueprint" \
+ --package-name io.github.blueprintplatform.samples.greeting \
+ --layout hexagonal \
+ --sample-code basic \
+ --dependency web \
+ --dependency data_jpa
+```
+
+This produces a ready-to-run service with a REST greeting endpoint:
+
+```bash
+GET /api/v1/sample/greetings/default
+
+→ 200 OK: "Hello from hexagonal sample!"
+```
+
+
+Run instantly:
+cd greeting-service
+./mvnw spring-boot:run
+
+---
+
+## 🔍 Start Here
+
+Follow the architecture execution path:
+
+```
+[ CLI input ]
+ ↓
+ProjectBlueprint
+ ↓
+ProjectArtifactsSelector // chooses profile implementation
+ ↓
+ProjectArtifactsPort // executes ordered ports
+ ↓
+ProjectWriterPort // writes physical output (FS/ZIP)
+```
+
+You are watching architecture → compiled and executed.
+
+---
+
+## ⭐ Final Thoughts
+
+Executable Architecture means:
+* 🚫 No framework leaking into domain logic
+* 🧠 Architecture intent is automated, not “documented & forgotten”
+* ♻️ Adaptable tech stacks w/o core rewrites
+* 🧪 Full test enforcement from pipeline to template
+
+Built for teams who believe:
+
+“Architecture isn’t a diagram — it’s a behavior that must execute.”
+
+Happy generating! 🚀✨
\ No newline at end of file
diff --git a/docs/images/architecture/architecture-overview.png b/docs/images/architecture/architecture-overview.png
new file mode 100644
index 0000000..a5bea7e
Binary files /dev/null and b/docs/images/architecture/architecture-overview.png differ
diff --git a/docs/images/architecture/value-proposition.png b/docs/images/architecture/value-proposition.png
new file mode 100644
index 0000000..988a0c8
Binary files /dev/null and b/docs/images/architecture/value-proposition.png differ
diff --git a/docs/images/cover/cover-mini.png b/docs/images/cover/cover-mini.png
new file mode 100644
index 0000000..a7727c1
Binary files /dev/null and b/docs/images/cover/cover-mini.png differ
diff --git a/docs/images/cover/cover.png b/docs/images/cover/cover.png
new file mode 100644
index 0000000..d3837df
Binary files /dev/null and b/docs/images/cover/cover.png differ
diff --git a/docs/images/social-preview.png b/docs/images/social-preview.png
deleted file mode 100644
index c4e56f8..0000000
Binary files a/docs/images/social-preview.png and /dev/null differ
diff --git a/pom.xml b/pom.xml
index 025a124..c16e43a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,16 +7,18 @@
org.springframework.boot
spring-boot-starter-parent
- 3.5.5
+ 3.5.8
- io.github.bsayli
- codegen-springboot-initializr
- 0.2.0
- codegen-springboot-initializr
- Spring Boot project generator (CLI & programmatic)
- https://github.com/bsayli/codegen-springboot-initializr
+ io.github.blueprint-platform
+ codegen-blueprint
+ 1.0.0
+ codegen-blueprint
+ Hexagonal, profile-driven blueprint engine for generating production-ready project scaffolding across
+ frameworks and languages
+
+ https://github.com/blueprint-platform/codegen-blueprint
@@ -26,9 +28,9 @@
- https://github.com/bsayli/codegen-springboot-initializr
- scm:git:https://github.com/bsayli/codegen-springboot-initializr.git
- scm:git:ssh://git@github.com/bsayli/codegen-springboot-initializr.git
+ https://github.com/blueprint-platform/codegen-blueprint
+ scm:git:https://github.com/blueprint-platform/codegen-blueprint.git
+ scm:git:ssh://git@github.com/blueprint-platform/codegen-blueprint.git
HEAD
@@ -47,7 +49,8 @@
3.9.11
2.20.0
3.18.0
- 1.28.0
+ 0.8.13
+ 4.7.7
@@ -56,12 +59,23 @@
spring-boot-starter
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
org.apache.maven
maven-model
${maven-model.version}
+
+ info.picocli
+ picocli
+ ${picocli.version}
+
+
commons-io
commons-io
@@ -74,11 +88,6 @@
${commons-lang3.version}
-
- org.apache.commons
- commons-compress
- ${commons-compress.version}
-
org.freemarker
@@ -106,7 +115,7 @@
- io.github.bsayli.codegen.initializr
+ io.github.blueprintplatform.codegen
@@ -125,6 +134,77 @@
${project.build.sourceEncoding}
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ **/*Test.java
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+ it-tests
+
+ integration-test
+ verify
+
+
+
+ **/*IT.java
+
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${jacoco-maven-plugin.version}
+
+
+ prepare-agent
+
+ prepare-agent
+
+
+
+
+ prepare-agent-integration
+
+ prepare-agent-integration
+
+
+
+
+ report
+ verify
+
+ report
+
+
+
+
+ report-integration
+ verify
+
+ report-integration
+
+
+
+
+
+
+ it
+
+
\ No newline at end of file
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/CodegenSpringbootInitializrApplication.java b/src/main/java/io/github/blueprintplatform/codegen/CodegenBlueprintApplication.java
similarity index 51%
rename from src/main/java/io/github/bsayli/codegen/initializr/CodegenSpringbootInitializrApplication.java
rename to src/main/java/io/github/blueprintplatform/codegen/CodegenBlueprintApplication.java
index db93952..155e75e 100644
--- a/src/main/java/io/github/bsayli/codegen/initializr/CodegenSpringbootInitializrApplication.java
+++ b/src/main/java/io/github/blueprintplatform/codegen/CodegenBlueprintApplication.java
@@ -1,14 +1,14 @@
-package io.github.bsayli.codegen.initializr;
+package io.github.blueprintplatform.codegen;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication
-@ConfigurationPropertiesScan(basePackages = "io.github.bsayli.codegen.initializr")
-public class CodegenSpringbootInitializrApplication {
+@ConfigurationPropertiesScan(basePackages = "io.github.blueprintplatform.codegen")
+public class CodegenBlueprintApplication {
public static void main(String[] args) {
- SpringApplication.run(CodegenSpringbootInitializrApplication.class, args);
+ SpringApplication.run(CodegenBlueprintApplication.class, args);
}
}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/AdapterException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/AdapterException.java
new file mode 100644
index 0000000..a28fadc
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/AdapterException.java
@@ -0,0 +1,13 @@
+package io.github.blueprintplatform.codegen.adapter.error.exception;
+
+import io.github.blueprintplatform.codegen.bootstrap.error.exception.InfrastructureException;
+
+public abstract class AdapterException extends InfrastructureException {
+ protected AdapterException(String messageKey, Object... args) {
+ super(messageKey, args);
+ }
+
+ protected AdapterException(String messageKey, Throwable cause, Object... args) {
+ super(messageKey, cause, args);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ArtifactKeyMismatchException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ArtifactKeyMismatchException.java
new file mode 100644
index 0000000..efccb04
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ArtifactKeyMismatchException.java
@@ -0,0 +1,12 @@
+package io.github.blueprintplatform.codegen.adapter.error.exception;
+
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+
+@SuppressWarnings("java:S110")
+public final class ArtifactKeyMismatchException extends AdapterException {
+ private static final String KEY = "adapter.generator.key.mismatch";
+
+ public ArtifactKeyMismatchException(ArtifactKey expected, ArtifactKey actual) {
+ super(KEY, expected.key(), actual.key());
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ArtifactsPortNotFoundException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ArtifactsPortNotFoundException.java
new file mode 100644
index 0000000..217b6b8
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ArtifactsPortNotFoundException.java
@@ -0,0 +1,19 @@
+package io.github.blueprintplatform.codegen.adapter.error.exception;
+
+import io.github.blueprintplatform.codegen.adapter.out.profile.ProfileType;
+
+@SuppressWarnings("java:S110")
+public final class ArtifactsPortNotFoundException extends AdapterException {
+
+ private static final String KEY = "adapter.artifacts.port.not.found";
+ private final ProfileType profileType;
+
+ public ArtifactsPortNotFoundException(ProfileType profileType) {
+ super(KEY, profileType.name());
+ this.profileType = profileType;
+ }
+
+ public ProfileType getProfileType() {
+ return profileType;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/InvalidDependencyAliasException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/InvalidDependencyAliasException.java
new file mode 100644
index 0000000..4070066
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/InvalidDependencyAliasException.java
@@ -0,0 +1,11 @@
+package io.github.blueprintplatform.codegen.adapter.error.exception;
+
+@SuppressWarnings("java:S110")
+public class InvalidDependencyAliasException extends AdapterException {
+
+ private static final String KEY = "adapter.dependency.alias.unknown";
+
+ public InvalidDependencyAliasException(String alias) {
+ super(KEY, alias);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectArchiveIOException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectArchiveIOException.java
new file mode 100644
index 0000000..a612603
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectArchiveIOException.java
@@ -0,0 +1,13 @@
+package io.github.blueprintplatform.codegen.adapter.error.exception;
+
+import java.nio.file.Path;
+
+@SuppressWarnings("java:S110")
+public final class ProjectArchiveIOException extends AdapterException {
+
+ private static final String KEY = "adapter.project.archive.io";
+
+ public ProjectArchiveIOException(Path root, Throwable cause) {
+ super(KEY, cause, root);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectArchiveInvalidRootException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectArchiveInvalidRootException.java
new file mode 100644
index 0000000..5b1fb69
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectArchiveInvalidRootException.java
@@ -0,0 +1,13 @@
+package io.github.blueprintplatform.codegen.adapter.error.exception;
+
+import java.nio.file.Path;
+
+@SuppressWarnings("java:S110")
+public final class ProjectArchiveInvalidRootException extends AdapterException {
+
+ private static final String KEY = "adapter.project.archive.invalid.root";
+
+ public ProjectArchiveInvalidRootException(Path root) {
+ super(KEY, root != null ? root : "");
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootAlreadyExistsException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootAlreadyExistsException.java
new file mode 100644
index 0000000..6882e37
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootAlreadyExistsException.java
@@ -0,0 +1,13 @@
+package io.github.blueprintplatform.codegen.adapter.error.exception;
+
+import java.nio.file.Path;
+
+@SuppressWarnings("java:S110")
+public final class ProjectRootAlreadyExistsException extends AdapterException {
+
+ private static final String KEY = "adapter.project-root.already-exists";
+
+ public ProjectRootAlreadyExistsException(Path path) {
+ super(KEY, path);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootIOException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootIOException.java
new file mode 100644
index 0000000..f282ee8
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootIOException.java
@@ -0,0 +1,13 @@
+package io.github.blueprintplatform.codegen.adapter.error.exception;
+
+import java.nio.file.Path;
+
+@SuppressWarnings("java:S110")
+public final class ProjectRootIOException extends AdapterException {
+
+ private static final String KEY = "adapter.project-root.io.failed";
+
+ public ProjectRootIOException(Path path, Throwable cause) {
+ super(KEY, cause, path);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootNotDirectoryException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootNotDirectoryException.java
new file mode 100644
index 0000000..9d4ce11
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectRootNotDirectoryException.java
@@ -0,0 +1,13 @@
+package io.github.blueprintplatform.codegen.adapter.error.exception;
+
+import java.nio.file.Path;
+
+@SuppressWarnings("java:S110")
+public final class ProjectRootNotDirectoryException extends AdapterException {
+
+ private static final String KEY = "adapter.project-root.not-directory";
+
+ public ProjectRootNotDirectoryException(Path path) {
+ super(KEY, path);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectWriteException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectWriteException.java
new file mode 100644
index 0000000..ab1197c
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/ProjectWriteException.java
@@ -0,0 +1,13 @@
+package io.github.blueprintplatform.codegen.adapter.error.exception;
+
+import java.nio.file.Path;
+
+@SuppressWarnings("java:S110")
+public final class ProjectWriteException extends AdapterException {
+
+ private static final String KEY = "adapter.project.write.failed";
+
+ public ProjectWriteException(Path path, Throwable cause) {
+ super(KEY, cause, path);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeLevelNotSupportedException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeLevelNotSupportedException.java
new file mode 100644
index 0000000..250cf61
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeLevelNotSupportedException.java
@@ -0,0 +1,11 @@
+package io.github.blueprintplatform.codegen.adapter.error.exception;
+
+@SuppressWarnings("java:S110")
+public final class SampleCodeLevelNotSupportedException extends AdapterException {
+
+ private static final String KEY = "adapter.sample-code.level.unsupported";
+
+ public SampleCodeLevelNotSupportedException(String levelKey) {
+ super(KEY, levelKey);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeTemplatesNotFoundException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeTemplatesNotFoundException.java
new file mode 100644
index 0000000..3f839bc
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeTemplatesNotFoundException.java
@@ -0,0 +1,12 @@
+package io.github.blueprintplatform.codegen.adapter.error.exception;
+
+@SuppressWarnings("java:S110")
+public final class SampleCodeTemplatesNotFoundException extends AdapterException {
+
+ private static final String KEY = "adapter.sample-code.templates.not-found";
+
+ public SampleCodeTemplatesNotFoundException(
+ String templateRoot, String layoutKey, String levelKey) {
+ super(KEY, templateRoot, layoutKey, levelKey);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeTemplatesScanException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeTemplatesScanException.java
new file mode 100644
index 0000000..26db9ca
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/SampleCodeTemplatesScanException.java
@@ -0,0 +1,11 @@
+package io.github.blueprintplatform.codegen.adapter.error.exception;
+
+@SuppressWarnings("java:S110")
+public final class SampleCodeTemplatesScanException extends AdapterException {
+
+ private static final String KEY = "adapter.sample-code.templates.scan.failed";
+
+ public SampleCodeTemplatesScanException(String templateRoot, Throwable cause) {
+ super(KEY, cause, templateRoot);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/TemplateRenderingException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/TemplateRenderingException.java
new file mode 100644
index 0000000..8fce891
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/TemplateRenderingException.java
@@ -0,0 +1,21 @@
+package io.github.blueprintplatform.codegen.adapter.error.exception;
+
+@SuppressWarnings("java:S110")
+public final class TemplateRenderingException extends AdapterException {
+ private static final String KEY = "adapter.template.render.failed";
+ private final String templateName;
+
+ public TemplateRenderingException(String templateName, Object... args) {
+ super(KEY, prepend(templateName, args));
+ this.templateName = templateName;
+ }
+
+ public TemplateRenderingException(String templateName, Throwable cause, Object... args) {
+ super(KEY, cause, prepend(templateName, args));
+ this.templateName = templateName;
+ }
+
+ public String getTemplateName() {
+ return templateName;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/UnsupportedProfileTypeException.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/UnsupportedProfileTypeException.java
new file mode 100644
index 0000000..d84d990
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/error/exception/UnsupportedProfileTypeException.java
@@ -0,0 +1,12 @@
+package io.github.blueprintplatform.codegen.adapter.error.exception;
+
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+
+@SuppressWarnings("java:S110")
+public final class UnsupportedProfileTypeException extends AdapterException {
+ private static final String KEY = "adapter.profile.unsupported";
+
+ public UnsupportedProfileTypeException(TechStack options) {
+ super(KEY, options.framework(), options.buildTool(), options.language());
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CliProjectRequest.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CliProjectRequest.java
new file mode 100644
index 0000000..96d34f2
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CliProjectRequest.java
@@ -0,0 +1,16 @@
+package io.github.blueprintplatform.codegen.adapter.in.cli;
+
+import java.nio.file.Path;
+import java.util.List;
+
+public record CliProjectRequest(
+ String groupId,
+ String artifactId,
+ String name,
+ String description,
+ String packageName,
+ String profile,
+ String layoutKey,
+ List dependencies,
+ String sampleCodeLevelKey,
+ Path targetDirectory) {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CodegenCliExceptionHandler.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CodegenCliExceptionHandler.java
new file mode 100644
index 0000000..7e4318f
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CodegenCliExceptionHandler.java
@@ -0,0 +1,76 @@
+package io.github.blueprintplatform.codegen.adapter.in.cli;
+
+import io.github.blueprintplatform.codegen.adapter.error.exception.AdapterException;
+import io.github.blueprintplatform.codegen.application.error.exception.ApplicationException;
+import io.github.blueprintplatform.codegen.bootstrap.error.exception.InfrastructureException;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainException;
+import java.io.IOException;
+import java.util.Locale;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.MessageSource;
+import picocli.CommandLine;
+import picocli.CommandLine.IExecutionExceptionHandler;
+import picocli.CommandLine.ParameterException;
+import picocli.CommandLine.ParseResult;
+
+public class CodegenCliExceptionHandler implements IExecutionExceptionHandler {
+
+ private static final Logger log = LoggerFactory.getLogger(CodegenCliExceptionHandler.class);
+
+ private final MessageSource messageSource;
+
+ public CodegenCliExceptionHandler(MessageSource messageSource) {
+ this.messageSource = messageSource;
+ }
+
+ @Override
+ public int handleExecutionException(Exception ex, CommandLine cmd, ParseResult parseResult) {
+
+ if (ex instanceof ParameterException parameterException) {
+ cmd.getErr().println("codegen: usage error: " + parameterException.getMessage());
+ cmd.usage(cmd.getErr());
+ return 1;
+ }
+
+ Throwable cause = (ex.getCause() != null) ? ex.getCause() : ex;
+
+ return switch (cause) {
+ case DomainException domainException -> {
+ printLocalizedError(cmd, domainException.getMessageKey(), domainException.getArgs());
+ yield 1;
+ }
+ case ApplicationException applicationException -> {
+ printLocalizedError(
+ cmd, applicationException.getMessageKey(), applicationException.getArgs());
+ yield 2;
+ }
+ case AdapterException adapterException -> {
+ printLocalizedError(cmd, adapterException.getMessageKey(), adapterException.getArgs());
+ yield 3;
+ }
+ case InfrastructureException infrastructureException -> {
+ printLocalizedError(
+ cmd, infrastructureException.getMessageKey(), infrastructureException.getArgs());
+ yield 3;
+ }
+ case IOException ioException -> {
+ cmd.getErr().println("codegen: error: I/O error occurred: " + ioException.getMessage());
+ log.error("I/O error in CLI execution", ioException);
+ yield 3;
+ }
+ default -> {
+ log.error("Unexpected CLI error", cause);
+ cmd.getErr()
+ .println("codegen: error: Unexpected failure. Please try again or open an issue.");
+ yield 99;
+ }
+ };
+ }
+
+ private void printLocalizedError(CommandLine cmd, String key, Object[] args) {
+ String resolved = messageSource.getMessage(key, args, key, Locale.getDefault());
+ cmd.getErr().println("codegen: error: " + resolved);
+ cmd.getErr().println("(code: " + key + ")");
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CodegenCommand.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CodegenCommand.java
new file mode 100644
index 0000000..704dafe
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/CodegenCommand.java
@@ -0,0 +1,12 @@
+package io.github.blueprintplatform.codegen.adapter.in.cli;
+
+import io.github.blueprintplatform.codegen.adapter.in.cli.springboot.SpringBootGenerateCommand;
+import picocli.CommandLine.Command;
+
+@Command(
+ name = "codegen",
+ mixinStandardHelpOptions = true,
+ version = "1.0.0",
+ description = "Hexagonal project code generator CLI",
+ subcommands = {SpringBootGenerateCommand.class})
+public class CodegenCommand {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/shared/KeyedEnumConverter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/shared/KeyedEnumConverter.java
new file mode 100644
index 0000000..3af833f
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/shared/KeyedEnumConverter.java
@@ -0,0 +1,19 @@
+package io.github.blueprintplatform.codegen.adapter.in.cli.shared;
+
+import io.github.blueprintplatform.codegen.domain.shared.KeyedEnum;
+import java.util.function.Function;
+import picocli.CommandLine.ITypeConverter;
+
+public final class KeyedEnumConverter & KeyedEnum> implements ITypeConverter {
+
+ private final Function delegate;
+
+ public KeyedEnumConverter(Function delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public E convert(String value) {
+ return delegate.apply(value);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/CreateProjectCommandMapper.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/CreateProjectCommandMapper.java
new file mode 100644
index 0000000..7909863
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/CreateProjectCommandMapper.java
@@ -0,0 +1,70 @@
+package io.github.blueprintplatform.codegen.adapter.in.cli.springboot;
+
+import io.github.blueprintplatform.codegen.adapter.in.cli.CliProjectRequest;
+import io.github.blueprintplatform.codegen.adapter.in.cli.springboot.dependency.SpringBootDependencyAlias;
+import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectCommand;
+import io.github.blueprintplatform.codegen.application.usecase.project.DependencyInput;
+import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeLevel;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeOptions;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+import java.util.ArrayList;
+import java.util.List;
+
+public class CreateProjectCommandMapper {
+
+ public CreateProjectCommand from(
+ CliProjectRequest request,
+ BuildTool buildTool,
+ Language language,
+ JavaVersion javaVersion,
+ SpringBootVersion bootVersion) {
+
+ TechStack techStack = new TechStack(Framework.SPRING_BOOT, buildTool, language);
+ PlatformTarget platformTarget = new SpringBootJvmTarget(javaVersion, bootVersion);
+ ProjectLayout layout = ProjectLayout.fromKey(request.layoutKey());
+ SampleCodeLevel sampleCodeLevel = SampleCodeLevel.fromKey(request.sampleCodeLevelKey());
+ List dependencies = toDependencyInputs(request.dependencies());
+ SampleCodeOptions sampleCodeOptions = new SampleCodeOptions(sampleCodeLevel);
+
+ return new CreateProjectCommand(
+ request.groupId(),
+ request.artifactId(),
+ request.name(),
+ request.description(),
+ request.packageName(),
+ techStack,
+ layout,
+ platformTarget,
+ dependencies,
+ sampleCodeOptions,
+ request.targetDirectory());
+ }
+
+ private List toDependencyInputs(List aliases) {
+ if (aliases == null || aliases.isEmpty()) {
+ return List.of();
+ }
+
+ List result = new ArrayList<>();
+
+ for (String raw : aliases) {
+ if (raw == null || raw.isBlank()) {
+ continue;
+ }
+
+ SpringBootDependencyAlias alias = SpringBootDependencyAlias.fromKey(raw);
+
+ result.add(new DependencyInput(alias.groupId(), alias.artifactId(), null, null));
+ }
+
+ return List.copyOf(result);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/SpringBootGenerateCommand.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/SpringBootGenerateCommand.java
new file mode 100644
index 0000000..4db0a9a
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/SpringBootGenerateCommand.java
@@ -0,0 +1,156 @@
+package io.github.blueprintplatform.codegen.adapter.in.cli.springboot;
+
+import io.github.blueprintplatform.codegen.adapter.in.cli.CliProjectRequest;
+import io.github.blueprintplatform.codegen.adapter.in.cli.springboot.dependency.SpringBootDependencyAlias;
+import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectResult;
+import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectUseCase;
+import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeLevel;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.Callable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+
+@Command(
+ name = "springboot",
+ mixinStandardHelpOptions = true,
+ description = "Generate a Spring Boot project scaffold (standard or hexagonal layout)")
+public class SpringBootGenerateCommand implements Callable {
+
+ private static final Logger log = LoggerFactory.getLogger(SpringBootGenerateCommand.class);
+
+ private final CreateProjectCommandMapper mapper;
+ private final CreateProjectUseCase createProjectUseCase;
+
+ @Option(
+ names = {"--group-id"},
+ required = true,
+ description = "Maven groupId, for example: com.example")
+ String groupId;
+
+ @Option(
+ names = {"--artifact-id"},
+ required = true,
+ description = "Maven artifactId, for example: demo-app")
+ String artifactId;
+
+ @Option(
+ names = {"--name"},
+ required = true,
+ description = "Human-readable project name")
+ String name;
+
+ @Option(
+ names = {"--description"},
+ required = true,
+ description = "Project description (min 10 characters)")
+ String description;
+
+ @Option(
+ names = {"--package-name"},
+ required = true,
+ description = "Base package name, for example: com.example.demo")
+ String packageName;
+
+ @Option(
+ names = {"--build-tool"},
+ required = false,
+ description = "Build tool. Valid values: ${COMPLETION-CANDIDATES}",
+ defaultValue = "maven")
+ BuildTool buildTool;
+
+ @Option(
+ names = {"--language"},
+ required = false,
+ description = "Programming language. Valid values: ${COMPLETION-CANDIDATES}",
+ defaultValue = "java")
+ Language language;
+
+ @Option(
+ names = {"--java"},
+ required = false,
+ description = "Java version. Valid values: ${COMPLETION-CANDIDATES}",
+ defaultValue = "21")
+ JavaVersion javaVersion;
+
+ @Option(
+ names = {"--boot"},
+ required = false,
+ description = "Spring Boot version. Valid values: ${COMPLETION-CANDIDATES}",
+ defaultValue = "3.5")
+ SpringBootVersion bootVersion;
+
+ @Option(
+ names = {"--layout"},
+ required = false,
+ description = "Project layout. Valid values: ${COMPLETION-CANDIDATES}",
+ defaultValue = "standard")
+ ProjectLayout layout;
+
+ @Option(
+ names = {"--dependency"},
+ required = false,
+ description = "Dependency alias, can be repeated. Available: ${COMPLETION-CANDIDATES}")
+ List dependencies;
+
+ @Option(
+ names = {"--sample-code"},
+ required = false,
+ description = "Sample code level. Valid values: ${COMPLETION-CANDIDATES}",
+ defaultValue = "none")
+ SampleCodeLevel sampleCode;
+
+ @Option(
+ names = {"--target-dir"},
+ required = false,
+ description = "Target directory for the generated project",
+ defaultValue = ".")
+ Path targetDirectory;
+
+ public SpringBootGenerateCommand(
+ CreateProjectCommandMapper mapper, CreateProjectUseCase createProjectUseCase) {
+ this.mapper = mapper;
+ this.createProjectUseCase = createProjectUseCase;
+ }
+
+ @Override
+ public Integer call() {
+ String profile = buildProfileKey(buildTool, language);
+
+ List dependencyAliases =
+ dependencies == null ? List.of() : dependencies.stream().map(Enum::name).toList();
+
+ CliProjectRequest request =
+ new CliProjectRequest(
+ groupId,
+ artifactId,
+ name,
+ description,
+ packageName,
+ profile,
+ layout.key(),
+ dependencyAliases,
+ sampleCode.key(),
+ targetDirectory);
+
+ var command = mapper.from(request, buildTool, language, javaVersion, bootVersion);
+
+ CreateProjectResult result = createProjectUseCase.handle(command);
+
+ log.info("Spring Boot project generated successfully.");
+ log.info("Archive path: {}", result.archivePath());
+
+ return 0;
+ }
+
+ private String buildProfileKey(BuildTool buildTool, Language language) {
+ return "springboot-" + buildTool.key() + "-" + language.key();
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/dependency/SpringBootDependencyAlias.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/dependency/SpringBootDependencyAlias.java
new file mode 100644
index 0000000..ab76b13
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/dependency/SpringBootDependencyAlias.java
@@ -0,0 +1,52 @@
+package io.github.blueprintplatform.codegen.adapter.in.cli.springboot.dependency;
+
+import io.github.blueprintplatform.codegen.adapter.error.exception.InvalidDependencyAliasException;
+
+public enum SpringBootDependencyAlias {
+ WEB(Constants.ORG_SPRINGFRAMEWORK_BOOT, "spring-boot-starter-web"),
+ DATA_JPA(Constants.ORG_SPRINGFRAMEWORK_BOOT, "spring-boot-starter-data-jpa"),
+ VALIDATION(Constants.ORG_SPRINGFRAMEWORK_BOOT, "spring-boot-starter-validation"),
+ ACTUATOR(Constants.ORG_SPRINGFRAMEWORK_BOOT, "spring-boot-starter-actuator"),
+ SECURITY(Constants.ORG_SPRINGFRAMEWORK_BOOT, "spring-boot-starter-security"),
+ DEVTOOLS(Constants.ORG_SPRINGFRAMEWORK_BOOT, "spring-boot-devtools");
+
+ private final String groupId;
+ private final String artifactId;
+
+ SpringBootDependencyAlias(String groupId, String artifactId) {
+ this.groupId = groupId;
+ this.artifactId = artifactId;
+ }
+
+ public static SpringBootDependencyAlias fromKey(String raw) {
+ if (raw == null || raw.isBlank()) {
+ throw new InvalidDependencyAliasException(String.valueOf(raw));
+ }
+
+ String normalized = raw.trim();
+
+ for (SpringBootDependencyAlias alias : values()) {
+ if (alias.name().equalsIgnoreCase(normalized)) {
+ return alias;
+ }
+ }
+ throw new InvalidDependencyAliasException(raw);
+ }
+
+ public String groupId() {
+ return groupId;
+ }
+
+ public String artifactId() {
+ return artifactId;
+ }
+
+ @Override
+ public String toString() {
+ return name().toLowerCase();
+ }
+
+ private static class Constants {
+ public static final String ORG_SPRINGFRAMEWORK_BOOT = "org.springframework.boot";
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependency.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependency.java
new file mode 100644
index 0000000..80b464f
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependency.java
@@ -0,0 +1,11 @@
+package io.github.blueprintplatform.codegen.adapter.out.build.maven.shared;
+
+public record PomDependency(String groupId, String artifactId, String version, String scope) {
+ public static PomDependency of(String groupId, String artifactId) {
+ return new PomDependency(groupId, artifactId, null, null);
+ }
+
+ public static PomDependency of(String groupId, String artifactId, String version, String scope) {
+ return new PomDependency(groupId, artifactId, version, scope);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependencyMapper.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependencyMapper.java
new file mode 100644
index 0000000..27bcd2a
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependencyMapper.java
@@ -0,0 +1,23 @@
+package io.github.blueprintplatform.codegen.adapter.out.build.maven.shared;
+
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PomDependencyMapper {
+
+ public List from(Dependencies dependencies) {
+ if (dependencies == null || dependencies.isEmpty()) return List.of();
+ var list = new ArrayList(dependencies.asList().size());
+ for (Dependency d : dependencies.asList()) list.add(from(d));
+ return list;
+ }
+
+ public PomDependency from(Dependency d) {
+ var v = (d.version() == null || d.version().value().isBlank()) ? null : d.version().value();
+ var s = (d.scope() == null || d.scope().value().isBlank()) ? null : d.scope().value();
+ return PomDependency.of(
+ d.coordinates().groupId().value(), d.coordinates().artifactId().value(), v, s);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectArchiverAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectArchiverAdapter.java
new file mode 100644
index 0000000..127b624
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectArchiverAdapter.java
@@ -0,0 +1,97 @@
+package io.github.blueprintplatform.codegen.adapter.out.filesystem;
+
+import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectArchiveIOException;
+import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectArchiveInvalidRootException;
+import io.github.blueprintplatform.codegen.application.port.out.archive.ProjectArchiverPort;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+public class FileSystemProjectArchiverAdapter implements ProjectArchiverPort {
+
+ private static final String ZIP_EXTENSION = ".zip";
+ private static final char ZIP_SEPARATOR = '/';
+
+ @Override
+ public Path archive(Path projectRoot, String artifactId) {
+ if (projectRoot == null) {
+ throw new ProjectArchiveInvalidRootException(null);
+ }
+
+ Path parent = projectRoot.getParent();
+ if (parent == null) {
+ throw new ProjectArchiveInvalidRootException(projectRoot);
+ }
+
+ if (!Files.exists(projectRoot) || !Files.isDirectory(projectRoot)) {
+ throw new ProjectArchiveInvalidRootException(projectRoot);
+ }
+
+ String baseName =
+ (artifactId == null || artifactId.isBlank())
+ ? projectRoot.getFileName().toString()
+ : artifactId;
+
+ Path archivePath = parent.resolve(baseName + ZIP_EXTENSION);
+
+ try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(archivePath))) {
+ writeDirectoryToZip(projectRoot, baseName, zipOut);
+ return archivePath;
+ } catch (IOException e) {
+ throw new ProjectArchiveIOException(projectRoot, e);
+ }
+ }
+
+ private void writeDirectoryToZip(Path root, String rootName, ZipOutputStream zos)
+ throws IOException {
+ Path normalizedRoot = root.toAbsolutePath().normalize();
+
+ try (Stream paths = Files.walk(normalizedRoot)) {
+ paths.forEachOrdered(
+ path -> {
+ try {
+ writeEntry(normalizedRoot, rootName, path, zos);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ });
+ } catch (UncheckedIOException e) {
+ throw e.getCause();
+ }
+ }
+
+ private void writeEntry(Path root, String rootName, Path current, ZipOutputStream zos)
+ throws IOException {
+
+ Path relative = root.relativize(current);
+ StringBuilder entryName = new StringBuilder();
+ entryName.append(rootName).append(ZIP_SEPARATOR);
+
+ String rel = relative.toString();
+ if (!rel.isEmpty()) {
+ String fsSep = root.getFileSystem().getSeparator();
+ if (!fsSep.equals(String.valueOf(ZIP_SEPARATOR))) {
+ rel = rel.replace(fsSep, String.valueOf(ZIP_SEPARATOR));
+ }
+ entryName.append(rel);
+ }
+
+ boolean directory = Files.isDirectory(current);
+ if (directory && entryName.charAt(entryName.length() - 1) != ZIP_SEPARATOR) {
+ entryName.append(ZIP_SEPARATOR);
+ }
+
+ ZipEntry entry = new ZipEntry(entryName.toString());
+ zos.putNextEntry(entry);
+
+ if (!directory) {
+ Files.copy(current, zos);
+ }
+
+ zos.closeEntry();
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectRootAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectRootAdapter.java
new file mode 100644
index 0000000..db6f0ea
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectRootAdapter.java
@@ -0,0 +1,39 @@
+package io.github.blueprintplatform.codegen.adapter.out.filesystem;
+
+import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectRootAlreadyExistsException;
+import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectRootIOException;
+import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectRootNotDirectoryException;
+import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootExistencePolicy;
+import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootPort;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class FileSystemProjectRootAdapter implements ProjectRootPort {
+
+ @Override
+ public Path prepareRoot(Path targetDir, String artifactId, ProjectRootExistencePolicy policy) {
+ Path projectRoot = targetDir.resolve(artifactId);
+
+ try {
+ if (Files.exists(projectRoot)) {
+
+ if (!Files.isDirectory(projectRoot)) {
+ throw new ProjectRootNotDirectoryException(projectRoot);
+ }
+
+ if (policy == ProjectRootExistencePolicy.FAIL_IF_EXISTS) {
+ throw new ProjectRootAlreadyExistsException(projectRoot);
+ }
+
+ return projectRoot;
+ }
+
+ Files.createDirectories(projectRoot);
+ return projectRoot;
+
+ } catch (IOException e) {
+ throw new ProjectRootIOException(projectRoot, e);
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectWriterAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectWriterAdapter.java
new file mode 100644
index 0000000..e0f44bd
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectWriterAdapter.java
@@ -0,0 +1,47 @@
+package io.github.blueprintplatform.codegen.adapter.out.filesystem;
+
+import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectWriteException;
+import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectWriterPort;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+public class FileSystemProjectWriterAdapter implements ProjectWriterPort {
+
+ @Override
+ public void writeBytes(Path projectRoot, Path relativePath, byte[] content) {
+ Path target = projectRoot.resolve(relativePath);
+ try {
+ Path parent = target.getParent();
+ if (parent != null) {
+ Files.createDirectories(parent);
+ }
+ Files.write(
+ target,
+ content,
+ StandardOpenOption.CREATE,
+ StandardOpenOption.TRUNCATE_EXISTING,
+ StandardOpenOption.WRITE);
+ } catch (IOException e) {
+ throw new ProjectWriteException(target, e);
+ }
+ }
+
+ @Override
+ public void writeText(Path projectRoot, Path relativePath, String content, Charset charset) {
+ byte[] bytes = content.getBytes(charset);
+ writeBytes(projectRoot, relativePath, bytes);
+ }
+
+ @Override
+ public void createDirectories(Path projectRoot, Path relativeDir) {
+ Path target = projectRoot.resolve(relativeDir);
+ try {
+ Files.createDirectories(target);
+ } catch (IOException e) {
+ throw new ProjectWriteException(target, e);
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileBasedArtifactsSelector.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileBasedArtifactsSelector.java
new file mode 100644
index 0000000..56db0b7
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileBasedArtifactsSelector.java
@@ -0,0 +1,32 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile;
+
+import io.github.blueprintplatform.codegen.adapter.error.exception.ArtifactsPortNotFoundException;
+import io.github.blueprintplatform.codegen.adapter.error.exception.UnsupportedProfileTypeException;
+import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsPort;
+import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsSelector;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+import java.util.Map;
+
+public class ProfileBasedArtifactsSelector implements ProjectArtifactsSelector {
+
+ private final Map registry;
+
+ public ProfileBasedArtifactsSelector(Map registry) {
+ this.registry = registry;
+ }
+
+ @Override
+ public ProjectArtifactsPort select(TechStack options) {
+ ProfileType type = ProfileType.from(options);
+ if (type == null) {
+ throw new UnsupportedProfileTypeException(options);
+ }
+
+ ProjectArtifactsPort port = registry.get(type);
+ if (port == null) {
+ throw new ArtifactsPortNotFoundException(type);
+ }
+
+ return port;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileType.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileType.java
new file mode 100644
index 0000000..c6e590a
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileType.java
@@ -0,0 +1,39 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile;
+
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+
+public enum ProfileType {
+ SPRINGBOOT_MAVEN_JAVA(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA);
+
+ private final Framework framework;
+ private final BuildTool buildTool;
+ private final Language language;
+
+ ProfileType(Framework framework, BuildTool buildTool, Language language) {
+ this.framework = framework;
+ this.buildTool = buildTool;
+ this.language = language;
+ }
+
+ public static ProfileType from(TechStack o) {
+ for (ProfileType p : values()) {
+ if (p.framework == o.framework()
+ && p.buildTool == o.buildTool()
+ && p.language == o.language()) {
+ return p;
+ }
+ }
+ return null;
+ }
+
+ private static String slug(Enum> e) {
+ return e.name().toLowerCase().replace("_", "");
+ }
+
+ public String key() {
+ return slug(framework) + "-" + slug(buildTool) + "-" + slug(language);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapter.java
new file mode 100644
index 0000000..27ab73e
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapter.java
@@ -0,0 +1,24 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java;
+
+import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsPort;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactPort;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+import java.util.List;
+import java.util.stream.StreamSupport;
+
+public class SpringBootMavenJavaArtifactsAdapter implements ProjectArtifactsPort {
+
+ private final List artifacts;
+
+ public SpringBootMavenJavaArtifactsAdapter(List artifacts) {
+ this.artifacts = artifacts;
+ }
+
+ @Override
+ public Iterable extends GeneratedResource> generate(ProjectBlueprint blueprint) {
+ return artifacts.stream()
+ .flatMap(p -> StreamSupport.stream(p.generate(blueprint).spliterator(), false))
+ .toList();
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/build/MavenPomBuildConfigurationAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/build/MavenPomBuildConfigurationAdapter.java
new file mode 100644
index 0000000..54acd0c
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/build/MavenPomBuildConfigurationAdapter.java
@@ -0,0 +1,70 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.build;
+
+import static java.util.Map.entry;
+
+import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependency;
+import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependencyMapper;
+import io.github.blueprintplatform.codegen.adapter.out.shared.artifact.AbstractSingleTemplateArtifactAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.BuildConfigurationPort;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class MavenPomBuildConfigurationAdapter extends AbstractSingleTemplateArtifactAdapter
+ implements BuildConfigurationPort {
+
+ private static final String KEY_GROUP_ID = "groupId";
+ private static final String KEY_ARTIFACT_ID = "artifactId";
+ private static final String KEY_JAVA_VERSION = "javaVersion";
+ private static final String KEY_SPRING_BOOT_VER = "springBootVersion";
+ private static final String KEY_DEPENDENCIES = "dependencies";
+ private static final String KEY_PROJECT_NAME = "projectName";
+ private static final String KEY_PROJECT_DESCRIPTION = "projectDescription";
+
+ private static final PomDependency CORE_STARTER =
+ PomDependency.of("org.springframework.boot", "spring-boot-starter");
+
+ private static final PomDependency TEST_STARTER =
+ PomDependency.of("org.springframework.boot", "spring-boot-starter-test", null, "test");
+
+ private final PomDependencyMapper pomDependencyMapper;
+
+ public MavenPomBuildConfigurationAdapter(
+ TemplateRenderer renderer,
+ ArtifactDefinition artifactDefinition,
+ PomDependencyMapper pomDependencyMapper) {
+ super(renderer, artifactDefinition);
+ this.pomDependencyMapper = pomDependencyMapper;
+ }
+
+ @Override
+ public ArtifactKey artifactKey() {
+ return ArtifactKey.BUILD_CONFIG;
+ }
+
+ @Override
+ protected Map buildModel(ProjectBlueprint bp) {
+ ProjectIdentity id = bp.getIdentity();
+ SpringBootJvmTarget pt = (SpringBootJvmTarget) bp.getPlatformTarget();
+
+ List dependencies = new ArrayList<>();
+ dependencies.add(CORE_STARTER);
+ dependencies.addAll(pomDependencyMapper.from(bp.getDependencies()));
+ dependencies.add(TEST_STARTER);
+
+ return Map.ofEntries(
+ entry(KEY_GROUP_ID, id.groupId().value()),
+ entry(KEY_ARTIFACT_ID, id.artifactId().value()),
+ entry(KEY_JAVA_VERSION, pt.java().asString()),
+ entry(KEY_SPRING_BOOT_VER, pt.springBoot().defaultVersion()),
+ entry(KEY_PROJECT_NAME, bp.getName().value()),
+ entry(KEY_PROJECT_DESCRIPTION, bp.getDescription().value()),
+ entry(KEY_DEPENDENCIES, dependencies));
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/config/ApplicationYamlAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/config/ApplicationYamlAdapter.java
new file mode 100644
index 0000000..582b69e
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/config/ApplicationYamlAdapter.java
@@ -0,0 +1,33 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.config;
+
+import static java.util.Map.entry;
+
+import io.github.blueprintplatform.codegen.adapter.out.shared.artifact.AbstractSingleTemplateArtifactAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ApplicationConfigurationPort;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity;
+import java.util.Map;
+
+public class ApplicationYamlAdapter extends AbstractSingleTemplateArtifactAdapter
+ implements ApplicationConfigurationPort {
+
+ private static final String KEY_APP_NAME = "applicationName";
+
+ public ApplicationYamlAdapter(TemplateRenderer renderer, ArtifactDefinition artifactDefinition) {
+ super(renderer, artifactDefinition);
+ }
+
+ @Override
+ public ArtifactKey artifactKey() {
+ return ArtifactKey.APP_CONFIG;
+ }
+
+ @Override
+ protected Map buildModel(ProjectBlueprint blueprint) {
+ ProjectIdentity id = blueprint.getIdentity();
+ return Map.ofEntries(entry(KEY_APP_NAME, id.artifactId().value()));
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/doc/ProjectDocumentationAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/doc/ProjectDocumentationAdapter.java
new file mode 100644
index 0000000..542f1fb
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/doc/ProjectDocumentationAdapter.java
@@ -0,0 +1,78 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.doc;
+
+import static java.util.Map.entry;
+
+import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependency;
+import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependencyMapper;
+import io.github.blueprintplatform.codegen.adapter.out.shared.artifact.AbstractSingleTemplateArtifactAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ProjectDocumentationPort;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity;
+import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+import java.util.List;
+import java.util.Map;
+
+public class ProjectDocumentationAdapter extends AbstractSingleTemplateArtifactAdapter
+ implements ProjectDocumentationPort {
+
+ private static final String KEY_PROJECT_NAME = "projectName";
+ private static final String KEY_PROJECT_DESCRIPTION = "projectDescription";
+ private static final String KEY_GROUP_ID = "groupId";
+ private static final String KEY_ARTIFACT_ID = "artifactId";
+ private static final String KEY_PACKAGE_NAME = "packageName";
+ private static final String KEY_BUILD_TOOL = "buildTool";
+ private static final String KEY_LANGUAGE = "language";
+ private static final String KEY_FRAMEWORK = "framework";
+ private static final String KEY_JAVA_VERSION = "javaVersion";
+ private static final String KEY_SPRING_BOOT_VERSION = "springBootVersion";
+ private static final String KEY_DEPENDENCIES = "dependencies";
+
+ private final PomDependencyMapper pomDependencyMapper;
+
+ public ProjectDocumentationAdapter(
+ TemplateRenderer renderer,
+ ArtifactDefinition artifactDefinition,
+ PomDependencyMapper pomDependencyMapper) {
+ super(renderer, artifactDefinition);
+ this.pomDependencyMapper = pomDependencyMapper;
+ }
+
+ @Override
+ public ArtifactKey artifactKey() {
+ return ArtifactKey.PROJECT_DOCUMENTATION;
+ }
+
+ @Override
+ protected Map buildModel(ProjectBlueprint bp) {
+ ProjectIdentity id = bp.getIdentity();
+ TechStack stack = bp.getTechStack();
+ SpringBootJvmTarget pt = (SpringBootJvmTarget) bp.getPlatformTarget();
+
+ PackageName pkg = bp.getPackageName();
+ Dependencies deps = bp.getDependencies();
+
+ List mappedDeps = pomDependencyMapper.from(deps);
+
+ boolean hex = bp.getLayout().isHexagonal();
+
+ return Map.ofEntries(
+ entry(KEY_PROJECT_NAME, bp.getName().value()),
+ entry(KEY_PROJECT_DESCRIPTION, bp.getDescription().value()),
+ entry(KEY_GROUP_ID, id.groupId().value()),
+ entry(KEY_ARTIFACT_ID, id.artifactId().value()),
+ entry(KEY_PACKAGE_NAME, pkg.value()),
+ entry(KEY_BUILD_TOOL, stack.buildTool().key()),
+ entry(KEY_LANGUAGE, stack.language().key()),
+ entry(KEY_FRAMEWORK, stack.framework().key()),
+ entry(KEY_JAVA_VERSION, pt.java().asString()),
+ entry(KEY_SPRING_BOOT_VERSION, pt.springBoot().defaultVersion()),
+ entry(KEY_DEPENDENCIES, mappedDeps),
+ entry("hasHexSample", hex));
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/ignore/GitIgnoreAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/ignore/GitIgnoreAdapter.java
new file mode 100644
index 0000000..f693a94
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/ignore/GitIgnoreAdapter.java
@@ -0,0 +1,30 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.ignore;
+
+import io.github.blueprintplatform.codegen.adapter.out.shared.artifact.AbstractSingleTemplateArtifactAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.IgnoreRulesPort;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import java.util.List;
+import java.util.Map;
+
+public class GitIgnoreAdapter extends AbstractSingleTemplateArtifactAdapter
+ implements IgnoreRulesPort {
+
+ private static final String KEY_IGNORE_LIST = "ignoreList";
+
+ public GitIgnoreAdapter(TemplateRenderer renderer, ArtifactDefinition artifactDefinition) {
+ super(renderer, artifactDefinition);
+ }
+
+ @Override
+ protected Map buildModel(ProjectBlueprint blueprint) {
+ return Map.of(KEY_IGNORE_LIST, List.of());
+ }
+
+ @Override
+ public ArtifactKey artifactKey() {
+ return ArtifactKey.IGNORE_RULES;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/sample/SampleCodeAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/sample/SampleCodeAdapter.java
new file mode 100644
index 0000000..f1ec48a
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/sample/SampleCodeAdapter.java
@@ -0,0 +1,174 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.sample;
+
+import io.github.blueprintplatform.codegen.adapter.error.exception.SampleCodeLevelNotSupportedException;
+import io.github.blueprintplatform.codegen.adapter.error.exception.SampleCodeTemplatesNotFoundException;
+import io.github.blueprintplatform.codegen.adapter.error.exception.SampleCodeTemplatesScanException;
+import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.SampleCodePort;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.bootstrap.config.SamplesProperties;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout;
+import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeLevel;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeOptions;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+import java.io.File;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class SampleCodeAdapter implements SampleCodePort {
+
+ private static final String TEMPLATES_ROOT_DIR = "templates";
+ private static final String SRC_MAIN_JAVA = "src/main/java";
+ private static final String PATH_SEPARATOR = "/";
+ private static final String JAVA_FTL_SUFFIX = ".java.ftl";
+ private static final String FTL_SUFFIX = ".ftl";
+ private static final String MODEL_KEY_PROJECT_PACKAGE_NAME = "projectPackageName";
+
+ private final TemplateRenderer renderer;
+ private final ArtifactDefinition artifactDefinition;
+ private final SamplesProperties samplesProperties;
+
+ public SampleCodeAdapter(
+ TemplateRenderer renderer,
+ ArtifactDefinition artifactDefinition,
+ SamplesProperties samplesProperties) {
+ this.renderer = renderer;
+ this.artifactDefinition = artifactDefinition;
+ this.samplesProperties = samplesProperties;
+ }
+
+ @Override
+ public ArtifactKey artifactKey() {
+ return ArtifactKey.SAMPLE_CODE;
+ }
+
+ @Override
+ public Iterable extends GeneratedResource> generate(ProjectBlueprint blueprint) {
+ SampleCodeOptions options = blueprint.getSampleCodeOptions();
+ SampleCodeLevel level = options == null ? SampleCodeLevel.NONE : options.level();
+
+ boolean levelInvalid = (level == null) || !level.isEnabled() || SampleCodeLevel.RICH == level;
+ if (levelInvalid || !blueprint.getLayout().isHexagonal()) {
+ return List.of();
+ }
+
+ ProjectLayout layout = blueprint.getLayout();
+ String templateBasePath = artifactDefinition.basePath();
+ String samplesRootRelative = resolveSamplesRoot(layout, level);
+ String templateRoot = normalizeTemplateRoot(templateBasePath, samplesRootRelative);
+
+ List templatePaths = resolveTemplatePaths(templateRoot);
+ if (templatePaths.isEmpty()) {
+ throw new SampleCodeTemplatesNotFoundException(templateRoot, layout.key(), level.key());
+ }
+
+ PackageName pkg = blueprint.getPackageName();
+ String packagePath = pkg.value().replace('.', '/');
+
+ Map model = buildModel(blueprint);
+ List generated = new ArrayList<>();
+
+ for (String fullTemplatePath : templatePaths) {
+ if (!fullTemplatePath.endsWith(JAVA_FTL_SUFFIX)) {
+ continue;
+ }
+
+ String relativeUnderRoot = fullTemplatePath.substring(templateRoot.length() + 1);
+ String javaRelative = stripSuffix(relativeUnderRoot);
+
+ Path outPath = Paths.get(SRC_MAIN_JAVA).resolve(packagePath).resolve(javaRelative);
+
+ GeneratedResource resource = renderer.renderUtf8(outPath, fullTemplatePath, model);
+
+ generated.add(resource);
+ }
+
+ return List.copyOf(generated);
+ }
+
+ private String resolveSamplesRoot(ProjectLayout layout, SampleCodeLevel level) {
+ String layoutRoot =
+ layout != null && layout.isHexagonal()
+ ? samplesProperties.hexagonal()
+ : samplesProperties.standard();
+
+ String levelDir =
+ switch (level) {
+ case BASIC -> samplesProperties.basicDirName();
+ case RICH -> samplesProperties.richDirName();
+ case NONE -> throw new SampleCodeLevelNotSupportedException(level.key());
+ };
+
+ String normalizedLayoutRoot =
+ layoutRoot.endsWith(PATH_SEPARATOR)
+ ? layoutRoot.substring(0, layoutRoot.length() - 1)
+ : layoutRoot;
+
+ return normalizedLayoutRoot + PATH_SEPARATOR + levelDir;
+ }
+
+ private String normalizeTemplateRoot(String basePath, String samplesRootRelative) {
+ String normalizedBase =
+ basePath.endsWith(PATH_SEPARATOR) ? basePath.substring(0, basePath.length() - 1) : basePath;
+ return normalizedBase + PATH_SEPARATOR + samplesRootRelative;
+ }
+
+ private Map buildModel(ProjectBlueprint blueprint) {
+ PackageName pkg = blueprint.getPackageName();
+ return Map.of(MODEL_KEY_PROJECT_PACKAGE_NAME, pkg.value());
+ }
+
+ private List resolveTemplatePaths(String templateRoot) {
+ List result = new ArrayList<>();
+
+ ClassLoader cl = Thread.currentThread().getContextClassLoader();
+ String rootWithPrefix = TEMPLATES_ROOT_DIR + PATH_SEPARATOR + templateRoot;
+
+ URL rootUrl = cl.getResource(rootWithPrefix);
+ if (rootUrl == null) {
+ return result;
+ }
+
+ try {
+ Path rootPath = Paths.get(rootUrl.toURI());
+
+ try (var paths = java.nio.file.Files.walk(rootPath)) {
+ paths
+ .filter(java.nio.file.Files::isRegularFile)
+ .filter(p -> p.getFileName().toString().endsWith(FTL_SUFFIX))
+ .forEach(
+ file -> {
+ Path relativeToRoot = rootPath.relativize(file);
+ String normalizedRelativeToRoot =
+ relativeToRoot
+ .toString()
+ .replace(File.separatorChar, PATH_SEPARATOR.charAt(0));
+
+ String fullTemplatePath =
+ templateRoot + PATH_SEPARATOR + normalizedRelativeToRoot;
+
+ result.add(fullTemplatePath);
+ });
+ }
+
+ return result;
+ } catch (URISyntaxException | java.io.IOException e) {
+ throw new SampleCodeTemplatesScanException(templateRoot, e);
+ }
+ }
+
+ private String stripSuffix(String value) {
+ if (value.endsWith(FTL_SUFFIX)) {
+ return value.substring(0, value.length() - FTL_SUFFIX.length());
+ }
+ return value;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/shared/AbstractJavaSourceFileAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/shared/AbstractJavaSourceFileAdapter.java
new file mode 100644
index 0000000..3e7ebde
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/shared/AbstractJavaSourceFileAdapter.java
@@ -0,0 +1,64 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.shared;
+
+import static java.util.Map.entry;
+
+import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer;
+import io.github.blueprintplatform.codegen.adapter.shared.naming.StringCaseFormatter;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactPort;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+public abstract class AbstractJavaSourceFileAdapter implements ArtifactPort {
+
+ private static final String KEY_PROJECT_PACKAGE = "projectPackageName";
+ private static final String KEY_CLASS_NAME = "className";
+ private static final String JAVA_FILE_EXTENSION = ".java";
+
+ private static final String PACKAGE_PATH_DELIMITER = ".";
+ private static final String FILE_PATH_DELIMITER = "/";
+
+ private final TemplateRenderer renderer;
+ private final ArtifactDefinition artifactDefinition;
+ private final StringCaseFormatter stringCaseFormatter;
+
+ protected AbstractJavaSourceFileAdapter(
+ TemplateRenderer renderer,
+ ArtifactDefinition artifactDefinition,
+ StringCaseFormatter stringCaseFormatter) {
+ this.renderer = renderer;
+ this.artifactDefinition = artifactDefinition;
+ this.stringCaseFormatter = stringCaseFormatter;
+ }
+
+ @Override
+ public final Iterable extends GeneratedResource> generate(ProjectBlueprint blueprint) {
+ String className = buildClassName(blueprint);
+ PackageName packageName = blueprint.getPackageName();
+
+ Map model =
+ Map.ofEntries(
+ entry(KEY_PROJECT_PACKAGE, packageName.value()), entry(KEY_CLASS_NAME, className));
+
+ TemplateDefinition templateDefinition = artifactDefinition.templates().getFirst();
+ Path baseDir = Path.of(templateDefinition.outputPath());
+ String templateName = artifactDefinition.basePath() + templateDefinition.template();
+
+ String packagePath = packageName.value().replace(PACKAGE_PATH_DELIMITER, FILE_PATH_DELIMITER);
+ Path outPath = baseDir.resolve(packagePath).resolve(className + JAVA_FILE_EXTENSION);
+
+ GeneratedResource file = renderer.renderUtf8(outPath, templateName, model);
+ return List.of(file);
+ }
+
+ protected String pascal(String value) {
+ return stringCaseFormatter.toPascalCase(value);
+ }
+
+ protected abstract String buildClassName(ProjectBlueprint blueprint);
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/MainSourceEntrypointAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/MainSourceEntrypointAdapter.java
new file mode 100644
index 0000000..ce28c40
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/MainSourceEntrypointAdapter.java
@@ -0,0 +1,34 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.source;
+
+import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.shared.AbstractJavaSourceFileAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer;
+import io.github.blueprintplatform.codegen.adapter.shared.naming.StringCaseFormatter;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.MainSourceEntrypointPort;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity;
+
+public class MainSourceEntrypointAdapter extends AbstractJavaSourceFileAdapter
+ implements MainSourceEntrypointPort {
+
+ public static final String POSTFIX_APPLICATION = "Application";
+
+ public MainSourceEntrypointAdapter(
+ TemplateRenderer renderer,
+ ArtifactDefinition artifactDefinition,
+ StringCaseFormatter stringCaseFormatter) {
+ super(renderer, artifactDefinition, stringCaseFormatter);
+ }
+
+ @Override
+ public ArtifactKey artifactKey() {
+ return ArtifactKey.MAIN_SOURCE_ENTRY_POINT;
+ }
+
+ @Override
+ protected String buildClassName(ProjectBlueprint blueprint) {
+ ProjectIdentity id = blueprint.getIdentity();
+ return pascal(id.artifactId().value()) + POSTFIX_APPLICATION;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/SourceLayoutAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/SourceLayoutAdapter.java
new file mode 100644
index 0000000..58a670c
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/SourceLayoutAdapter.java
@@ -0,0 +1,59 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.source;
+
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.SourceLayoutPort;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout;
+import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedDirectory;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SourceLayoutAdapter implements SourceLayoutPort {
+
+ private static final Path SRC_MAIN_JAVA = Path.of("src/main/java");
+ private static final Path SRC_TEST_JAVA = Path.of("src/test/java");
+ private static final Path SRC_MAIN_RESOURCES = Path.of("src/main/resources");
+ private static final Path SRC_TEST_RESOURCES = Path.of("src/test/resources");
+
+ private static final String SEGMENT_ADAPTER = "adapter";
+ private static final String SEGMENT_APPLICATION = "application";
+ private static final String SEGMENT_BOOTSTRAP = "bootstrap";
+ private static final String SEGMENT_DOMAIN = "domain";
+
+ @Override
+ public ArtifactKey artifactKey() {
+ return ArtifactKey.SOURCE_LAYOUT;
+ }
+
+ @Override
+ public Iterable extends GeneratedResource> generate(ProjectBlueprint blueprint) {
+ List resources = new ArrayList<>();
+
+ resources.add(new GeneratedDirectory(SRC_MAIN_JAVA));
+ resources.add(new GeneratedDirectory(SRC_TEST_JAVA));
+ resources.add(new GeneratedDirectory(SRC_MAIN_RESOURCES));
+ resources.add(new GeneratedDirectory(SRC_TEST_RESOURCES));
+
+ PackageName packageName = blueprint.getPackageName();
+ String packagePath = packageName.value().replace('.', '/');
+
+ Path mainBasePackageDir = SRC_MAIN_JAVA.resolve(packagePath);
+ Path testBasePackageDir = SRC_TEST_JAVA.resolve(packagePath);
+
+ resources.add(new GeneratedDirectory(mainBasePackageDir));
+ resources.add(new GeneratedDirectory(testBasePackageDir));
+
+ ProjectLayout layout = blueprint.getLayout();
+ if (layout.isHexagonal()) {
+ resources.add(new GeneratedDirectory(mainBasePackageDir.resolve(SEGMENT_ADAPTER)));
+ resources.add(new GeneratedDirectory(mainBasePackageDir.resolve(SEGMENT_APPLICATION)));
+ resources.add(new GeneratedDirectory(mainBasePackageDir.resolve(SEGMENT_BOOTSTRAP)));
+ resources.add(new GeneratedDirectory(mainBasePackageDir.resolve(SEGMENT_DOMAIN)));
+ }
+
+ return List.copyOf(resources);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/TestSourceEntrypointAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/TestSourceEntrypointAdapter.java
new file mode 100644
index 0000000..18e77b9
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/source/TestSourceEntrypointAdapter.java
@@ -0,0 +1,34 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.source;
+
+import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.shared.AbstractJavaSourceFileAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer;
+import io.github.blueprintplatform.codegen.adapter.shared.naming.StringCaseFormatter;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.TestSourceEntrypointPort;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity;
+
+public class TestSourceEntrypointAdapter extends AbstractJavaSourceFileAdapter
+ implements TestSourceEntrypointPort {
+
+ public static final String POSTFIX_APPLICATION_TESTS = "ApplicationTests";
+
+ public TestSourceEntrypointAdapter(
+ TemplateRenderer renderer,
+ ArtifactDefinition artifactDefinition,
+ StringCaseFormatter stringCaseFormatter) {
+ super(renderer, artifactDefinition, stringCaseFormatter);
+ }
+
+ @Override
+ public ArtifactKey artifactKey() {
+ return ArtifactKey.TEST_SOURCE_ENTRY_POINT;
+ }
+
+ @Override
+ protected String buildClassName(ProjectBlueprint blueprint) {
+ ProjectIdentity id = blueprint.getIdentity();
+ return pascal(id.artifactId().value()) + POSTFIX_APPLICATION_TESTS;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/wrapper/MavenWrapperBuildToolFilesAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/wrapper/MavenWrapperBuildToolFilesAdapter.java
new file mode 100644
index 0000000..c7cf61b
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/wrapper/MavenWrapperBuildToolFilesAdapter.java
@@ -0,0 +1,38 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.wrapper;
+
+import static java.util.Map.entry;
+
+import io.github.blueprintplatform.codegen.adapter.out.shared.artifact.AbstractSingleTemplateArtifactAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.BuildToolFilesPort;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import java.util.Map;
+
+public class MavenWrapperBuildToolFilesAdapter extends AbstractSingleTemplateArtifactAdapter
+ implements BuildToolFilesPort {
+
+ private static final String KEY_WRAPPER_VERSION = "wrapperVersion";
+ private static final String KEY_MAVEN_VERSION = "mavenVersion";
+
+ private static final String DEFAULT_WRAPPER_VERSION = "3.3.4";
+ private static final String DEFAULT_MAVEN_VERSION = "3.9.11";
+
+ public MavenWrapperBuildToolFilesAdapter(
+ TemplateRenderer renderer, ArtifactDefinition artifactDefinition) {
+ super(renderer, artifactDefinition);
+ }
+
+ @Override
+ public ArtifactKey artifactKey() {
+ return ArtifactKey.BUILD_TOOL_METADATA;
+ }
+
+ @Override
+ protected Map buildModel(ProjectBlueprint blueprint) {
+ return Map.ofEntries(
+ entry(KEY_WRAPPER_VERSION, DEFAULT_WRAPPER_VERSION),
+ entry(KEY_MAVEN_VERSION, DEFAULT_MAVEN_VERSION));
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/shared/artifact/AbstractSingleTemplateArtifactAdapter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/shared/artifact/AbstractSingleTemplateArtifactAdapter.java
new file mode 100644
index 0000000..eafadb1
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/shared/artifact/AbstractSingleTemplateArtifactAdapter.java
@@ -0,0 +1,38 @@
+package io.github.blueprintplatform.codegen.adapter.out.shared.artifact;
+
+import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactPort;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+public abstract class AbstractSingleTemplateArtifactAdapter implements ArtifactPort {
+
+ private final TemplateRenderer renderer;
+ private final ArtifactDefinition artifactDefinition;
+
+ protected AbstractSingleTemplateArtifactAdapter(
+ TemplateRenderer renderer, ArtifactDefinition artifactDefinition) {
+ this.renderer = renderer;
+ this.artifactDefinition = artifactDefinition;
+ }
+
+ @Override
+ public final Iterable extends GeneratedResource> generate(ProjectBlueprint blueprint) {
+ TemplateDefinition templateDefinition = artifactDefinition.templates().getFirst();
+
+ Path outPath = Path.of(templateDefinition.outputPath());
+ String templateName = artifactDefinition.basePath() + templateDefinition.template();
+
+ Map model = buildModel(blueprint);
+ GeneratedResource file = renderer.renderUtf8(outPath, templateName, model);
+
+ return List.of(file);
+ }
+
+ protected abstract Map buildModel(ProjectBlueprint blueprint);
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/templating/FreeMarkerTemplateRenderer.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/templating/FreeMarkerTemplateRenderer.java
new file mode 100644
index 0000000..653ce60
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/templating/FreeMarkerTemplateRenderer.java
@@ -0,0 +1,32 @@
+package io.github.blueprintplatform.codegen.adapter.out.templating;
+
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import io.github.blueprintplatform.codegen.adapter.error.exception.TemplateRenderingException;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Map;
+
+public class FreeMarkerTemplateRenderer implements TemplateRenderer {
+
+ private final Configuration cfg;
+
+ public FreeMarkerTemplateRenderer(Configuration cfg) {
+ this.cfg = cfg;
+ }
+
+ @Override
+ public GeneratedResource renderUtf8(
+ Path outPath, String templateName, Map model) {
+ try (StringWriter sw = new StringWriter()) {
+ Template tpl = cfg.getTemplate(templateName);
+ tpl.process(model, sw);
+ return new GeneratedTextResource(outPath, sw.toString(), StandardCharsets.UTF_8);
+ } catch (Exception e) {
+ throw new TemplateRenderingException(templateName, e);
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/out/templating/TemplateRenderer.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/templating/TemplateRenderer.java
new file mode 100644
index 0000000..3a55016
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/out/templating/TemplateRenderer.java
@@ -0,0 +1,9 @@
+package io.github.blueprintplatform.codegen.adapter.out.templating;
+
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+import java.nio.file.Path;
+import java.util.Map;
+
+public interface TemplateRenderer {
+ GeneratedResource renderUtf8(Path outPath, String templateName, Map model);
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/adapter/shared/naming/StringCaseFormatter.java b/src/main/java/io/github/blueprintplatform/codegen/adapter/shared/naming/StringCaseFormatter.java
new file mode 100644
index 0000000..edc7805
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/adapter/shared/naming/StringCaseFormatter.java
@@ -0,0 +1,24 @@
+package io.github.blueprintplatform.codegen.adapter.shared.naming;
+
+import java.util.regex.Pattern;
+
+public class StringCaseFormatter {
+
+ private static final String EMPTY = "";
+ private static final Pattern NON_ALPHANUMERIC_DELIMITER = Pattern.compile("[^A-Za-z0-9]+");
+
+ public String toPascalCase(String raw) {
+ if (raw == null || raw.isBlank()) return EMPTY;
+
+ String[] parts = NON_ALPHANUMERIC_DELIMITER.split(raw.trim());
+ StringBuilder sb = new StringBuilder(parts.length * 8);
+
+ for (String part : parts) {
+ if (part.isEmpty()) continue;
+ sb.append(Character.toUpperCase(part.charAt(0)));
+ if (part.length() > 1) sb.append(part.substring(1).toLowerCase());
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/error/exception/ApplicationException.java b/src/main/java/io/github/blueprintplatform/codegen/application/error/exception/ApplicationException.java
new file mode 100644
index 0000000..7b6e4e9
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/error/exception/ApplicationException.java
@@ -0,0 +1,41 @@
+package io.github.blueprintplatform.codegen.application.error.exception;
+
+import java.io.Serial;
+
+public abstract class ApplicationException extends RuntimeException {
+
+ @Serial private static final long serialVersionUID = 1L;
+
+ private final String messageKey;
+ private final transient Object[] args;
+
+ protected ApplicationException(String messageKey, Object... args) {
+ super(messageKey);
+ this.messageKey = messageKey;
+ this.args = args;
+ }
+
+ protected ApplicationException(String messageKey, Throwable cause, Object... args) {
+ super(messageKey, cause);
+ this.messageKey = messageKey;
+ this.args = args;
+ }
+
+ protected static Object[] prepend(Object first, Object... rest) {
+ int extra = rest == null ? 0 : rest.length;
+ Object[] merged = new Object[1 + extra];
+ merged[0] = first;
+ if (extra > 0) {
+ System.arraycopy(rest, 0, merged, 1, extra);
+ }
+ return merged;
+ }
+
+ public String getMessageKey() {
+ return messageKey;
+ }
+
+ public Object[] getArgs() {
+ return args;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/error/exception/UnknownArtifactKeyException.java b/src/main/java/io/github/blueprintplatform/codegen/application/error/exception/UnknownArtifactKeyException.java
new file mode 100644
index 0000000..c75bb7c
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/error/exception/UnknownArtifactKeyException.java
@@ -0,0 +1,10 @@
+package io.github.blueprintplatform.codegen.application.error.exception;
+
+public final class UnknownArtifactKeyException extends ApplicationException {
+
+ private static final String KEY = "application.artifact.key.unknown";
+
+ public UnknownArtifactKeyException(String key) {
+ super(KEY, key);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/ProjectArtifactsPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/ProjectArtifactsPort.java
new file mode 100644
index 0000000..3452697
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/ProjectArtifactsPort.java
@@ -0,0 +1,9 @@
+package io.github.blueprintplatform.codegen.application.port.out;
+
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+
+public interface ProjectArtifactsPort {
+
+ Iterable extends GeneratedResource> generate(ProjectBlueprint blueprint);
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/ProjectArtifactsSelector.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/ProjectArtifactsSelector.java
new file mode 100644
index 0000000..069e4d7
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/ProjectArtifactsSelector.java
@@ -0,0 +1,7 @@
+package io.github.blueprintplatform.codegen.application.port.out;
+
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+
+public interface ProjectArtifactsSelector {
+ ProjectArtifactsPort select(TechStack options);
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/archive/ProjectArchiverPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/archive/ProjectArchiverPort.java
new file mode 100644
index 0000000..68d68cf
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/archive/ProjectArchiverPort.java
@@ -0,0 +1,7 @@
+package io.github.blueprintplatform.codegen.application.port.out.archive;
+
+import java.nio.file.Path;
+
+public interface ProjectArchiverPort {
+ Path archive(Path projectRoot, String artifactId);
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ApplicationConfigurationPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ApplicationConfigurationPort.java
new file mode 100644
index 0000000..ca71b53
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ApplicationConfigurationPort.java
@@ -0,0 +1,3 @@
+package io.github.blueprintplatform.codegen.application.port.out.artifact;
+
+public interface ApplicationConfigurationPort extends ArtifactPort {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ArtifactKey.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ArtifactKey.java
new file mode 100644
index 0000000..285093c
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ArtifactKey.java
@@ -0,0 +1,38 @@
+package io.github.blueprintplatform.codegen.application.port.out.artifact;
+
+import io.github.blueprintplatform.codegen.application.error.exception.UnknownArtifactKeyException;
+import java.util.Arrays;
+
+public enum ArtifactKey {
+ BUILD_CONFIG("build-config"),
+ BUILD_TOOL_METADATA("build-tool-metadata"),
+ IGNORE_RULES("ignore-rules"),
+ SOURCE_LAYOUT("source-layout"),
+ APP_CONFIG("app-config"),
+ MAIN_SOURCE_ENTRY_POINT("main-source-entrypoint"),
+ TEST_SOURCE_ENTRY_POINT("test-source-entrypoint"),
+ SAMPLE_CODE("sample-code"),
+ PROJECT_DOCUMENTATION("project-documentation");
+
+ private final String key;
+
+ ArtifactKey(String key) {
+ this.key = key;
+ }
+
+ public static ArtifactKey fromKey(String key) {
+ return Arrays.stream(values())
+ .filter(a -> a.key.equals(key))
+ .findFirst()
+ .orElseThrow(() -> new UnknownArtifactKeyException(key));
+ }
+
+ public String key() {
+ return key;
+ }
+
+ @Override
+ public String toString() {
+ return key;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ArtifactPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ArtifactPort.java
new file mode 100644
index 0000000..97d03e7
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ArtifactPort.java
@@ -0,0 +1,10 @@
+package io.github.blueprintplatform.codegen.application.port.out.artifact;
+
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+
+public interface ArtifactPort {
+ ArtifactKey artifactKey();
+
+ Iterable extends GeneratedResource> generate(ProjectBlueprint blueprint);
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/BuildConfigurationPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/BuildConfigurationPort.java
new file mode 100644
index 0000000..b51fa70
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/BuildConfigurationPort.java
@@ -0,0 +1,3 @@
+package io.github.blueprintplatform.codegen.application.port.out.artifact;
+
+public interface BuildConfigurationPort extends ArtifactPort {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/BuildToolFilesPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/BuildToolFilesPort.java
new file mode 100644
index 0000000..63a792b
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/BuildToolFilesPort.java
@@ -0,0 +1,3 @@
+package io.github.blueprintplatform.codegen.application.port.out.artifact;
+
+public interface BuildToolFilesPort extends ArtifactPort {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/IgnoreRulesPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/IgnoreRulesPort.java
new file mode 100644
index 0000000..acd2d23
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/IgnoreRulesPort.java
@@ -0,0 +1,3 @@
+package io.github.blueprintplatform.codegen.application.port.out.artifact;
+
+public interface IgnoreRulesPort extends ArtifactPort {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/MainSourceEntrypointPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/MainSourceEntrypointPort.java
new file mode 100644
index 0000000..844adc7
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/MainSourceEntrypointPort.java
@@ -0,0 +1,3 @@
+package io.github.blueprintplatform.codegen.application.port.out.artifact;
+
+public interface MainSourceEntrypointPort extends ArtifactPort {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ProjectDocumentationPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ProjectDocumentationPort.java
new file mode 100644
index 0000000..fc68a9e
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/ProjectDocumentationPort.java
@@ -0,0 +1,3 @@
+package io.github.blueprintplatform.codegen.application.port.out.artifact;
+
+public interface ProjectDocumentationPort extends ArtifactPort {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/SampleCodePort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/SampleCodePort.java
new file mode 100644
index 0000000..259d833
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/SampleCodePort.java
@@ -0,0 +1,3 @@
+package io.github.blueprintplatform.codegen.application.port.out.artifact;
+
+public interface SampleCodePort extends ArtifactPort {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/SourceLayoutPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/SourceLayoutPort.java
new file mode 100644
index 0000000..bf4fede
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/SourceLayoutPort.java
@@ -0,0 +1,3 @@
+package io.github.blueprintplatform.codegen.application.port.out.artifact;
+
+public interface SourceLayoutPort extends ArtifactPort {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/TestSourceEntrypointPort.java b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/TestSourceEntrypointPort.java
new file mode 100644
index 0000000..adf6ac1
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/port/out/artifact/TestSourceEntrypointPort.java
@@ -0,0 +1,3 @@
+package io.github.blueprintplatform.codegen.application.port.out.artifact;
+
+public interface TestSourceEntrypointPort extends ArtifactPort {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectCommand.java b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectCommand.java
new file mode 100644
index 0000000..1d8cc8b
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectCommand.java
@@ -0,0 +1,21 @@
+package io.github.blueprintplatform.codegen.application.usecase.project;
+
+import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeOptions;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+import java.nio.file.Path;
+import java.util.List;
+
+public record CreateProjectCommand(
+ String groupId,
+ String artifactId,
+ String projectName,
+ String projectDescription,
+ String packageName,
+ TechStack techStack,
+ ProjectLayout layout,
+ PlatformTarget platformTarget,
+ List dependencies,
+ SampleCodeOptions sampleCodeOptions,
+ Path targetDirectory) {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectHandler.java b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectHandler.java
new file mode 100644
index 0000000..85a8da3
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectHandler.java
@@ -0,0 +1,54 @@
+package io.github.blueprintplatform.codegen.application.usecase.project;
+
+import static io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootExistencePolicy.FAIL_IF_EXISTS;
+
+import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsPort;
+import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsSelector;
+import io.github.blueprintplatform.codegen.application.port.out.archive.ProjectArchiverPort;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootPort;
+import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectWriterPort;
+import java.nio.file.Path;
+
+public class CreateProjectHandler implements CreateProjectUseCase {
+
+ private final ProjectBlueprintMapper mapper;
+ private final ProjectRootPort rootPort;
+ private final ProjectArtifactsSelector artifactsSelector;
+ private final ProjectWriterPort writerPort;
+ private final ProjectArchiverPort archiverPort;
+
+ public CreateProjectHandler(
+ ProjectBlueprintMapper mapper,
+ ProjectRootPort rootPort,
+ ProjectArtifactsSelector artifactsSelector,
+ ProjectWriterPort writerPort,
+ ProjectArchiverPort archiverPort) {
+ this.mapper = mapper;
+ this.rootPort = rootPort;
+ this.artifactsSelector = artifactsSelector;
+ this.writerPort = writerPort;
+ this.archiverPort = archiverPort;
+ }
+
+ @Override
+ public CreateProjectResult handle(CreateProjectCommand command) {
+ ProjectBlueprint projectBlueprint = mapper.from(command);
+
+ Path projectRoot =
+ rootPort.prepareRoot(
+ command.targetDirectory(),
+ projectBlueprint.getIdentity().artifactId().value(),
+ FAIL_IF_EXISTS);
+
+ ProjectArtifactsPort port = artifactsSelector.select(projectBlueprint.getTechStack());
+ var files = port.generate(projectBlueprint);
+
+ writerPort.write(projectRoot, files);
+
+ String baseName = projectBlueprint.getIdentity().artifactId().value();
+ Path archive = archiverPort.archive(projectRoot, baseName);
+
+ return new CreateProjectResult(archive);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectResult.java b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectResult.java
new file mode 100644
index 0000000..04c0224
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectResult.java
@@ -0,0 +1,5 @@
+package io.github.blueprintplatform.codegen.application.usecase.project;
+
+import java.nio.file.Path;
+
+public record CreateProjectResult(Path archivePath) {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectUseCase.java b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectUseCase.java
new file mode 100644
index 0000000..ca16e97
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/CreateProjectUseCase.java
@@ -0,0 +1,5 @@
+package io.github.blueprintplatform.codegen.application.usecase.project;
+
+public interface CreateProjectUseCase {
+ CreateProjectResult handle(CreateProjectCommand command);
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/DependencyInput.java b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/DependencyInput.java
new file mode 100644
index 0000000..20faf80
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/DependencyInput.java
@@ -0,0 +1,3 @@
+package io.github.blueprintplatform.codegen.application.usecase.project;
+
+public record DependencyInput(String groupId, String artifactId, String version, String scope) {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/ProjectBlueprintMapper.java b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/ProjectBlueprintMapper.java
new file mode 100644
index 0000000..254be00
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/application/usecase/project/ProjectBlueprintMapper.java
@@ -0,0 +1,67 @@
+package io.github.blueprintplatform.codegen.application.usecase.project;
+
+import io.github.blueprintplatform.codegen.domain.factory.ProjectBlueprintFactory;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyCoordinates;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyScope;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity;
+import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName;
+import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeOptions;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ProjectBlueprintMapper {
+
+ public ProjectBlueprint from(CreateProjectCommand c) {
+ ProjectIdentity identity =
+ new ProjectIdentity(new GroupId(c.groupId()), new ArtifactId(c.artifactId()));
+
+ ProjectName name = new ProjectName(c.projectName());
+ ProjectDescription description = new ProjectDescription(c.projectDescription());
+ PackageName pkg = new PackageName(c.packageName());
+
+ PlatformTarget target = c.platformTarget();
+ ProjectLayout layout = c.layout();
+ SampleCodeOptions sampleCodeOptions = c.sampleCodeOptions();
+
+ Dependencies deps = mapDependencies(c.dependencies());
+
+ return ProjectBlueprintFactory.of(
+ identity, name, description, pkg, c.techStack(), layout, target, deps, sampleCodeOptions);
+ }
+
+ private Dependencies mapDependencies(List raw) {
+ if (raw == null || raw.isEmpty()) {
+ return Dependencies.of(List.of());
+ }
+
+ List items = new ArrayList<>(raw.size());
+ for (DependencyInput d : raw) {
+ DependencyVersion version =
+ (d.version() == null || d.version().isBlank())
+ ? null
+ : new DependencyVersion(d.version());
+
+ DependencyScope scope =
+ (d.scope() == null || d.scope().isBlank())
+ ? null
+ : DependencyScope.valueOf(d.scope().trim().toUpperCase());
+
+ items.add(
+ new Dependency(
+ new DependencyCoordinates(new GroupId(d.groupId()), new ArtifactId(d.artifactId())),
+ version,
+ scope));
+ }
+ return Dependencies.of(items);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ArtifactDefinition.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ArtifactDefinition.java
new file mode 100644
index 0000000..a97e6af
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ArtifactDefinition.java
@@ -0,0 +1,8 @@
+package io.github.blueprintplatform.codegen.bootstrap.config;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import java.util.List;
+
+public record ArtifactDefinition(
+ String basePath, @Valid @NotNull List templates) {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ArtifactKeyConverter.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ArtifactKeyConverter.java
new file mode 100644
index 0000000..26fada8
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ArtifactKeyConverter.java
@@ -0,0 +1,16 @@
+package io.github.blueprintplatform.codegen.bootstrap.config;
+
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.lang.NonNull;
+import org.springframework.stereotype.Component;
+
+@Component
+@ConfigurationPropertiesBinding
+public class ArtifactKeyConverter implements Converter {
+ @Override
+ public ArtifactKey convert(@NonNull String source) {
+ return ArtifactKey.fromKey(source);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/CodegenProfilesProperties.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/CodegenProfilesProperties.java
new file mode 100644
index 0000000..f29f43b
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/CodegenProfilesProperties.java
@@ -0,0 +1,51 @@
+package io.github.blueprintplatform.codegen.bootstrap.config;
+
+import io.github.blueprintplatform.codegen.adapter.out.profile.ProfileType;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import io.github.blueprintplatform.codegen.bootstrap.error.exception.ProfileConfigurationException;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import java.util.Map;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+
+@Validated
+@ConfigurationProperties(prefix = "codegen")
+public record CodegenProfilesProperties(
+ @Valid @NotNull Map profiles,
+ @Valid @NotNull SamplesProperties samples) {
+
+ public ArtifactDefinition artifact(ProfileType profile, ArtifactKey artifactKey) {
+ var profileProps = requireProfile(profile);
+ return requireArtifact(profile, profileProps, artifactKey);
+ }
+
+ public ProfileProperties requireProfile(ProfileType profile) {
+ var key = profile.key();
+ var profileProps = profiles.get(key);
+ if (profileProps == null) {
+ throw new ProfileConfigurationException(
+ ProfileConfigurationException.KEY_PROFILE_NOT_FOUND, key);
+ }
+ return profileProps;
+ }
+
+ ArtifactDefinition requireArtifact(
+ ProfileType profile, ProfileProperties profileProps, ArtifactKey artifactKey) {
+
+ ArtifactDefinition artifact = profileProps.artifacts().get(artifactKey.key());
+ if (artifact == null) {
+ throw new ProfileConfigurationException(
+ ProfileConfigurationException.KEY_ARTIFACT_NOT_FOUND, artifactKey.key(), profile.key());
+ }
+
+ String basePath = profileProps.templateBasePath();
+
+ if (basePath == null || basePath.isBlank()) {
+ throw new ProfileConfigurationException(
+ ProfileConfigurationException.KEY_TEMPLATE_BASE_MISSING, profile.key());
+ }
+
+ return new ArtifactDefinition(basePath, artifact.templates());
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ProfileProperties.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ProfileProperties.java
new file mode 100644
index 0000000..00db4c5
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/ProfileProperties.java
@@ -0,0 +1,13 @@
+package io.github.blueprintplatform.codegen.bootstrap.config;
+
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import java.util.List;
+import java.util.Map;
+
+public record ProfileProperties(
+ @NotBlank String templateBasePath,
+ @Valid @NotNull List orderedArtifactKeys,
+ @Valid @NotNull Map artifacts) {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/SamplesProperties.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/SamplesProperties.java
new file mode 100644
index 0000000..ecda988
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/SamplesProperties.java
@@ -0,0 +1,9 @@
+package io.github.blueprintplatform.codegen.bootstrap.config;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record SamplesProperties(
+ @NotBlank String standard,
+ @NotBlank String hexagonal,
+ @NotBlank String basicDirName,
+ @NotBlank String richDirName) {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/TemplateDefinition.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/TemplateDefinition.java
new file mode 100644
index 0000000..74c93ef
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/config/TemplateDefinition.java
@@ -0,0 +1,5 @@
+package io.github.blueprintplatform.codegen.bootstrap.config;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record TemplateDefinition(@NotBlank String template, @NotBlank String outputPath) {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/error/exception/InfrastructureException.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/error/exception/InfrastructureException.java
new file mode 100644
index 0000000..9681ddd
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/error/exception/InfrastructureException.java
@@ -0,0 +1,38 @@
+package io.github.blueprintplatform.codegen.bootstrap.error.exception;
+
+import java.io.Serial;
+
+public abstract class InfrastructureException extends RuntimeException {
+ @Serial private static final long serialVersionUID = 1L;
+
+ private final String messageKey;
+ private final transient Object[] args;
+
+ protected InfrastructureException(String messageKey, Object... args) {
+ super(messageKey);
+ this.messageKey = messageKey;
+ this.args = args;
+ }
+
+ protected InfrastructureException(String messageKey, Throwable cause, Object... args) {
+ super(messageKey, cause);
+ this.messageKey = messageKey;
+ this.args = args;
+ }
+
+ protected static Object[] prepend(Object first, Object... rest) {
+ int extra = (rest == null) ? 0 : rest.length;
+ Object[] merged = new Object[1 + extra];
+ merged[0] = first;
+ if (extra > 0) System.arraycopy(rest, 0, merged, 1, extra);
+ return merged;
+ }
+
+ public String getMessageKey() {
+ return messageKey;
+ }
+
+ public Object[] getArgs() {
+ return args;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/error/exception/ProfileConfigurationException.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/error/exception/ProfileConfigurationException.java
new file mode 100644
index 0000000..68162e6
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/error/exception/ProfileConfigurationException.java
@@ -0,0 +1,15 @@
+package io.github.blueprintplatform.codegen.bootstrap.error.exception;
+
+public final class ProfileConfigurationException extends InfrastructureException {
+ public static final String KEY_PROFILE_NOT_FOUND = "bootstrap.profile.not.found";
+ public static final String KEY_ARTIFACT_NOT_FOUND = "bootstrap.artifact.not.found";
+ public static final String KEY_TEMPLATE_BASE_MISSING = "bootstrap.template.base.missing";
+
+ public ProfileConfigurationException(String key, Object... args) {
+ super(key, args);
+ }
+
+ public ProfileConfigurationException(String key, Throwable cause, Object... args) {
+ super(key, cause, args);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/templating/FreeMarkerTemplatingConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/templating/FreeMarkerTemplatingConfig.java
new file mode 100644
index 0000000..0be67b5
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/templating/FreeMarkerTemplatingConfig.java
@@ -0,0 +1,58 @@
+package io.github.blueprintplatform.codegen.bootstrap.templating;
+
+import static freemarker.template.Configuration.VERSION_2_3_34;
+
+import freemarker.template.Configuration;
+import freemarker.template.TemplateExceptionHandler;
+import freemarker.template.Version;
+import io.github.blueprintplatform.codegen.adapter.out.templating.FreeMarkerTemplateRenderer;
+import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+@org.springframework.context.annotation.Configuration
+@EnableConfigurationProperties(FreeMarkerTemplatingProperties.class)
+public class FreeMarkerTemplatingConfig {
+
+ public static final String NUMBER_FORMAT_COMPUTER = "computer";
+
+ private static final Version FM_VER = VERSION_2_3_34;
+
+ private final FreeMarkerTemplatingProperties props;
+
+ public FreeMarkerTemplatingConfig(FreeMarkerTemplatingProperties props) {
+ this.props = props;
+ }
+
+ @Bean
+ Configuration freemarkerConfiguration() {
+ Configuration cfg = new Configuration(FM_VER);
+ cfg.setDefaultEncoding(props.encoding());
+ cfg.setOutputEncoding(props.encoding());
+ cfg.setClassForTemplateLoading(getClass(), props.templatePath());
+ cfg.setTemplateExceptionHandler(toHandler(props.handler()));
+ cfg.setLogTemplateExceptions(false);
+ cfg.setWrapUncheckedExceptions(true);
+ cfg.setLocalizedLookup(false);
+ cfg.setNumberFormat(NUMBER_FORMAT_COMPUTER);
+ cfg.setFallbackOnNullLoopVariable(false);
+
+ cfg.setTemplateUpdateDelayMilliseconds(props.cacheEnabled() ? props.cacheUpdateDelayMs() : 0L);
+
+ return cfg;
+ }
+
+ @Bean
+ TemplateRenderer templateRenderer(Configuration freemarkerConfiguration) {
+ return new FreeMarkerTemplateRenderer(freemarkerConfiguration);
+ }
+
+ private TemplateExceptionHandler toHandler(FreeMarkerTemplatingProperties.Handler h) {
+ return switch (h) {
+ case RETHROW -> TemplateExceptionHandler.RETHROW_HANDLER;
+ case DEBUG -> TemplateExceptionHandler.DEBUG_HANDLER;
+ case HTML_DEBUG -> TemplateExceptionHandler.HTML_DEBUG_HANDLER;
+ case IGNORE -> TemplateExceptionHandler.IGNORE_HANDLER;
+ };
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/templating/FreeMarkerTemplatingProperties.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/templating/FreeMarkerTemplatingProperties.java
new file mode 100644
index 0000000..0b74e6b
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/templating/FreeMarkerTemplatingProperties.java
@@ -0,0 +1,22 @@
+package io.github.blueprintplatform.codegen.bootstrap.templating;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+
+@Validated
+@ConfigurationProperties(prefix = "templating")
+public record FreeMarkerTemplatingProperties(
+ @NotBlank String encoding,
+ @NotNull Handler handler,
+ @NotBlank String templatePath,
+ boolean cacheEnabled,
+ long cacheUpdateDelayMs) {
+ public enum Handler {
+ RETHROW,
+ DEBUG,
+ HTML_DEBUG,
+ IGNORE
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/application/project/ProjectUseCaseConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/application/project/ProjectUseCaseConfig.java
new file mode 100644
index 0000000..e13a448
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/application/project/ProjectUseCaseConfig.java
@@ -0,0 +1,31 @@
+package io.github.blueprintplatform.codegen.bootstrap.wiring.application.project;
+
+import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsSelector;
+import io.github.blueprintplatform.codegen.application.port.out.archive.ProjectArchiverPort;
+import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectHandler;
+import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectUseCase;
+import io.github.blueprintplatform.codegen.application.usecase.project.ProjectBlueprintMapper;
+import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootPort;
+import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectWriterPort;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class ProjectUseCaseConfig {
+
+ @Bean
+ public ProjectBlueprintMapper projectBlueprintMapper() {
+ return new ProjectBlueprintMapper();
+ }
+
+ @Bean
+ public CreateProjectUseCase createProjectHandler(
+ ProjectBlueprintMapper mapper,
+ ProjectRootPort rootPort,
+ ProjectArtifactsSelector artifactsSelector,
+ ProjectWriterPort writerPort,
+ ProjectArchiverPort archiverPort) {
+
+ return new CreateProjectHandler(mapper, rootPort, artifactsSelector, writerPort, archiverPort);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/common/CodegenCommonConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/common/CodegenCommonConfig.java
new file mode 100644
index 0000000..c325117
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/common/CodegenCommonConfig.java
@@ -0,0 +1,20 @@
+package io.github.blueprintplatform.codegen.bootstrap.wiring.common;
+
+import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependencyMapper;
+import io.github.blueprintplatform.codegen.adapter.shared.naming.StringCaseFormatter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class CodegenCommonConfig {
+
+ @Bean
+ public StringCaseFormatter stringCaseFormatter() {
+ return new StringCaseFormatter();
+ }
+
+ @Bean
+ public PomDependencyMapper pomDependencyMapper() {
+ return new PomDependencyMapper();
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CliCommonConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CliCommonConfig.java
new file mode 100644
index 0000000..3000109
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CliCommonConfig.java
@@ -0,0 +1,21 @@
+package io.github.blueprintplatform.codegen.bootstrap.wiring.in.cli;
+
+import io.github.blueprintplatform.codegen.adapter.in.cli.CodegenCliExceptionHandler;
+import io.github.blueprintplatform.codegen.adapter.in.cli.CodegenCommand;
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class CliCommonConfig {
+
+ @Bean
+ public CodegenCommand codegenCommand() {
+ return new CodegenCommand();
+ }
+
+ @Bean
+ public CodegenCliExceptionHandler codegenCliExceptionHandler(MessageSource messageSource) {
+ return new CodegenCliExceptionHandler(messageSource);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CodegenCliRunner.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CodegenCliRunner.java
new file mode 100644
index 0000000..c13a7af
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/CodegenCliRunner.java
@@ -0,0 +1,107 @@
+package io.github.blueprintplatform.codegen.bootstrap.wiring.in.cli;
+
+import io.github.blueprintplatform.codegen.adapter.in.cli.CodegenCliExceptionHandler;
+import io.github.blueprintplatform.codegen.adapter.in.cli.CodegenCommand;
+import io.github.blueprintplatform.codegen.adapter.in.cli.shared.KeyedEnumConverter;
+import io.github.blueprintplatform.codegen.adapter.in.cli.springboot.dependency.SpringBootDependencyAlias;
+import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeLevel;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language;
+import java.util.ArrayList;
+import java.util.List;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.stereotype.Component;
+import picocli.CommandLine;
+
+@Component
+public class CodegenCliRunner implements ApplicationRunner {
+
+ private static final String CLI_OPTION_NAME = "cli";
+ private static final String CLI_FLAG = "--" + CLI_OPTION_NAME;
+ private static final String LONG_OPTION_PREFIX = "--";
+
+ private static final List FILTERED_PREFIXES = List.of("--spring.");
+
+ private final CodegenCommand codegenCommand;
+ private final CommandLine.IFactory factory;
+ private final CodegenCliExceptionHandler exceptionHandler;
+
+ public CodegenCliRunner(
+ CodegenCommand codegenCommand,
+ CommandLine.IFactory factory,
+ CodegenCliExceptionHandler exceptionHandler) {
+ this.codegenCommand = codegenCommand;
+ this.factory = factory;
+ this.exceptionHandler = exceptionHandler;
+ }
+
+ @Override
+ public void run(ApplicationArguments args) {
+ if (!args.containsOption(CLI_OPTION_NAME)) {
+ return;
+ }
+
+ var cliArgs = extractCliArgs(args.getSourceArgs());
+
+ var cmd =
+ new CommandLine(codegenCommand, factory)
+ .registerConverter(BuildTool.class, new KeyedEnumConverter<>(BuildTool::fromKey))
+ .registerConverter(Language.class, new KeyedEnumConverter<>(Language::fromKey))
+ .registerConverter(
+ ProjectLayout.class, new KeyedEnumConverter<>(ProjectLayout::fromKey))
+ .registerConverter(JavaVersion.class, new KeyedEnumConverter<>(JavaVersion::fromKey))
+ .registerConverter(
+ SpringBootVersion.class, new KeyedEnumConverter<>(SpringBootVersion::fromKey))
+ .registerConverter(
+ SampleCodeLevel.class, new KeyedEnumConverter<>(SampleCodeLevel::fromKey))
+ .registerConverter(SpringBootDependencyAlias.class, SpringBootDependencyAlias::fromKey);
+
+ cmd.setExecutionExceptionHandler(exceptionHandler);
+
+ System.exit(cmd.execute(cliArgs));
+ }
+
+ @SuppressWarnings("java:S135")
+ private String[] extractCliArgs(String[] source) {
+ var cli = new ArrayList(source.length);
+ boolean skipNextValue = false;
+
+ for (int i = 0; i < source.length; i++) {
+ var arg = source[i];
+
+ if (CLI_FLAG.equals(arg)) {
+ continue;
+ }
+ if (skipNextValue) {
+ skipNextValue = false;
+ continue;
+ }
+
+ if (shouldFilter(arg)) {
+ if (requiresValueSkip(arg, source, i)) {
+ skipNextValue = true;
+ }
+ continue;
+ }
+
+ cli.add(arg);
+ }
+
+ return cli.toArray(String[]::new);
+ }
+
+ private boolean shouldFilter(String arg) {
+ return FILTERED_PREFIXES.stream().anyMatch(arg::startsWith);
+ }
+
+ private boolean requiresValueSkip(String arg, String[] source, int index) {
+ var nextIndex = index + 1;
+ return !arg.contains("=")
+ && nextIndex < source.length
+ && !source[nextIndex].startsWith(LONG_OPTION_PREFIX);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/PicocliSpringFactory.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/PicocliSpringFactory.java
new file mode 100644
index 0000000..bdfe390
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/PicocliSpringFactory.java
@@ -0,0 +1,33 @@
+package io.github.blueprintplatform.codegen.bootstrap.wiring.in.cli;
+
+import io.github.blueprintplatform.codegen.adapter.in.cli.CodegenCommand;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Component;
+import picocli.CommandLine;
+
+@Component
+public class PicocliSpringFactory implements CommandLine.IFactory {
+
+ private static final String CLI_COMMAND_BASE_PACKAGE =
+ CodegenCommand.class.getPackage().getName();
+
+ private final ApplicationContext applicationContext;
+ private final CommandLine.IFactory defaultFactory = CommandLine.defaultFactory();
+
+ public PicocliSpringFactory(ApplicationContext applicationContext) {
+ this.applicationContext = applicationContext;
+ }
+
+ @Override
+ public K create(Class cls) throws Exception {
+ if (isCliCommandType(cls)) {
+ return applicationContext.getBean(cls);
+ }
+ return defaultFactory.create(cls);
+ }
+
+ private boolean isCliCommandType(Class> cls) {
+ String pkg = cls.getPackageName();
+ return pkg.startsWith(CLI_COMMAND_BASE_PACKAGE);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/SpringBootCliConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/SpringBootCliConfig.java
new file mode 100644
index 0000000..45536eb
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/in/cli/SpringBootCliConfig.java
@@ -0,0 +1,23 @@
+package io.github.blueprintplatform.codegen.bootstrap.wiring.in.cli;
+
+import io.github.blueprintplatform.codegen.adapter.in.cli.springboot.CreateProjectCommandMapper;
+import io.github.blueprintplatform.codegen.adapter.in.cli.springboot.SpringBootGenerateCommand;
+import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectUseCase;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class SpringBootCliConfig {
+
+ @Bean
+ public CreateProjectCommandMapper springBootCreateProjectCommandMapper() {
+ return new CreateProjectCommandMapper();
+ }
+
+ @Bean
+ public SpringBootGenerateCommand springBootGenerateCommand(
+ CreateProjectCommandMapper mapper, CreateProjectUseCase createProjectUseCase) {
+
+ return new SpringBootGenerateCommand(mapper, createProjectUseCase);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/filesystem/ProjectFilesystemConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/filesystem/ProjectFilesystemConfig.java
new file mode 100644
index 0000000..8336fce
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/filesystem/ProjectFilesystemConfig.java
@@ -0,0 +1,29 @@
+package io.github.blueprintplatform.codegen.bootstrap.wiring.out.filesystem;
+
+import io.github.blueprintplatform.codegen.adapter.out.filesystem.FileSystemProjectArchiverAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.filesystem.FileSystemProjectRootAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.filesystem.FileSystemProjectWriterAdapter;
+import io.github.blueprintplatform.codegen.application.port.out.archive.ProjectArchiverPort;
+import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootPort;
+import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectWriterPort;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class ProjectFilesystemConfig {
+
+ @Bean
+ public ProjectRootPort fileSystemProjectRootAdapter() {
+ return new FileSystemProjectRootAdapter();
+ }
+
+ @Bean
+ public ProjectWriterPort fileSystemProjectWriterAdapter() {
+ return new FileSystemProjectWriterAdapter();
+ }
+
+ @Bean
+ public ProjectArchiverPort fileSystemProjectArchiverAdapter() {
+ return new FileSystemProjectArchiverAdapter();
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/profile/ProjectArtifactsSelectorConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/profile/ProjectArtifactsSelectorConfig.java
new file mode 100644
index 0000000..bb70256
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/profile/ProjectArtifactsSelectorConfig.java
@@ -0,0 +1,31 @@
+package io.github.blueprintplatform.codegen.bootstrap.wiring.out.profile;
+
+import io.github.blueprintplatform.codegen.adapter.out.profile.ProfileBasedArtifactsSelector;
+import io.github.blueprintplatform.codegen.adapter.out.profile.ProfileType;
+import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsPort;
+import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsSelector;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.Map;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class ProjectArtifactsSelectorConfig {
+
+ @Bean
+ public Map projectArtifactsPortRegistry(
+ ProjectArtifactsPort springBootMavenJavaArtifactsAdapter) {
+
+ Map registry = new EnumMap<>(ProfileType.class);
+ registry.put(ProfileType.SPRINGBOOT_MAVEN_JAVA, springBootMavenJavaArtifactsAdapter);
+ return Collections.unmodifiableMap(registry);
+ }
+
+ @Bean
+ public ProjectArtifactsSelector projectArtifactsSelector(
+ Map projectArtifactsPortRegistry) {
+
+ return new ProfileBasedArtifactsSelector(projectArtifactsPortRegistry);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/profile/SpringBootMavenJavaConfig.java b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/profile/SpringBootMavenJavaConfig.java
new file mode 100644
index 0000000..67a50f5
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/bootstrap/wiring/out/profile/SpringBootMavenJavaConfig.java
@@ -0,0 +1,166 @@
+package io.github.blueprintplatform.codegen.bootstrap.wiring.out.profile;
+
+import io.github.blueprintplatform.codegen.adapter.error.exception.ArtifactKeyMismatchException;
+import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependencyMapper;
+import io.github.blueprintplatform.codegen.adapter.out.profile.ProfileType;
+import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.SpringBootMavenJavaArtifactsAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.build.MavenPomBuildConfigurationAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.config.ApplicationYamlAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.doc.ProjectDocumentationAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.ignore.GitIgnoreAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.sample.SampleCodeAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.source.MainSourceEntrypointAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.source.SourceLayoutAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.source.TestSourceEntrypointAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.wrapper.MavenWrapperBuildToolFilesAdapter;
+import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer;
+import io.github.blueprintplatform.codegen.adapter.shared.naming.StringCaseFormatter;
+import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsPort;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.*;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.bootstrap.config.CodegenProfilesProperties;
+import io.github.blueprintplatform.codegen.bootstrap.error.exception.ProfileConfigurationException;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class SpringBootMavenJavaConfig {
+
+ @Bean
+ BuildConfigurationPort springBootMavenJavaMavenPomBuildConfigurationAdapter(
+ TemplateRenderer renderer,
+ CodegenProfilesProperties profiles,
+ PomDependencyMapper pomDependencyMapper) {
+ ArtifactDefinition props =
+ profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.BUILD_CONFIG);
+ return new MavenPomBuildConfigurationAdapter(renderer, props, pomDependencyMapper);
+ }
+
+ @Bean
+ BuildToolFilesPort springBootMavenJavaMavenWrapperBuildToolFilesAdapter(
+ TemplateRenderer renderer, CodegenProfilesProperties profiles) {
+ ArtifactDefinition props =
+ profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.BUILD_TOOL_METADATA);
+ return new MavenWrapperBuildToolFilesAdapter(renderer, props);
+ }
+
+ @Bean
+ IgnoreRulesPort springBootMavenJavaGitIgnoreAdapter(
+ TemplateRenderer renderer, CodegenProfilesProperties profiles) {
+ ArtifactDefinition props =
+ profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.IGNORE_RULES);
+ return new GitIgnoreAdapter(renderer, props);
+ }
+
+ @Bean
+ SourceLayoutPort springBootMavenJavaSourceLayoutAdapter() {
+ return new SourceLayoutAdapter();
+ }
+
+ @Bean
+ ApplicationConfigurationPort springBootMavenJavaApplicationYamlAdapter(
+ TemplateRenderer renderer, CodegenProfilesProperties profiles) {
+ ArtifactDefinition props =
+ profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.APP_CONFIG);
+ return new ApplicationYamlAdapter(renderer, props);
+ }
+
+ @Bean
+ MainSourceEntrypointPort springBootMavenJavaMainSourceEntrypointAdapter(
+ TemplateRenderer renderer,
+ CodegenProfilesProperties profiles,
+ StringCaseFormatter stringCaseFormatter) {
+ ArtifactDefinition props =
+ profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.MAIN_SOURCE_ENTRY_POINT);
+ return new MainSourceEntrypointAdapter(renderer, props, stringCaseFormatter);
+ }
+
+ @Bean
+ TestSourceEntrypointPort springBootMavenJavaTestSourceEntrypointAdapter(
+ TemplateRenderer renderer,
+ CodegenProfilesProperties profiles,
+ StringCaseFormatter stringCaseFormatter) {
+ ArtifactDefinition props =
+ profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.TEST_SOURCE_ENTRY_POINT);
+ return new TestSourceEntrypointAdapter(renderer, props, stringCaseFormatter);
+ }
+
+ @Bean
+ SampleCodePort springBootMavenJavaSampleCodeAdapter(
+ TemplateRenderer renderer, CodegenProfilesProperties profiles) {
+ ArtifactDefinition props =
+ profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.PROJECT_DOCUMENTATION);
+ return new SampleCodeAdapter(renderer, props, profiles.samples());
+ }
+
+ @Bean
+ ProjectDocumentationPort springBootMavenJavaProjectDocumentationAdapter(
+ TemplateRenderer renderer,
+ CodegenProfilesProperties profiles,
+ PomDependencyMapper pomDependencyMapper) {
+ ArtifactDefinition props =
+ profiles.artifact(ProfileType.SPRINGBOOT_MAVEN_JAVA, ArtifactKey.PROJECT_DOCUMENTATION);
+ return new ProjectDocumentationAdapter(renderer, props, pomDependencyMapper);
+ }
+
+ @Bean
+ Map springBootMavenJavaArtifactRegistry(
+ BuildConfigurationPort springBootMavenJavaMavenPomBuildConfigurationAdapter,
+ BuildToolFilesPort springBootMavenJavaMavenWrapperBuildToolFilesAdapter,
+ IgnoreRulesPort springBootMavenJavaGitIgnoreAdapter,
+ SourceLayoutPort springBootMavenJavaSourceLayoutAdapter,
+ ApplicationConfigurationPort springBootMavenJavaApplicationYamlAdapter,
+ MainSourceEntrypointPort springBootMavenJavaMainSourceEntrypointAdapter,
+ TestSourceEntrypointPort springBootMavenJavaTestSourceEntrypointAdapter,
+ SampleCodePort springBootMavenJavaSampleCodeAdapter,
+ ProjectDocumentationPort springBootMavenJavaProjectDocumentationAdapter) {
+
+ Map registry = new EnumMap<>(ArtifactKey.class);
+ registry.put(ArtifactKey.BUILD_CONFIG, springBootMavenJavaMavenPomBuildConfigurationAdapter);
+ registry.put(
+ ArtifactKey.BUILD_TOOL_METADATA, springBootMavenJavaMavenWrapperBuildToolFilesAdapter);
+ registry.put(ArtifactKey.IGNORE_RULES, springBootMavenJavaGitIgnoreAdapter);
+ registry.put(ArtifactKey.SOURCE_LAYOUT, springBootMavenJavaSourceLayoutAdapter);
+ registry.put(ArtifactKey.APP_CONFIG, springBootMavenJavaApplicationYamlAdapter);
+ registry.put(
+ ArtifactKey.MAIN_SOURCE_ENTRY_POINT, springBootMavenJavaMainSourceEntrypointAdapter);
+ registry.put(
+ ArtifactKey.TEST_SOURCE_ENTRY_POINT, springBootMavenJavaTestSourceEntrypointAdapter);
+ registry.put(ArtifactKey.SAMPLE_CODE, springBootMavenJavaSampleCodeAdapter);
+ registry.put(ArtifactKey.PROJECT_DOCUMENTATION, springBootMavenJavaProjectDocumentationAdapter);
+ return Collections.unmodifiableMap(registry);
+ }
+
+ @Bean
+ ProjectArtifactsPort springBootMavenJavaArtifactsAdapter(
+ CodegenProfilesProperties codegenProfilesProperties,
+ Map springBootMavenJavaArtifactRegistry) {
+
+ var profile = codegenProfilesProperties.requireProfile(ProfileType.SPRINGBOOT_MAVEN_JAVA);
+ var orderedArtifactKeys = profile.orderedArtifactKeys();
+
+ List ordered =
+ orderedArtifactKeys.stream()
+ .map(
+ key -> {
+ ArtifactPort port = springBootMavenJavaArtifactRegistry.get(key);
+ if (port == null) {
+ throw new ProfileConfigurationException(
+ "bootstrap.artifact.not.found",
+ key.key(),
+ ProfileType.SPRINGBOOT_MAVEN_JAVA.key());
+ }
+ if (!port.artifactKey().equals(key)) {
+ throw new ArtifactKeyMismatchException(key, port.artifactKey());
+ }
+ return port;
+ })
+ .toList();
+
+ return new SpringBootMavenJavaArtifactsAdapter(ordered);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/ErrorCode.java b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/ErrorCode.java
new file mode 100644
index 0000000..4d38eb7
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/ErrorCode.java
@@ -0,0 +1,5 @@
+package io.github.blueprintplatform.codegen.domain.error.code;
+
+public interface ErrorCode {
+ String key();
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/ErrorKeys.java b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/ErrorKeys.java
new file mode 100644
index 0000000..fb8a771
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/ErrorKeys.java
@@ -0,0 +1,9 @@
+package io.github.blueprintplatform.codegen.domain.error.code;
+
+public final class ErrorKeys {
+ private ErrorKeys() {}
+
+ public static ErrorCode compose(Field field, Violation v) {
+ return () -> field.key() + v.suffix;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/Field.java b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/Field.java
new file mode 100644
index 0000000..ba14d8e
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/Field.java
@@ -0,0 +1,29 @@
+package io.github.blueprintplatform.codegen.domain.error.code;
+
+public enum Field implements ErrorCode {
+ PROJECT_NAME(project("name")),
+ PROJECT_DESCRIPTION(project("description")),
+ GROUP_ID(project("group-id")),
+ ARTIFACT_ID(project("artifact-id")),
+ PACKAGE_NAME(project("package-name")),
+ DEPENDENCY_VERSION(dependency("version"));
+
+ private final String key;
+
+ Field(String key) {
+ this.key = key;
+ }
+
+ private static String project(String suffix) {
+ return "project." + suffix;
+ }
+
+ private static String dependency(String suffix) {
+ return "dependency." + suffix;
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/Violation.java b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/Violation.java
new file mode 100644
index 0000000..b04eb8c
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/error/code/Violation.java
@@ -0,0 +1,21 @@
+package io.github.blueprintplatform.codegen.domain.error.code;
+
+public enum Violation {
+ NOT_BLANK(".not.blank"),
+ LENGTH(".length"),
+ INVALID_CHARS(".invalid.chars"),
+ RESERVED(".reserved"),
+
+ STARTS_WITH_LETTER(".starts.with.letter"),
+ EDGE_CHAR(".edge.char"),
+ CONSECUTIVE_CHAR(".consecutive.char"),
+ SEGMENT_FORMAT(".segment.format"),
+ RESERVED_PREFIX(".reserved.prefix"),
+ CONTROL_CHARS(".control.chars");
+
+ public final String suffix;
+
+ Violation(String s) {
+ this.suffix = s;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/error/exception/DomainException.java b/src/main/java/io/github/blueprintplatform/codegen/domain/error/exception/DomainException.java
new file mode 100644
index 0000000..4eb5a51
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/error/exception/DomainException.java
@@ -0,0 +1,26 @@
+package io.github.blueprintplatform.codegen.domain.error.exception;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+
+public abstract class DomainException extends RuntimeException {
+ private final transient ErrorCode code;
+ private final transient Object[] args;
+
+ protected DomainException(ErrorCode code, Object... args) {
+ super(code.key());
+ this.code = code;
+ this.args = args;
+ }
+
+ public ErrorCode getCode() {
+ return code;
+ }
+
+ public String getMessageKey() {
+ return code.key();
+ }
+
+ public Object[] getArgs() {
+ return args;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/error/exception/DomainViolationException.java b/src/main/java/io/github/blueprintplatform/codegen/domain/error/exception/DomainViolationException.java
new file mode 100644
index 0000000..6c27bff
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/error/exception/DomainViolationException.java
@@ -0,0 +1,9 @@
+package io.github.blueprintplatform.codegen.domain.error.exception;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+
+public class DomainViolationException extends DomainException {
+ public DomainViolationException(ErrorCode code, Object... args) {
+ super(code, args);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/factory/ProjectBlueprintFactory.java b/src/main/java/io/github/blueprintplatform/codegen/domain/factory/ProjectBlueprintFactory.java
new file mode 100644
index 0000000..2dbdd71
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/factory/ProjectBlueprintFactory.java
@@ -0,0 +1,90 @@
+package io.github.blueprintplatform.codegen.domain.factory;
+
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity;
+import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName;
+import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeOptions;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+import io.github.blueprintplatform.codegen.domain.policy.tech.CompatibilityPolicy;
+import java.util.Arrays;
+import java.util.List;
+
+public final class ProjectBlueprintFactory {
+
+ private ProjectBlueprintFactory() {}
+
+ public static ProjectBlueprint of(
+ ProjectIdentity identity,
+ ProjectName name,
+ ProjectDescription description,
+ PackageName packageName,
+ TechStack techStack,
+ ProjectLayout layout,
+ PlatformTarget platformTarget,
+ Dependencies dependencies,
+ SampleCodeOptions sampleCodeOptions) {
+
+ CompatibilityPolicy.ensureCompatible(techStack, platformTarget);
+
+ return new ProjectBlueprint(
+ identity,
+ name,
+ description,
+ packageName,
+ techStack,
+ layout,
+ platformTarget,
+ dependencies,
+ sampleCodeOptions);
+ }
+
+ public static ProjectBlueprint of(
+ ProjectIdentity identity,
+ ProjectName name,
+ ProjectDescription description,
+ PackageName packageName,
+ TechStack techStack,
+ ProjectLayout layout,
+ PlatformTarget platformTarget,
+ List dependencies) {
+
+ return of(
+ identity,
+ name,
+ description,
+ packageName,
+ techStack,
+ layout,
+ platformTarget,
+ Dependencies.of(dependencies),
+ SampleCodeOptions.none());
+ }
+
+ public static ProjectBlueprint of(
+ ProjectIdentity identity,
+ ProjectName name,
+ ProjectDescription description,
+ PackageName packageName,
+ TechStack techStack,
+ ProjectLayout layout,
+ PlatformTarget platformTarget,
+ Dependency... deps) {
+
+ return of(
+ identity,
+ name,
+ description,
+ packageName,
+ techStack,
+ layout,
+ platformTarget,
+ Dependencies.of(Arrays.asList(deps)),
+ SampleCodeOptions.none());
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/ProjectBlueprint.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/ProjectBlueprint.java
new file mode 100644
index 0000000..2ec8762
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/ProjectBlueprint.java
@@ -0,0 +1,81 @@
+package io.github.blueprintplatform.codegen.domain.model;
+
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity;
+import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName;
+import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeOptions;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+
+public class ProjectBlueprint {
+
+ private final ProjectIdentity identity;
+ private final ProjectName name;
+ private final ProjectDescription description;
+ private final PackageName packageName;
+ private final TechStack techStack;
+ private final ProjectLayout layout;
+ private final PlatformTarget platformTarget;
+ private final Dependencies dependencies;
+ private final SampleCodeOptions sampleCodeOptions;
+
+ public ProjectBlueprint(
+ ProjectIdentity identity,
+ ProjectName name,
+ ProjectDescription description,
+ PackageName packageName,
+ TechStack techStack,
+ ProjectLayout layout,
+ PlatformTarget platformTarget,
+ Dependencies dependencies,
+ SampleCodeOptions sampleCodeOptions) {
+ this.identity = identity;
+ this.name = name;
+ this.description = description;
+ this.packageName = packageName;
+ this.techStack = techStack;
+ this.layout = layout;
+ this.platformTarget = platformTarget;
+ this.dependencies = dependencies;
+ this.sampleCodeOptions = sampleCodeOptions;
+ }
+
+ public ProjectIdentity getIdentity() {
+ return identity;
+ }
+
+ public ProjectName getName() {
+ return name;
+ }
+
+ public ProjectDescription getDescription() {
+ return description;
+ }
+
+ public PackageName getPackageName() {
+ return packageName;
+ }
+
+ public TechStack getTechStack() {
+ return techStack;
+ }
+
+ public ProjectLayout getLayout() {
+ return layout;
+ }
+
+ public PlatformTarget getPlatformTarget() {
+ return platformTarget;
+ }
+
+ public Dependencies getDependencies() {
+ return dependencies;
+ }
+
+ public SampleCodeOptions getSampleCodeOptions() {
+ return sampleCodeOptions;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/Dependencies.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/Dependencies.java
new file mode 100644
index 0000000..575ba82
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/Dependencies.java
@@ -0,0 +1,24 @@
+package io.github.blueprintplatform.codegen.domain.model.value.dependency;
+
+import io.github.blueprintplatform.codegen.domain.policy.dependency.DependenciesPolicy;
+import java.util.List;
+
+public final class Dependencies {
+ private final List items;
+
+ private Dependencies(List items) {
+ this.items = List.copyOf(items);
+ }
+
+ public static Dependencies of(List raw) {
+ return new Dependencies(DependenciesPolicy.enforce(raw));
+ }
+
+ public List asList() {
+ return List.copyOf(items);
+ }
+
+ public boolean isEmpty() {
+ return items.isEmpty();
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/Dependency.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/Dependency.java
new file mode 100644
index 0000000..cafc143
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/Dependency.java
@@ -0,0 +1,20 @@
+package io.github.blueprintplatform.codegen.domain.model.value.dependency;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+
+public record Dependency(
+ DependencyCoordinates coordinates, DependencyVersion version, DependencyScope scope) {
+
+ private static final ErrorCode COORDINATES_REQUIRED = () -> "dependency.coordinates.not.blank";
+
+ public Dependency {
+ if (coordinates == null) {
+ throw new DomainViolationException(COORDINATES_REQUIRED);
+ }
+ }
+
+ public boolean isDefaultScope() {
+ return scope == null || scope == DependencyScope.COMPILE;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyCoordinates.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyCoordinates.java
new file mode 100644
index 0000000..8d528c5
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyCoordinates.java
@@ -0,0 +1,17 @@
+package io.github.blueprintplatform.codegen.domain.model.value.dependency;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId;
+
+public record DependencyCoordinates(GroupId groupId, ArtifactId artifactId) {
+
+ private static final ErrorCode COORDINATES_REQUIRED = () -> "dependency.coordinates.not.blank";
+
+ public DependencyCoordinates {
+ if (groupId == null || artifactId == null) {
+ throw new DomainViolationException(COORDINATES_REQUIRED);
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyScope.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyScope.java
new file mode 100644
index 0000000..f2ab4b0
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyScope.java
@@ -0,0 +1,25 @@
+package io.github.blueprintplatform.codegen.domain.model.value.dependency;
+
+public enum DependencyScope {
+ COMPILE("compile"),
+ PROVIDED("provided"),
+ RUNTIME("runtime"),
+ TEST("test"),
+ SYSTEM("system"),
+ IMPORT("import");
+
+ private final String value;
+
+ DependencyScope(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyVersion.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyVersion.java
new file mode 100644
index 0000000..80ed2b5
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/dependency/DependencyVersion.java
@@ -0,0 +1,9 @@
+package io.github.blueprintplatform.codegen.domain.model.value.dependency;
+
+import io.github.blueprintplatform.codegen.domain.policy.dependency.DependencyVersionPolicy;
+
+public record DependencyVersion(String value) {
+ public DependencyVersion {
+ value = DependencyVersionPolicy.enforce(value);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ArtifactId.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ArtifactId.java
new file mode 100644
index 0000000..fc01b39
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ArtifactId.java
@@ -0,0 +1,9 @@
+package io.github.blueprintplatform.codegen.domain.model.value.identity;
+
+import io.github.blueprintplatform.codegen.domain.policy.identity.ArtifactIdPolicy;
+
+public record ArtifactId(String value) {
+ public ArtifactId {
+ value = ArtifactIdPolicy.enforce(value);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/GroupId.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/GroupId.java
new file mode 100644
index 0000000..e199227
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/GroupId.java
@@ -0,0 +1,9 @@
+package io.github.blueprintplatform.codegen.domain.model.value.identity;
+
+import io.github.blueprintplatform.codegen.domain.policy.identity.GroupIdPolicy;
+
+public record GroupId(String value) {
+ public GroupId {
+ value = GroupIdPolicy.enforce(value);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ProjectIdentity.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ProjectIdentity.java
new file mode 100644
index 0000000..3fb8006
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/identity/ProjectIdentity.java
@@ -0,0 +1,15 @@
+package io.github.blueprintplatform.codegen.domain.model.value.identity;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+
+public record ProjectIdentity(GroupId groupId, ArtifactId artifactId) {
+
+ private static final ErrorCode IDENTITY_REQUIRED = () -> "project.identity.not.blank";
+
+ public ProjectIdentity {
+ if (groupId == null || artifactId == null) {
+ throw new DomainViolationException(IDENTITY_REQUIRED);
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/layout/ProjectLayout.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/layout/ProjectLayout.java
new file mode 100644
index 0000000..0394340
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/layout/ProjectLayout.java
@@ -0,0 +1,40 @@
+package io.github.blueprintplatform.codegen.domain.model.value.layout;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+import io.github.blueprintplatform.codegen.domain.shared.KeyEnumParser;
+import io.github.blueprintplatform.codegen.domain.shared.KeyedEnum;
+
+public enum ProjectLayout implements KeyedEnum {
+ STANDARD("standard"),
+ HEXAGONAL("hexagonal");
+
+ private static final ErrorCode UNKNOWN = () -> "project.layout.unknown";
+
+ private final String key;
+
+ ProjectLayout(String key) {
+ this.key = key;
+ }
+
+ public static ProjectLayout fromKey(String rawKey) {
+ return KeyEnumParser.parse(ProjectLayout.class, rawKey, UNKNOWN);
+ }
+
+ public boolean isHexagonal() {
+ return this == HEXAGONAL;
+ }
+
+ public boolean isStandard() {
+ return this == STANDARD;
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ @Override
+ public String toString() {
+ return key;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectDescription.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectDescription.java
new file mode 100644
index 0000000..82117be
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectDescription.java
@@ -0,0 +1,13 @@
+package io.github.blueprintplatform.codegen.domain.model.value.naming;
+
+import io.github.blueprintplatform.codegen.domain.policy.naming.ProjectDescriptionPolicy;
+
+public record ProjectDescription(String value) {
+ public ProjectDescription {
+ value = ProjectDescriptionPolicy.enforce(value);
+ }
+
+ public boolean isEmpty() {
+ return value.isEmpty();
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectName.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectName.java
new file mode 100644
index 0000000..adf068c
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/naming/ProjectName.java
@@ -0,0 +1,9 @@
+package io.github.blueprintplatform.codegen.domain.model.value.naming;
+
+import io.github.blueprintplatform.codegen.domain.policy.naming.ProjectNamePolicy;
+
+public record ProjectName(String value) {
+ public ProjectName {
+ value = ProjectNamePolicy.enforce(value);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/pkg/PackageName.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/pkg/PackageName.java
new file mode 100644
index 0000000..545da64
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/pkg/PackageName.java
@@ -0,0 +1,9 @@
+package io.github.blueprintplatform.codegen.domain.model.value.pkg;
+
+import io.github.blueprintplatform.codegen.domain.policy.pkg.PackageNamePolicy;
+
+public record PackageName(String value) {
+ public PackageName {
+ value = PackageNamePolicy.enforce(value);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/sample/SampleCodeLevel.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/sample/SampleCodeLevel.java
new file mode 100644
index 0000000..bd6dc24
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/sample/SampleCodeLevel.java
@@ -0,0 +1,37 @@
+package io.github.blueprintplatform.codegen.domain.model.value.sample;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+import io.github.blueprintplatform.codegen.domain.shared.KeyEnumParser;
+import io.github.blueprintplatform.codegen.domain.shared.KeyedEnum;
+
+public enum SampleCodeLevel implements KeyedEnum {
+ NONE("none"),
+ BASIC("basic"),
+ RICH("rich");
+
+ private static final ErrorCode UNKNOWN = () -> "project.sample.level.unknown";
+
+ private final String key;
+
+ SampleCodeLevel(String key) {
+ this.key = key;
+ }
+
+ public static SampleCodeLevel fromKey(String rawKey) {
+ return KeyEnumParser.parse(SampleCodeLevel.class, rawKey, UNKNOWN);
+ }
+
+ public boolean isEnabled() {
+ return this != NONE;
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ @Override
+ public String toString() {
+ return key;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/sample/SampleCodeOptions.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/sample/SampleCodeOptions.java
new file mode 100644
index 0000000..3a8b588
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/sample/SampleCodeOptions.java
@@ -0,0 +1,26 @@
+package io.github.blueprintplatform.codegen.domain.model.value.sample;
+
+public record SampleCodeOptions(SampleCodeLevel level) {
+
+ public SampleCodeOptions {
+ if (level == null) {
+ level = SampleCodeLevel.NONE;
+ }
+ }
+
+ public static SampleCodeOptions none() {
+ return new SampleCodeOptions(SampleCodeLevel.NONE);
+ }
+
+ public static SampleCodeOptions basic() {
+ return new SampleCodeOptions(SampleCodeLevel.BASIC);
+ }
+
+ public static SampleCodeOptions rich() {
+ return new SampleCodeOptions(SampleCodeLevel.RICH);
+ }
+
+ public boolean isEnabled() {
+ return level.isEnabled();
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/JavaVersion.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/JavaVersion.java
new file mode 100644
index 0000000..5fc5c55
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/JavaVersion.java
@@ -0,0 +1,42 @@
+package io.github.blueprintplatform.codegen.domain.model.value.tech.platform;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+import io.github.blueprintplatform.codegen.domain.shared.KeyEnumParser;
+import io.github.blueprintplatform.codegen.domain.shared.KeyedEnum;
+
+public enum JavaVersion implements KeyedEnum {
+ JAVA_21("21", 21),
+ JAVA_25("25", 25);
+
+ private static final ErrorCode UNKNOWN = () -> "platform.java-version.unknown";
+
+ private final String key;
+ private final int major;
+
+ JavaVersion(String key, int major) {
+ this.key = key;
+ this.major = major;
+ }
+
+ public static JavaVersion fromKey(String raw) {
+ return KeyEnumParser.parse(JavaVersion.class, raw, UNKNOWN);
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ public int major() {
+ return major;
+ }
+
+ public String asString() {
+ return key;
+ }
+
+ @Override
+ public String toString() {
+ return key;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/PlatformTarget.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/PlatformTarget.java
new file mode 100644
index 0000000..6a81938
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/PlatformTarget.java
@@ -0,0 +1,3 @@
+package io.github.blueprintplatform.codegen.domain.model.value.tech.platform;
+
+public sealed interface PlatformTarget permits SpringBootJvmTarget {}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/SpringBootJvmTarget.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/SpringBootJvmTarget.java
new file mode 100644
index 0000000..8f8c476
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/SpringBootJvmTarget.java
@@ -0,0 +1,16 @@
+package io.github.blueprintplatform.codegen.domain.model.value.tech.platform;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+
+public record SpringBootJvmTarget(JavaVersion java, SpringBootVersion springBoot)
+ implements PlatformTarget {
+
+ private static final ErrorCode TARGET_REQUIRED = () -> "platform.target.not.blank";
+
+ public SpringBootJvmTarget {
+ if (java == null || springBoot == null) {
+ throw new DomainViolationException(TARGET_REQUIRED);
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/SpringBootVersion.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/SpringBootVersion.java
new file mode 100644
index 0000000..d6356a4
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/platform/SpringBootVersion.java
@@ -0,0 +1,44 @@
+package io.github.blueprintplatform.codegen.domain.model.value.tech.platform;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+import io.github.blueprintplatform.codegen.domain.shared.KeyEnumParser;
+import io.github.blueprintplatform.codegen.domain.shared.KeyedEnum;
+
+public enum SpringBootVersion implements KeyedEnum {
+ V3_5("3.5", "3.5.8"), // Latest known stable patch for 3.5.x
+ V3_4("3.4", "3.4.12"); // Latest known stable patch for 3.4.x
+
+ private static final ErrorCode UNKNOWN = () -> "platform.springboot-version.unknown";
+
+ private final String key; // major.minor
+ private final String defaultPatch; // full version, e.g. 3.5.8
+
+ SpringBootVersion(String key, String defaultPatch) {
+ this.key = key;
+ this.defaultPatch = defaultPatch;
+ }
+
+ public static SpringBootVersion fromKey(String raw) {
+ return KeyEnumParser.parse(SpringBootVersion.class, raw, UNKNOWN);
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ /** Major.minor representation, e.g. 3.5 */
+ public String majorMinor() {
+ return key;
+ }
+
+ /** Full default version, e.g. 3.5.8 */
+ public String defaultVersion() {
+ return defaultPatch;
+ }
+
+ @Override
+ public String toString() {
+ return key;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/BuildTool.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/BuildTool.java
new file mode 100644
index 0000000..329f377
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/BuildTool.java
@@ -0,0 +1,31 @@
+package io.github.blueprintplatform.codegen.domain.model.value.tech.stack;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+import io.github.blueprintplatform.codegen.domain.shared.KeyEnumParser;
+import io.github.blueprintplatform.codegen.domain.shared.KeyedEnum;
+
+public enum BuildTool implements KeyedEnum {
+ MAVEN("maven");
+
+ private static final ErrorCode UNKNOWN = () -> "project.tech-stack.build-tool.unknown";
+
+ private final String key;
+
+ BuildTool(String key) {
+ this.key = key;
+ }
+
+ public static BuildTool fromKey(String raw) {
+ return KeyEnumParser.parse(BuildTool.class, raw, UNKNOWN);
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ @Override
+ public String toString() {
+ return key;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/Framework.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/Framework.java
new file mode 100644
index 0000000..292be4c
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/Framework.java
@@ -0,0 +1,31 @@
+package io.github.blueprintplatform.codegen.domain.model.value.tech.stack;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+import io.github.blueprintplatform.codegen.domain.shared.KeyEnumParser;
+import io.github.blueprintplatform.codegen.domain.shared.KeyedEnum;
+
+public enum Framework implements KeyedEnum {
+ SPRING_BOOT("spring-boot");
+
+ private static final ErrorCode UNKNOWN = () -> "project.tech-stack.framework.unknown";
+
+ private final String key;
+
+ Framework(String key) {
+ this.key = key;
+ }
+
+ public static Framework fromKey(String raw) {
+ return KeyEnumParser.parse(Framework.class, raw, UNKNOWN);
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ @Override
+ public String toString() {
+ return key;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/Language.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/Language.java
new file mode 100644
index 0000000..87e3caf
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/Language.java
@@ -0,0 +1,31 @@
+package io.github.blueprintplatform.codegen.domain.model.value.tech.stack;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+import io.github.blueprintplatform.codegen.domain.shared.KeyEnumParser;
+import io.github.blueprintplatform.codegen.domain.shared.KeyedEnum;
+
+public enum Language implements KeyedEnum {
+ JAVA("java");
+
+ private static final ErrorCode UNKNOWN = () -> "project.tech-stack.language.unknown";
+
+ private final String key;
+
+ Language(String key) {
+ this.key = key;
+ }
+
+ public static Language fromKey(String raw) {
+ return KeyEnumParser.parse(Language.class, raw, UNKNOWN);
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ @Override
+ public String toString() {
+ return key;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/TechStack.java b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/TechStack.java
new file mode 100644
index 0000000..d4692c6
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/model/value/tech/stack/TechStack.java
@@ -0,0 +1,9 @@
+package io.github.blueprintplatform.codegen.domain.model.value.tech.stack;
+
+import io.github.blueprintplatform.codegen.domain.policy.tech.TechStackPolicy;
+
+public record TechStack(Framework framework, BuildTool buildTool, Language language) {
+ public TechStack {
+ TechStackPolicy.requireNonNull(framework, buildTool, language);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/dependency/DependenciesPolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/dependency/DependenciesPolicy.java
new file mode 100644
index 0000000..c456c18
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/dependency/DependenciesPolicy.java
@@ -0,0 +1,51 @@
+package io.github.blueprintplatform.codegen.domain.policy.dependency;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency;
+import java.util.*;
+
+public final class DependenciesPolicy {
+
+ private static final ErrorCode LIST_REQUIRED = () -> "dependency.list.not.blank";
+ private static final ErrorCode ITEM_REQUIRED = () -> "dependency.item.not.blank";
+ private static final ErrorCode DUPLICATE_COORDS = () -> "dependency.duplicate.coordinates";
+
+ private DependenciesPolicy() {}
+
+ public static List enforce(List raw) {
+ if (raw == null) {
+ throw new DomainViolationException(LIST_REQUIRED);
+ }
+ if (raw.isEmpty()) {
+ return List.of();
+ }
+
+ Map byCoords = getDependencyMap(raw);
+
+ List list = new ArrayList<>(byCoords.values());
+ list.sort(
+ Comparator.comparing(
+ dep ->
+ dep.coordinates().groupId().value()
+ + ":"
+ + dep.coordinates().artifactId().value()));
+ return List.copyOf(list);
+ }
+
+ private static Map getDependencyMap(List raw) {
+ Map byCoords = new LinkedHashMap<>();
+ for (Dependency d : raw) {
+ if (d == null) {
+ throw new DomainViolationException(ITEM_REQUIRED);
+ }
+ var coords = d.coordinates();
+ var key = coords.groupId().value() + ":" + coords.artifactId().value();
+ if (byCoords.containsKey(key)) {
+ throw new DomainViolationException(DUPLICATE_COORDS, key);
+ }
+ byCoords.put(key, d);
+ }
+ return byCoords;
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/dependency/DependencyVersionPolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/dependency/DependencyVersionPolicy.java
new file mode 100644
index 0000000..e5e2ab3
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/dependency/DependencyVersionPolicy.java
@@ -0,0 +1,44 @@
+package io.github.blueprintplatform.codegen.domain.policy.dependency;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+import static io.github.blueprintplatform.codegen.domain.error.code.Field.DEPENDENCY_VERSION;
+import static io.github.blueprintplatform.codegen.domain.error.code.Violation.INVALID_CHARS;
+import static io.github.blueprintplatform.codegen.domain.error.code.Violation.NOT_BLANK;
+
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.LengthBetweenRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.RegexMatchRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.CompositeRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+import java.util.regex.Pattern;
+
+public final class DependencyVersionPolicy {
+
+ private static final Pattern ALLOWED = Pattern.compile("^[A-Za-z0-9._\\-+\\[\\](),:{}$\\s]+$");
+
+ private static final int MIN = 1;
+ private static final int MAX = 100;
+
+ private DependencyVersionPolicy() {}
+
+ public static String enforce(String raw) {
+ String n = normalize(raw);
+ validate(n);
+ return n;
+ }
+
+ private static String normalize(String raw) {
+ if (raw == null || raw.isBlank()) {
+ throw new DomainViolationException(compose(DEPENDENCY_VERSION, NOT_BLANK));
+ }
+ return raw.trim();
+ }
+
+ private static void validate(String value) {
+ Rule rule =
+ CompositeRule.of(
+ new LengthBetweenRule(MIN, MAX, DEPENDENCY_VERSION),
+ new RegexMatchRule(ALLOWED, DEPENDENCY_VERSION, INVALID_CHARS));
+ rule.check(value);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/file/GeneratedFilePolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/file/GeneratedFilePolicy.java
new file mode 100644
index 0000000..5f11ff9
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/file/GeneratedFilePolicy.java
@@ -0,0 +1,31 @@
+package io.github.blueprintplatform.codegen.domain.policy.file;
+
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+
+public final class GeneratedFilePolicy {
+ private GeneratedFilePolicy() {}
+
+ public static void requireRelativePath(Path path) {
+ if (path == null) throw new DomainViolationException(() -> "file.path.not.blank");
+ if (path.isAbsolute())
+ throw new DomainViolationException(() -> "file.path.absolute.not.allowed");
+ if (path.getNameCount() == 0) throw new DomainViolationException(() -> "file.path.not.blank");
+ for (Path part : path) {
+ String s = part.toString();
+ if (s.isEmpty() || ".".equals(s) || "..".equals(s)) {
+ throw new DomainViolationException(() -> "file.path.traversal.not.allowed");
+ }
+ }
+ }
+
+ public static void requireTextContent(CharSequence content, Charset charset) {
+ if (content == null) throw new DomainViolationException(() -> "file.content.not.blank");
+ if (charset == null) throw new DomainViolationException(() -> "file.charset.not.blank");
+ }
+
+ public static void requireBinaryContent(byte[] bytes) {
+ if (bytes == null) throw new DomainViolationException(() -> "file.content.not.blank");
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/identity/ArtifactIdPolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/identity/ArtifactIdPolicy.java
new file mode 100644
index 0000000..211e9da
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/identity/ArtifactIdPolicy.java
@@ -0,0 +1,49 @@
+package io.github.blueprintplatform.codegen.domain.policy.identity;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+import static io.github.blueprintplatform.codegen.domain.error.code.Field.ARTIFACT_ID;
+import static io.github.blueprintplatform.codegen.domain.error.code.Violation.*;
+
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.AllowedCharsRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.LengthBetweenRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.NoEdgeCharRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.NotBlankRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.StartsWithLetterRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.CompositeRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+import java.util.Locale;
+
+public final class ArtifactIdPolicy {
+
+ private static final int MIN = 3;
+ private static final int MAX = 50;
+
+ private ArtifactIdPolicy() {}
+
+ public static String enforce(String raw) {
+ String n = normalize(raw);
+ validate(n);
+ return n;
+ }
+
+ private static String normalize(String raw) {
+ if (raw == null) throw new DomainViolationException(compose(ARTIFACT_ID, NOT_BLANK));
+ return raw.trim()
+ .replaceAll("\\s+", "-")
+ .replace('_', '-')
+ .toLowerCase(Locale.ROOT)
+ .replaceAll("-{2,}", "-");
+ }
+
+ private static void validate(String value) {
+ Rule rule =
+ CompositeRule.of(
+ new NotBlankRule(ARTIFACT_ID),
+ new LengthBetweenRule(MIN, MAX, ARTIFACT_ID),
+ new AllowedCharsRule("[a-z0-9-]", ARTIFACT_ID, INVALID_CHARS),
+ new StartsWithLetterRule(ARTIFACT_ID, STARTS_WITH_LETTER),
+ new NoEdgeCharRule('-', ARTIFACT_ID, EDGE_CHAR));
+ rule.check(value);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/identity/GroupIdPolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/identity/GroupIdPolicy.java
new file mode 100644
index 0000000..a3f93a4
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/identity/GroupIdPolicy.java
@@ -0,0 +1,44 @@
+package io.github.blueprintplatform.codegen.domain.policy.identity;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+import static io.github.blueprintplatform.codegen.domain.error.code.Field.GROUP_ID;
+import static io.github.blueprintplatform.codegen.domain.error.code.Violation.*;
+
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.DotSeparatedSegmentsRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.LengthBetweenRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.NotBlankRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.CompositeRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+public final class GroupIdPolicy {
+
+ private static final int MIN = 3;
+ private static final int MAX = 100;
+
+ private static final Pattern SEGMENT = Pattern.compile("^[a-z][a-z0-9]*$");
+
+ private GroupIdPolicy() {}
+
+ public static String enforce(String raw) {
+ String n = normalize(raw);
+ validate(n);
+ return n;
+ }
+
+ private static String normalize(String raw) {
+ if (raw == null) throw new DomainViolationException(compose(GROUP_ID, NOT_BLANK));
+ return raw.trim().replaceAll("\\s+", "").toLowerCase(Locale.ROOT);
+ }
+
+ private static void validate(String value) {
+ Rule rule =
+ CompositeRule.of(
+ new NotBlankRule(GROUP_ID),
+ new LengthBetweenRule(MIN, MAX, GROUP_ID),
+ new DotSeparatedSegmentsRule(SEGMENT, GROUP_ID, SEGMENT_FORMAT));
+ rule.check(value);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/naming/ProjectDescriptionPolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/naming/ProjectDescriptionPolicy.java
new file mode 100644
index 0000000..d428e57
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/naming/ProjectDescriptionPolicy.java
@@ -0,0 +1,47 @@
+package io.github.blueprintplatform.codegen.domain.policy.naming;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+import static io.github.blueprintplatform.codegen.domain.error.code.Field.PROJECT_DESCRIPTION;
+import static io.github.blueprintplatform.codegen.domain.error.code.Violation.CONTROL_CHARS;
+import static io.github.blueprintplatform.codegen.domain.error.code.Violation.NOT_BLANK;
+
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.LengthBetweenRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.NotBlankRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.RegexMatchRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.CompositeRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+import java.util.regex.Pattern;
+
+public final class ProjectDescriptionPolicy {
+
+ private static final int MIN = 10;
+ private static final int MAX = 280;
+
+ private static final Pattern NO_CONTROL_CHARS = Pattern.compile("^\\P{Cntrl}*$");
+
+ private ProjectDescriptionPolicy() {}
+
+ public static String enforce(String raw) {
+ if (raw == null) {
+ throw new DomainViolationException(compose(PROJECT_DESCRIPTION, NOT_BLANK));
+ }
+
+ String n = normalize(raw);
+ validate(n);
+ return n;
+ }
+
+ private static String normalize(String raw) {
+ return raw.trim().replaceAll("\\s+", " ");
+ }
+
+ private static void validate(String value) {
+ Rule rule =
+ CompositeRule.of(
+ new NotBlankRule(PROJECT_DESCRIPTION),
+ new LengthBetweenRule(MIN, MAX, PROJECT_DESCRIPTION),
+ new RegexMatchRule(NO_CONTROL_CHARS, PROJECT_DESCRIPTION, CONTROL_CHARS));
+ rule.check(value);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/naming/ProjectNamePolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/naming/ProjectNamePolicy.java
new file mode 100644
index 0000000..b43b21d
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/naming/ProjectNamePolicy.java
@@ -0,0 +1,46 @@
+package io.github.blueprintplatform.codegen.domain.policy.naming;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+import static io.github.blueprintplatform.codegen.domain.error.code.Field.PROJECT_NAME;
+import static io.github.blueprintplatform.codegen.domain.error.code.Violation.INVALID_CHARS;
+import static io.github.blueprintplatform.codegen.domain.error.code.Violation.NOT_BLANK;
+
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.AllowedCharsRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.LengthBetweenRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.NotBlankRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.CompositeRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+
+public final class ProjectNamePolicy {
+
+ private static final int MIN = 3;
+ private static final int MAX = 60;
+
+ private static final String ALLOWED_CHARS = "[A-Za-z0-9 .,_'()\\-]";
+
+ private ProjectNamePolicy() {}
+
+ public static String enforce(String raw) {
+ String n = normalize(raw);
+ validate(n);
+ return n;
+ }
+
+ private static String normalize(String raw) {
+ if (raw == null) {
+ throw new DomainViolationException(compose(PROJECT_NAME, NOT_BLANK));
+ }
+ return raw.trim();
+ }
+
+ private static void validate(String value) {
+ Rule rule =
+ CompositeRule.of(
+ new NotBlankRule(PROJECT_NAME),
+ new LengthBetweenRule(MIN, MAX, PROJECT_NAME),
+ new AllowedCharsRule(ALLOWED_CHARS, PROJECT_NAME, INVALID_CHARS));
+
+ rule.check(value);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/pkg/PackageNamePolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/pkg/PackageNamePolicy.java
new file mode 100644
index 0000000..f92d7b4
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/pkg/PackageNamePolicy.java
@@ -0,0 +1,61 @@
+package io.github.blueprintplatform.codegen.domain.policy.pkg;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+import static io.github.blueprintplatform.codegen.domain.error.code.Field.PACKAGE_NAME;
+import static io.github.blueprintplatform.codegen.domain.error.code.Violation.*;
+
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.DotSeparatedSegmentsRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.LengthBetweenRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.NotBlankRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.ReservedPrefixRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.CompositeRule;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+import java.util.Locale;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+public final class PackageNamePolicy {
+
+ private static final int MIN = 3;
+ private static final int MAX = 255;
+
+ private static final Pattern SEGMENT = Pattern.compile("^[a-z][a-z0-9]*$");
+
+ private static final Pattern SEP_CHARS = Pattern.compile("[\\s_\\-]+");
+ private static final Pattern MULTI_DOTS = Pattern.compile("\\.{2,}");
+ private static final Pattern LEADING_DOTS = Pattern.compile("^\\.+");
+ private static final Pattern TRAILING_DOTS = Pattern.compile("\\.+$");
+
+ private static final Set RESERVED_PREFIXES = Set.of("java", "javax", "sun", "com.sun");
+
+ private PackageNamePolicy() {}
+
+ public static String enforce(String raw) {
+ String n = normalize(raw);
+ validate(n);
+ return n;
+ }
+
+ private static String normalize(String raw) {
+ if (raw == null) throw new DomainViolationException(compose(PACKAGE_NAME, NOT_BLANK));
+
+ String s = raw.trim();
+ s = SEP_CHARS.matcher(s).replaceAll(".");
+ s = MULTI_DOTS.matcher(s).replaceAll(".");
+ s = LEADING_DOTS.matcher(s).replaceAll("");
+ s = TRAILING_DOTS.matcher(s).replaceAll("");
+ s = s.toLowerCase(Locale.ROOT);
+ return s;
+ }
+
+ private static void validate(String value) {
+ Rule rule =
+ CompositeRule.of(
+ new NotBlankRule(PACKAGE_NAME),
+ new LengthBetweenRule(MIN, MAX, PACKAGE_NAME),
+ new DotSeparatedSegmentsRule(SEGMENT, PACKAGE_NAME, SEGMENT_FORMAT),
+ new ReservedPrefixRule(RESERVED_PREFIXES, PACKAGE_NAME, RESERVED_PREFIX));
+ rule.check(value);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/AllowedCharsRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/AllowedCharsRule.java
new file mode 100644
index 0000000..98f3244
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/AllowedCharsRule.java
@@ -0,0 +1,30 @@
+package io.github.blueprintplatform.codegen.domain.policy.rule;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+
+import io.github.blueprintplatform.codegen.domain.error.code.Field;
+import io.github.blueprintplatform.codegen.domain.error.code.Violation;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+public final class AllowedCharsRule implements Rule {
+ private final Pattern allowed;
+ private final Field field;
+ private final Violation violation;
+
+ public AllowedCharsRule(String allowedCharClassRegex, Field field, Violation violation) {
+ Objects.requireNonNull(allowedCharClassRegex);
+ this.allowed = Pattern.compile("^(?:" + allowedCharClassRegex + ")+$");
+ this.field = field;
+ this.violation = violation;
+ }
+
+ @Override
+ public void check(String value) {
+ if (value == null || !allowed.matcher(value).matches()) {
+ throw new DomainViolationException(compose(field, violation));
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/DotSeparatedSegmentsRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/DotSeparatedSegmentsRule.java
new file mode 100644
index 0000000..5d46a47
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/DotSeparatedSegmentsRule.java
@@ -0,0 +1,37 @@
+package io.github.blueprintplatform.codegen.domain.policy.rule;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+
+import io.github.blueprintplatform.codegen.domain.error.code.Field;
+import io.github.blueprintplatform.codegen.domain.error.code.Violation;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+import java.util.regex.Pattern;
+
+public final class DotSeparatedSegmentsRule implements Rule {
+ private final Pattern segmentPattern;
+ private final Field field;
+ private final Violation violation;
+
+ public DotSeparatedSegmentsRule(Pattern segmentPattern, Field field, Violation violation) {
+ this.segmentPattern = segmentPattern;
+ this.field = field;
+ this.violation = violation;
+ }
+
+ @Override
+ public void check(String value) {
+ if (value == null) {
+ throw new DomainViolationException(compose(field, violation));
+ }
+ String[] parts = value.split("\\.", -1);
+ if (parts.length == 0) {
+ throw new DomainViolationException(compose(field, violation));
+ }
+ for (String p : parts) {
+ if (p.isEmpty() || !segmentPattern.matcher(p).matches()) {
+ throw new DomainViolationException(compose(field, violation), p);
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/LengthBetweenRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/LengthBetweenRule.java
new file mode 100644
index 0000000..55d3aee
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/LengthBetweenRule.java
@@ -0,0 +1,26 @@
+package io.github.blueprintplatform.codegen.domain.policy.rule;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+import static io.github.blueprintplatform.codegen.domain.error.code.Violation.LENGTH;
+
+import io.github.blueprintplatform.codegen.domain.error.code.Field;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+
+public final class LengthBetweenRule implements Rule {
+ private final int min;
+ private final int max;
+ private final Field field;
+
+ public LengthBetweenRule(int min, int max, Field field) {
+ this.min = min;
+ this.max = max;
+ this.field = field;
+ }
+
+ @Override
+ public void check(String v) {
+ if (v == null || v.length() < min || v.length() > max)
+ throw new DomainViolationException(compose(field, LENGTH), min, max);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NoConsecutiveCharRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NoConsecutiveCharRule.java
new file mode 100644
index 0000000..ac17b80
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NoConsecutiveCharRule.java
@@ -0,0 +1,32 @@
+package io.github.blueprintplatform.codegen.domain.policy.rule;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+
+import io.github.blueprintplatform.codegen.domain.error.code.Field;
+import io.github.blueprintplatform.codegen.domain.error.code.Violation;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+
+public final class NoConsecutiveCharRule implements Rule {
+ private final char ch;
+ private final Field field;
+ private final Violation violation;
+
+ public NoConsecutiveCharRule(char ch, Field field, Violation violation) {
+ this.ch = ch;
+ this.field = field;
+ this.violation = violation;
+ }
+
+ @Override
+ public void check(String value) {
+ if (value == null) {
+ throw new DomainViolationException(compose(field, violation));
+ }
+ for (int i = 1; i < value.length(); i++) {
+ if (value.charAt(i) == ch && value.charAt(i - 1) == ch) {
+ throw new DomainViolationException(compose(field, violation));
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NoEdgeCharRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NoEdgeCharRule.java
new file mode 100644
index 0000000..d6b8740
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NoEdgeCharRule.java
@@ -0,0 +1,30 @@
+package io.github.blueprintplatform.codegen.domain.policy.rule;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+
+import io.github.blueprintplatform.codegen.domain.error.code.Field;
+import io.github.blueprintplatform.codegen.domain.error.code.Violation;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+
+public final class NoEdgeCharRule implements Rule {
+ private final char edgeChar;
+ private final Field field;
+ private final Violation violation;
+
+ public NoEdgeCharRule(char edgeChar, Field field, Violation violation) {
+ this.edgeChar = edgeChar;
+ this.field = field;
+ this.violation = violation;
+ }
+
+ @Override
+ public void check(String value) {
+ if (value == null || value.isEmpty()) {
+ throw new DomainViolationException(compose(field, violation));
+ }
+ if (value.charAt(0) == edgeChar || value.charAt(value.length() - 1) == edgeChar) {
+ throw new DomainViolationException(compose(field, violation));
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NotBlankRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NotBlankRule.java
new file mode 100644
index 0000000..f3e74c8
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/NotBlankRule.java
@@ -0,0 +1,22 @@
+package io.github.blueprintplatform.codegen.domain.policy.rule;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+import static io.github.blueprintplatform.codegen.domain.error.code.Violation.NOT_BLANK;
+
+import io.github.blueprintplatform.codegen.domain.error.code.Field;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+
+public final class NotBlankRule implements Rule {
+ private final Field field;
+
+ public NotBlankRule(Field field) {
+ this.field = field;
+ }
+
+ @Override
+ public void check(String value) {
+ if (value == null || value.isBlank())
+ throw new DomainViolationException(compose(field, NOT_BLANK));
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/RegexMatchRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/RegexMatchRule.java
new file mode 100644
index 0000000..37ee043
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/RegexMatchRule.java
@@ -0,0 +1,28 @@
+package io.github.blueprintplatform.codegen.domain.policy.rule;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+
+import io.github.blueprintplatform.codegen.domain.error.code.Field;
+import io.github.blueprintplatform.codegen.domain.error.code.Violation;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+import java.util.regex.Pattern;
+
+public final class RegexMatchRule implements Rule {
+ private final Pattern pattern;
+ private final Field field;
+ private final Violation violation;
+
+ public RegexMatchRule(Pattern pattern, Field field, Violation violation) {
+ this.pattern = pattern;
+ this.field = field;
+ this.violation = violation;
+ }
+
+ @Override
+ public void check(String value) {
+ if (value == null || !pattern.matcher(value).matches()) {
+ throw new DomainViolationException(compose(field, violation));
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/ReservedNamesRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/ReservedNamesRule.java
new file mode 100644
index 0000000..cf4d2e0
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/ReservedNamesRule.java
@@ -0,0 +1,33 @@
+package io.github.blueprintplatform.codegen.domain.policy.rule;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+
+import io.github.blueprintplatform.codegen.domain.error.code.Field;
+import io.github.blueprintplatform.codegen.domain.error.code.Violation;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+import java.util.Locale;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public final class ReservedNamesRule implements Rule {
+ private final Set reservedLower;
+ private final Field field;
+
+ public ReservedNamesRule(Set reserved, Field field) {
+ this.reservedLower =
+ reserved.stream()
+ .map(s -> s.toLowerCase(Locale.ROOT))
+ .collect(Collectors.toUnmodifiableSet());
+ this.field = field;
+ }
+
+ @Override
+ public void check(String value) {
+ if (value == null) throw new DomainViolationException(compose(field, Violation.RESERVED));
+ String lower = value.toLowerCase(Locale.ROOT);
+ if (reservedLower.contains(lower)) {
+ throw new DomainViolationException(compose(field, Violation.RESERVED), value);
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/ReservedPrefixRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/ReservedPrefixRule.java
new file mode 100644
index 0000000..640ae60
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/ReservedPrefixRule.java
@@ -0,0 +1,36 @@
+package io.github.blueprintplatform.codegen.domain.policy.rule;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+
+import io.github.blueprintplatform.codegen.domain.error.code.Field;
+import io.github.blueprintplatform.codegen.domain.error.code.Violation;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+import java.util.Locale;
+import java.util.Set;
+
+public final class ReservedPrefixRule implements Rule {
+ private final Set reservedPrefixesLower;
+ private final Field field;
+ private final Violation violation;
+
+ public ReservedPrefixRule(Set reservedPrefixes, Field field, Violation violation) {
+ this.reservedPrefixesLower =
+ reservedPrefixes.stream()
+ .map(s -> s.toLowerCase(Locale.ROOT))
+ .collect(java.util.stream.Collectors.toUnmodifiableSet());
+ this.field = field;
+ this.violation = violation;
+ }
+
+ @Override
+ public void check(String value) {
+ if (value == null) throw new DomainViolationException(compose(field, violation));
+ String lower = value.toLowerCase(Locale.ROOT);
+ for (String p : reservedPrefixesLower) {
+ if (lower.equals(p) || lower.startsWith(p + ".")) {
+ throw new DomainViolationException(compose(field, violation), p);
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/StartsWithLetterRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/StartsWithLetterRule.java
new file mode 100644
index 0000000..958a99e
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/StartsWithLetterRule.java
@@ -0,0 +1,29 @@
+package io.github.blueprintplatform.codegen.domain.policy.rule;
+
+import static io.github.blueprintplatform.codegen.domain.error.code.ErrorKeys.compose;
+
+import io.github.blueprintplatform.codegen.domain.error.code.Field;
+import io.github.blueprintplatform.codegen.domain.error.code.Violation;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.policy.rule.base.Rule;
+
+public final class StartsWithLetterRule implements Rule {
+ private final Field field;
+ private final Violation violation;
+
+ public StartsWithLetterRule(Field field, Violation violation) {
+ this.field = field;
+ this.violation = violation;
+ }
+
+ @Override
+ public void check(String value) {
+ if (value == null || value.isEmpty()) {
+ throw new DomainViolationException(compose(field, violation));
+ }
+ char c = value.charAt(0);
+ if (c < 'a' || c > 'z') {
+ throw new DomainViolationException(compose(field, violation));
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/base/CompositeRule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/base/CompositeRule.java
new file mode 100644
index 0000000..aa0f97c
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/base/CompositeRule.java
@@ -0,0 +1,31 @@
+package io.github.blueprintplatform.codegen.domain.policy.rule.base;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public final class CompositeRule implements Rule {
+
+ private final List> rules;
+
+ private CompositeRule(List> rules) {
+ this.rules = List.copyOf(rules);
+ }
+
+ @SafeVarargs
+ public static CompositeRule of(Rule super T>... rules) {
+ List> list = Arrays.asList(rules);
+ return new CompositeRule<>(list);
+ }
+
+ public static CompositeRule of(List extends Rule super T>> rules) {
+ return new CompositeRule<>(new ArrayList<>(rules));
+ }
+
+ @Override
+ public void check(T value) {
+ for (Rule super T> r : rules) {
+ r.check(value);
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/base/Rule.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/base/Rule.java
new file mode 100644
index 0000000..7e7cc2e
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/rule/base/Rule.java
@@ -0,0 +1,6 @@
+package io.github.blueprintplatform.codegen.domain.policy.rule.base;
+
+@FunctionalInterface
+public interface Rule {
+ void check(T value);
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/CompatibilityPolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/CompatibilityPolicy.java
new file mode 100644
index 0000000..f9d1e3b
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/CompatibilityPolicy.java
@@ -0,0 +1,69 @@
+package io.github.blueprintplatform.codegen.domain.policy.tech;
+
+import static io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion.JAVA_21;
+import static io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion.JAVA_25;
+import static io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion.V3_4;
+import static io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion.V3_5;
+import static java.util.Map.entry;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public final class CompatibilityPolicy {
+
+ private static final ErrorCode TARGET_MISSING = () -> "platform.target.missing";
+ private static final ErrorCode OPTIONS_UNSUPPORTED = () -> "platform.target.unsupported.options";
+ private static final ErrorCode TARGET_INCOMPATIBLE = () -> "platform.target.incompatible";
+
+ private static final Map> SPRINGBOOT_JAVA_SUPPORT =
+ Map.ofEntries(entry(V3_5, EnumSet.of(JAVA_21, JAVA_25)), entry(V3_4, EnumSet.of(JAVA_21)));
+
+ private CompatibilityPolicy() {}
+
+ public static void ensureCompatible(TechStack techStack, PlatformTarget target) {
+ if (techStack == null || target == null) {
+ throw new DomainViolationException(TARGET_MISSING);
+ }
+
+ if (techStack.framework() != Framework.SPRING_BOOT
+ || techStack.language() != Language.JAVA
+ || techStack.buildTool() != BuildTool.MAVEN) {
+ throw new DomainViolationException(
+ OPTIONS_UNSUPPORTED, techStack.framework(), techStack.language(), techStack.buildTool());
+ }
+
+ if (!(target instanceof SpringBootJvmTarget(JavaVersion java, SpringBootVersion springBoot))) {
+ throw new DomainViolationException(
+ TARGET_INCOMPATIBLE, "SPRING_BOOT", target.getClass().getSimpleName());
+ }
+
+ var allowed = SPRINGBOOT_JAVA_SUPPORT.getOrDefault(springBoot, Set.of());
+ if (!allowed.contains(java)) {
+ throw new DomainViolationException(
+ TARGET_INCOMPATIBLE, springBoot.defaultVersion(), java.asString());
+ }
+ }
+
+ public static List allSupportedTargets() {
+ List list = new ArrayList<>();
+ for (var e : SPRINGBOOT_JAVA_SUPPORT.entrySet()) {
+ for (var j : e.getValue()) {
+ list.add(new SpringBootJvmTarget(j, e.getKey()));
+ }
+ }
+ return List.copyOf(list);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/PlatformTargetSelector.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/PlatformTargetSelector.java
new file mode 100644
index 0000000..3cc0660
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/PlatformTargetSelector.java
@@ -0,0 +1,25 @@
+package io.github.blueprintplatform.codegen.domain.policy.tech;
+
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+import java.util.List;
+
+public final class PlatformTargetSelector {
+
+ private PlatformTargetSelector() {}
+
+ public static PlatformTarget select(
+ TechStack techStack, JavaVersion preferredJava, SpringBootVersion preferredBoot) {
+
+ var requested = new SpringBootJvmTarget(preferredJava, preferredBoot);
+ CompatibilityPolicy.ensureCompatible(techStack, requested);
+ return requested;
+ }
+
+ public static List supportedTargetsFor() {
+ return CompatibilityPolicy.allSupportedTargets();
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/TechStackPolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/TechStackPolicy.java
new file mode 100644
index 0000000..8d482a4
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/policy/tech/TechStackPolicy.java
@@ -0,0 +1,28 @@
+package io.github.blueprintplatform.codegen.domain.policy.tech;
+
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+
+public final class TechStackPolicy {
+
+ private TechStackPolicy() {}
+
+ public static TechStack enforce(TechStack techStack) {
+ if (techStack == null
+ || techStack.framework() == null
+ || techStack.buildTool() == null
+ || techStack.language() == null) {
+ throw new DomainViolationException(() -> "project.tech-stack.not.blank");
+ }
+ return techStack;
+ }
+
+ public static void requireNonNull(Framework framework, BuildTool buildTool, Language language) {
+ if (framework == null || buildTool == null || language == null) {
+ throw new DomainViolationException(() -> "project.tech-stack.not.blank");
+ }
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedBinaryResource.java b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedBinaryResource.java
new file mode 100644
index 0000000..5e5b7da
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedBinaryResource.java
@@ -0,0 +1,46 @@
+package io.github.blueprintplatform.codegen.domain.port.out.artifact;
+
+import static io.github.blueprintplatform.codegen.domain.policy.file.GeneratedFilePolicy.*;
+
+import java.nio.file.Path;
+import java.util.Arrays;
+
+public record GeneratedBinaryResource(Path relativePath, byte[] bytes)
+ implements GeneratedResource {
+
+ public GeneratedBinaryResource(Path relativePath, byte[] bytes) {
+ requireRelativePath(relativePath);
+ requireBinaryContent(bytes);
+ this.relativePath = relativePath;
+ this.bytes = Arrays.copyOf(bytes, bytes.length);
+ }
+
+ @Override
+ public byte[] bytes() {
+ return Arrays.copyOf(bytes, bytes.length);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof GeneratedBinaryResource(Path path, byte[] bytes1))) {
+ return false;
+ }
+ return relativePath.equals(path) && Arrays.equals(bytes, bytes1);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = relativePath.hashCode();
+ result = 31 * result + Arrays.hashCode(bytes);
+ return result;
+ }
+
+ @SuppressWarnings("NullableProblems")
+ @Override
+ public String toString() {
+ return "GeneratedBinaryFile[" + relativePath + ", size=" + bytes.length + "]";
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedDirectory.java b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedDirectory.java
new file mode 100644
index 0000000..22ed6db
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedDirectory.java
@@ -0,0 +1,18 @@
+package io.github.blueprintplatform.codegen.domain.port.out.artifact;
+
+import static io.github.blueprintplatform.codegen.domain.policy.file.GeneratedFilePolicy.*;
+
+import java.nio.file.Path;
+
+public record GeneratedDirectory(Path relativePath) implements GeneratedResource {
+
+ public GeneratedDirectory {
+ requireRelativePath(relativePath);
+ }
+
+ @SuppressWarnings("NullableProblems")
+ @Override
+ public String toString() {
+ return "GeneratedDirectory[" + relativePath + "]";
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedResource.java b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedResource.java
new file mode 100644
index 0000000..a7d83bc
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedResource.java
@@ -0,0 +1,9 @@
+package io.github.blueprintplatform.codegen.domain.port.out.artifact;
+
+import java.nio.file.Path;
+
+public sealed interface GeneratedResource
+ permits GeneratedTextResource, GeneratedBinaryResource, GeneratedDirectory {
+
+ Path relativePath();
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedTextResource.java b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedTextResource.java
new file mode 100644
index 0000000..da78fa2
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/artifact/GeneratedTextResource.java
@@ -0,0 +1,15 @@
+package io.github.blueprintplatform.codegen.domain.port.out.artifact;
+
+import static io.github.blueprintplatform.codegen.domain.policy.file.GeneratedFilePolicy.*;
+
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+
+public record GeneratedTextResource(Path relativePath, String content, Charset charset)
+ implements GeneratedResource {
+
+ public GeneratedTextResource {
+ requireRelativePath(relativePath);
+ requireTextContent(content, charset);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectRootExistencePolicy.java b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectRootExistencePolicy.java
new file mode 100644
index 0000000..b8ed61a
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectRootExistencePolicy.java
@@ -0,0 +1,6 @@
+package io.github.blueprintplatform.codegen.domain.port.out.filesystem;
+
+public enum ProjectRootExistencePolicy {
+ FAIL_IF_EXISTS,
+ OVERWRITE
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectRootPort.java b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectRootPort.java
new file mode 100644
index 0000000..3b3d13f
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectRootPort.java
@@ -0,0 +1,8 @@
+package io.github.blueprintplatform.codegen.domain.port.out.filesystem;
+
+import java.nio.file.Path;
+
+public interface ProjectRootPort {
+
+ Path prepareRoot(Path targetDir, String artifactId, ProjectRootExistencePolicy policy);
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectWriterPort.java b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectWriterPort.java
new file mode 100644
index 0000000..4f9445b
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/port/out/filesystem/ProjectWriterPort.java
@@ -0,0 +1,41 @@
+package io.github.blueprintplatform.codegen.domain.port.out.filesystem;
+
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedBinaryResource;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedDirectory;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+
+public interface ProjectWriterPort {
+
+ void writeBytes(Path projectRoot, Path relativePath, byte[] content);
+
+ void writeText(Path projectRoot, Path relativePath, String content, Charset charset);
+
+ void createDirectories(Path projectRoot, Path relativeDir);
+
+ default void writeText(Path root, Path relative, String content) {
+ writeText(root, relative, content, java.nio.charset.StandardCharsets.UTF_8);
+ }
+
+ default void write(Path projectRoot, GeneratedResource resource) {
+ switch (resource) {
+ case GeneratedTextResource(Path p, String c, Charset cs) -> writeText(projectRoot, p, c, cs);
+ case GeneratedBinaryResource(Path p, byte[] b) -> writeBytes(projectRoot, p, b);
+ case GeneratedDirectory(Path p) -> createDirectories(projectRoot, p);
+ }
+ }
+
+ default void write(Path projectRoot, Iterable extends GeneratedResource> resources) {
+ for (GeneratedResource f : resources) write(projectRoot, f);
+ }
+
+ default void write(Path projectRoot, GeneratedResource... files) {
+ for (GeneratedResource f : files) write(projectRoot, f);
+ }
+
+ default void write(Path projectRoot, java.util.stream.Stream extends GeneratedResource> files) {
+ files.forEach(f -> write(projectRoot, f));
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/shared/KeyEnumParser.java b/src/main/java/io/github/blueprintplatform/codegen/domain/shared/KeyEnumParser.java
new file mode 100644
index 0000000..d7c87d0
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/shared/KeyEnumParser.java
@@ -0,0 +1,27 @@
+package io.github.blueprintplatform.codegen.domain.shared;
+
+import io.github.blueprintplatform.codegen.domain.error.code.ErrorCode;
+import io.github.blueprintplatform.codegen.domain.error.exception.DomainViolationException;
+
+public final class KeyEnumParser {
+
+ private KeyEnumParser() {}
+
+ public static & KeyedEnum> E parse(
+ Class type, String raw, ErrorCode unknownCode) {
+
+ if (raw == null || raw.isBlank()) {
+ throw new DomainViolationException(unknownCode, raw);
+ }
+
+ String normalized = raw.trim().toLowerCase();
+
+ for (E e : type.getEnumConstants()) {
+ if (e.key().equalsIgnoreCase(normalized)) {
+ return e;
+ }
+ }
+
+ throw new DomainViolationException(unknownCode, normalized);
+ }
+}
diff --git a/src/main/java/io/github/blueprintplatform/codegen/domain/shared/KeyedEnum.java b/src/main/java/io/github/blueprintplatform/codegen/domain/shared/KeyedEnum.java
new file mode 100644
index 0000000..4083e46
--- /dev/null
+++ b/src/main/java/io/github/blueprintplatform/codegen/domain/shared/KeyedEnum.java
@@ -0,0 +1,5 @@
+package io.github.blueprintplatform.codegen.domain.shared;
+
+public interface KeyedEnum {
+ String key();
+}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/cli/CliRunner.java b/src/main/java/io/github/bsayli/codegen/initializr/cli/CliRunner.java
deleted file mode 100644
index a3241c2..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/cli/CliRunner.java
+++ /dev/null
@@ -1,162 +0,0 @@
-package io.github.bsayli.codegen.initializr.cli;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.Dependency;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.spring.SpringBootJavaProjectMetadata.SpringBootJavaProjectMetadataBuilder;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.BuildTool;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Framework;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Language;
-import io.github.bsayli.codegen.initializr.projectgeneration.service.ProjectGenerationService;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.file.*;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Stream;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.boot.ApplicationArguments;
-import org.springframework.boot.ApplicationRunner;
-import org.springframework.context.annotation.Profile;
-import org.springframework.stereotype.Component;
-
-@Component
-@Profile("cli")
-public class CliRunner implements ApplicationRunner {
-
- private static final Logger log = LoggerFactory.getLogger(CliRunner.class);
-
- private static final String ARG_GROUP_ID = "groupId";
- private static final String ARG_ARTIFACT_ID = "artifactId";
- private static final String ARG_NAME = "name";
- private static final String ARG_PACKAGE_NAME = "packageName";
- private static final String ARG_JAVA_VERSION = "javaVersion";
- private static final String ARG_BOOT_VERSION = "springBootVersion";
- private static final String ARG_OUTPUT_DIR = "outputDir";
- private static final String ARG_OVERWRITE = "overwrite";
-
- private static final String DEF_GROUP_ID = "com.example";
- private static final String DEF_ARTIFACT_ID = "demo-app";
- private static final String DEF_PACKAGE_NAME = "com.example.demo";
- private static final String DEF_JAVA_VERSION = "21";
- private static final String DEF_BOOT_VERSION = "3.5.5";
- private static final String DEF_DESCRIPTION = "Generated by codegen-initializr-core";
- private static final boolean DEF_OVERWRITE = false;
-
- private static final Path DEFAULT_OUTPUT_ROOT =
- Paths.get(System.getProperty("user.dir"), "target", "generated-projects");
-
- private final ProjectGenerationService service;
-
- public CliRunner(ProjectGenerationService service) {
- this.service = service;
- }
-
- @Override
- public void run(ApplicationArguments args) throws Exception {
- String groupId = argOrDefault(args, ARG_GROUP_ID, DEF_GROUP_ID);
- String artifact = argOrDefault(args, ARG_ARTIFACT_ID, DEF_ARTIFACT_ID);
- String name = argOrDefault(args, ARG_NAME, artifact);
- String pkg = argOrDefault(args, ARG_PACKAGE_NAME, DEF_PACKAGE_NAME);
- String javaVer = argOrDefault(args, ARG_JAVA_VERSION, DEF_JAVA_VERSION);
- String bootVer = argOrDefault(args, ARG_BOOT_VERSION, DEF_BOOT_VERSION);
- Path outputRoot = argPath(args, ARG_OUTPUT_DIR).orElse(DEFAULT_OUTPUT_ROOT);
- boolean overwrite = argBoolean(args, ARG_OVERWRITE, DEF_OVERWRITE);
-
- Path projectDir = outputRoot.resolve(artifact);
-
- if (Files.exists(projectDir)) {
- if (overwrite) {
- log.info(
- "♻️ Existing directory found. Deleting before regeneration: {}",
- projectDir.toAbsolutePath());
- deleteRecursively(projectDir);
- } else {
- throw new IllegalStateException(
- "Target directory already exists: "
- + projectDir.toAbsolutePath()
- + " (use --overwrite=true to replace it)");
- }
- }
-
- var depWeb =
- new Dependency.DependencyBuilder()
- .groupId("org.springframework.boot")
- .artifactId("spring-boot-starter-web")
- .build();
-
- var depTest =
- new Dependency.DependencyBuilder()
- .groupId("org.springframework.boot")
- .artifactId("spring-boot-starter-test")
- .scope("test")
- .build();
-
- var metadata =
- new SpringBootJavaProjectMetadataBuilder()
- .springBootVersion(bootVer)
- .javaVersion(javaVer)
- .groupId(groupId)
- .artifactId(artifact)
- .name(name)
- .description(DEF_DESCRIPTION)
- .packageName(pkg)
- .projectLocation(outputRoot)
- .dependencies(List.of(depWeb, depTest))
- .build();
-
- var type = new ProjectType(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA);
-
- Path zip = service.generateProject(type, metadata);
-
- log.info("✅ Project archive generated at: {}", zip.toAbsolutePath());
- if (outputRoot.equals(DEFAULT_OUTPUT_ROOT)) {
- log.info("ℹ️ Tip: Use --{}=/absolute/path to control the output location.", ARG_OUTPUT_DIR);
- }
- }
-
- private String argOrDefault(ApplicationArguments args, String name, String def) {
- var values = args.getOptionValues(name);
- return (values != null && !values.isEmpty()) ? values.getFirst() : def;
- }
-
- private Optional argPath(ApplicationArguments args, String name) {
- var values = args.getOptionValues(name);
- if (values == null || values.isEmpty()) return Optional.empty();
- String raw = values.getFirst();
- if (raw == null || raw.isBlank()) return Optional.empty();
- return Optional.of(Paths.get(raw));
- }
-
- private boolean argBoolean(ApplicationArguments args, String name, boolean def) {
- var values = args.getOptionValues(name);
- if (values == null || values.isEmpty()) return def;
- String raw = values.getFirst();
- if (raw == null) return def;
- return switch (raw.trim().toLowerCase()) {
- case "true", "1", "yes", "y" -> true;
- case "false", "0", "no", "n" -> false;
- default -> def;
- };
- }
-
- private void deleteRecursively(Path dir) {
- if (!Files.exists(dir)) return;
-
- try (Stream paths = Files.walk(dir)) {
- paths
- .sorted(Comparator.reverseOrder())
- .forEach(
- p -> {
- try {
- Files.deleteIfExists(p);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- });
- } catch (IOException e) {
- throw new UncheckedIOException("Failed to clean directory: " + dir.toAbsolutePath(), e);
- }
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/GitIgnoreFileGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/GitIgnoreFileGenerator.java
deleted file mode 100644
index 6a8d26d..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/GitIgnoreFileGenerator.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.adapters;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.templating.TemplateType;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.GitIgnoreGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine;
-import java.io.File;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.springframework.stereotype.Component;
-
-@Component("gitIgnoreFileGenerator")
-public class GitIgnoreFileGenerator implements GitIgnoreGenerator {
-
- private final TemplateEngine freeMarkerTemplateEngine;
-
- public GitIgnoreFileGenerator(TemplateEngine freeMarkerTemplateEngine) {
- this.freeMarkerTemplateEngine = freeMarkerTemplateEngine;
- }
-
- @Override
- public void generateGitIgnoreContent(File projectDestination, List ignoreList)
- throws IOException {
-
- Map gitIgnoreData = new HashMap<>();
- gitIgnoreData.put("ignoreList", ignoreList != null ? ignoreList : Collections.emptyList());
-
- freeMarkerTemplateEngine.generateFileFromTemplate(
- TemplateType.GITIGNORE, gitIgnoreData, projectDestination);
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ProjectRootDirectoryInitializer.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ProjectRootDirectoryInitializer.java
deleted file mode 100644
index 05c5c20..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ProjectRootDirectoryInitializer.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.adapters;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectDirectoryInitializer;
-import java.io.IOException;
-import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import org.springframework.stereotype.Component;
-
-@Component("projectRootDirectoryInitializer")
-public class ProjectRootDirectoryInitializer implements ProjectDirectoryInitializer {
-
- @Override
- public Path initializeProjectDirectory(String projectName) throws IOException {
- Path tempPath = Files.createTempDirectory(projectName);
- Path projectPath = Paths.get(tempPath.toString(), projectName);
- Files.createDirectories(projectPath);
- return projectPath;
- }
-
- @Override
- public Path initializeProjectDirectory(String projectName, Path projectLocation)
- throws IOException {
- if (projectLocation != null) {
- Path projectDir = projectLocation.resolve(projectName);
- if (Files.exists(projectDir)) {
- throw new FileAlreadyExistsException(projectDir.toString(), null, "File already exists!");
- }
- Files.createDirectories(projectDir); // Create directories recursively
- return projectDir;
- } else {
- return initializeProjectDirectory(projectName);
- }
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ZipProjectArchiver.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ZipProjectArchiver.java
deleted file mode 100644
index be4c8f5..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/ZipProjectArchiver.java
+++ /dev/null
@@ -1,100 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.adapters;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectArchiver;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Stream;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
-import org.springframework.stereotype.Component;
-
-@Component("zipProjectArchiver")
-public class ZipProjectArchiver implements ProjectArchiver {
-
- @Override
- public Path archiveProject(File projectDestination, String projectName) throws IOException {
- projectName = sanitizeFilename(projectName);
- String archiveFilename = projectName + ".zip";
- File archiveFile = new File(projectDestination.getParent(), archiveFilename);
-
- try (FileOutputStream fos = new FileOutputStream(archiveFile);
- ZipOutputStream zipOut = new ZipOutputStream(fos)) {
-
- zipOut.setLevel(ZipOutputStream.DEFLATED);
-
- Path projectPath = Paths.get(projectDestination.getAbsolutePath());
-
- addFilesToZip(projectPath, zipOut);
- }
-
- return archiveFile.toPath();
- }
-
- private void addFilesToZip(Path projectPath, ZipOutputStream zipOut) throws IOException {
- String rootFileName = sanitizeRootFileName(projectPath.getFileName().toString());
- List processingErrors = new ArrayList<>();
- try (Stream walkStream = Files.walk(projectPath)) {
- walkStream.forEach(
- filePath -> {
- String entryName = getEntryName(projectPath, filePath, rootFileName);
- try {
- addFileToZip(zipOut, filePath, entryName);
- } catch (IOException e) {
- processingErrors.add(
- String.format("Error processing file: %s. Reason: %s", filePath, e.getMessage()));
- }
- });
- }
-
- if (!processingErrors.isEmpty()) {
- throw new IOException(
- String.format(
- "Error encountered during archive creation for %s: %s",
- projectPath.getFileName(), String.join(", ", processingErrors)));
- }
- }
-
- private String getEntryName(Path projectPath, Path filePath, String rootFileName) {
- if (projectPath.equals(filePath)) {
- return rootFileName;
- } else {
- String entryName = sanitizeEntryName(projectPath.relativize(filePath).toString());
- return "/" + rootFileName + "/" + entryName;
- }
- }
-
- private void addFileToZip(ZipOutputStream zipOut, Path filePath, String entryName)
- throws IOException {
- if (Files.isDirectory(filePath)) {
- zipOut.putNextEntry(new ZipEntry(entryName + "/"));
- } else {
- zipOut.putNextEntry(new ZipEntry(entryName));
- try (FileInputStream fis = new FileInputStream(filePath.toFile())) {
- byte[] buffer = new byte[1024];
- int bytesRead;
- while ((bytesRead = fis.read(buffer)) != -1) {
- zipOut.write(buffer, 0, bytesRead);
- }
- }
- }
- }
-
- private String sanitizeFilename(String filename) {
- return filename.replaceAll("[/:*?<>|\\\\.^]", "").replaceAll("--+", "-");
- }
-
- private String sanitizeEntryName(String filename) {
- return filename.replaceAll("[:*?<>|\\\\^]", "").replaceAll("-+", "_");
- }
-
- private String sanitizeRootFileName(String filename) {
- return filename.replaceAll("[:*?<>|\\\\.^]", "").replaceAll("--+", "-");
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootApplicationYamlGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootApplicationYamlGenerator.java
deleted file mode 100644
index 0551f74..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootApplicationYamlGenerator.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.templating.TemplateType;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ApplicationYamlGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine;
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import org.springframework.stereotype.Component;
-
-@Component("springBootApplicationYamlGenerator")
-public class SpringBootApplicationYamlGenerator implements ApplicationYamlGenerator {
-
- private static final String SRC_MAIN_RESOURCES = "src/main/resources";
- private final TemplateEngine freeMarkerTemplateEngine;
-
- public SpringBootApplicationYamlGenerator(TemplateEngine freeMarkerTemplateEngine) {
- this.freeMarkerTemplateEngine = freeMarkerTemplateEngine;
- }
-
- @Override
- public void generateApplicationYaml(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException {
- Map appPropertiesModel = new HashMap<>();
- appPropertiesModel.put("projectName", projectMetadata.getName());
-
- File srcMainResourcesFile = new File(projectDestination, SRC_MAIN_RESOURCES);
-
- freeMarkerTemplateEngine.generateFileFromTemplate(
- TemplateType.SPRING_BOOT_APPLICATION_YAML, appPropertiesModel, srcMainResourcesFile);
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaMainClassGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaMainClassGenerator.java
deleted file mode 100644
index 4341ee0..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaMainClassGenerator.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot;
-
-import static io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot.constants.SpringBootJavaMainClassGeneratorConstants.FILE_NAME_EXTENSION;
-import static io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot.constants.SpringBootJavaMainClassGeneratorConstants.FILE_NAME_POSTFIX;
-import static io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot.constants.SpringBootJavaMainClassGeneratorConstants.TEMPLATE_NAME;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.configuration.properties.MavenJavaSourceFolderProperties;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import io.github.bsayli.codegen.initializr.projectgeneration.naming.NameConverter;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.FrameworkProjectStarterClassGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine;
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import org.springframework.stereotype.Component;
-
-@Component("springBootJavaMainClassGenerator")
-public class SpringBootJavaMainClassGenerator implements FrameworkProjectStarterClassGenerator {
-
- private final TemplateEngine freeMarkerTemplateEngine;
- private final MavenJavaSourceFolderProperties sourceFolder;
- private final NameConverter nameConverter;
-
- public SpringBootJavaMainClassGenerator(
- TemplateEngine freeMarkerTemplateEngine,
- MavenJavaSourceFolderProperties sourceFolder,
- NameConverter nameConverter) {
- this.sourceFolder = sourceFolder;
- this.freeMarkerTemplateEngine = freeMarkerTemplateEngine;
- this.nameConverter = nameConverter;
- }
-
- @Override
- public void generateProjectStarterClass(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException {
-
- Map mainClassModel = new HashMap<>();
- mainClassModel.put("projectPackageName", projectMetadata.getPackageName());
-
- // e.g. "codegen-demo" -> "CodegenDemo" + "Application"
- String classBase = nameConverter.toPascalCase(projectMetadata.getName());
- String className = classBase + FILE_NAME_POSTFIX;
- mainClassModel.put("className", className);
-
- String basePackagePath = projectMetadata.getPackageName().replace(".", "/");
- File srcMainJavaFile = new File(projectDestination, sourceFolder.srcMainJava());
-
- File mainClassFileDestination = new File(srcMainJavaFile, basePackagePath);
- String fileName = className + FILE_NAME_EXTENSION;
-
- freeMarkerTemplateEngine.generateFileFromTemplate(
- TEMPLATE_NAME, fileName, mainClassModel, mainClassFileDestination);
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaTestClassGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaTestClassGenerator.java
deleted file mode 100644
index 8008858..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootJavaTestClassGenerator.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot;
-
-import static io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot.constants.SpringBootJavaTestClassGeneratorConstants.*;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.configuration.properties.MavenJavaSourceFolderProperties;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import io.github.bsayli.codegen.initializr.projectgeneration.naming.NameConverter;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.FrameworkSpecificTestUnitGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine;
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import org.springframework.stereotype.Component;
-
-@Component("springBootJavaTestClassGenerator")
-public class SpringBootJavaTestClassGenerator implements FrameworkSpecificTestUnitGenerator {
-
- private final TemplateEngine freeMarkerTemplateEngine;
- private final MavenJavaSourceFolderProperties sourceFolder;
- private final NameConverter nameConverter;
-
- public SpringBootJavaTestClassGenerator(
- TemplateEngine freeMarkerTemplateEngine,
- MavenJavaSourceFolderProperties sourceFolder,
- NameConverter nameConverter) {
- this.sourceFolder = sourceFolder;
- this.freeMarkerTemplateEngine = freeMarkerTemplateEngine;
- this.nameConverter = nameConverter;
- }
-
- @Override
- public void generateTestClass(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException {
-
- Map testClassModel = new HashMap<>();
- testClassModel.put("projectPackageName", projectMetadata.getPackageName());
-
- String classBase = nameConverter.toPascalCase(projectMetadata.getName());
- String className = classBase + FILE_NAME_POSTFIX;
- testClassModel.put("className", className);
-
- String basePackagePath = projectMetadata.getPackageName().replace(".", "/");
- File srcTestJavaFile = new File(projectDestination, sourceFolder.srcTestJava());
-
- File testClassFileDestination = new File(srcTestJavaFile, basePackagePath);
- String fileName = className + FILE_NAME_EXTENSION;
-
- freeMarkerTemplateEngine.generateFileFromTemplate(
- TEMPLATE_NAME, fileName, testClassModel, testClassFileDestination);
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaProjectBuildGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaProjectBuildGenerator.java
deleted file mode 100644
index 817a7fd..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaProjectBuildGenerator.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.maven.MavenPlugin;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.maven.MavenPom;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.maven.MavenPom.MavenPomBuilder;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.templating.TemplateType;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectBuildGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectBuildWrapperGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine;
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.springframework.stereotype.Component;
-
-@Component("springBootMavenJavaProjectBuildGenerator")
-public class SpringBootMavenJavaProjectBuildGenerator implements ProjectBuildGenerator {
-
- private static final String MAVEN_MODEL_VERSION = "4.0.0";
- private static final String MAVEN_PROJECT_VERSION = "0.0.1-SNAPSHOT";
-
- private final TemplateEngine freeMarkerTemplateEngine;
- private final ProjectBuildWrapperGenerator mavenBuildWrapperGenerator;
- private final List springBootMavenJavaPlugins;
-
- public SpringBootMavenJavaProjectBuildGenerator(
- TemplateEngine freeMarkerTemplateEngine,
- ProjectBuildWrapperGenerator mavenBuildWrapperGenerator,
- List springBootMavenJavaPlugins) {
- this.freeMarkerTemplateEngine = freeMarkerTemplateEngine;
- this.springBootMavenJavaPlugins = springBootMavenJavaPlugins;
- this.mavenBuildWrapperGenerator = mavenBuildWrapperGenerator;
- }
-
- @Override
- public void generateBuildConfiguration(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException {
- MavenPomBuilder mavenPomBuilder = new MavenPom.MavenPomBuilder();
- MavenPom mavenPom =
- mavenPomBuilder
- .modelVersion(MAVEN_MODEL_VERSION)
- .version(MAVEN_PROJECT_VERSION)
- .projectMetadata(projectMetadata)
- .addDependencies(projectMetadata.getDependencies())
- .addPlugins(springBootMavenJavaPlugins)
- .build();
-
- Map pomModel = new HashMap<>();
- pomModel.put("pom", mavenPom);
-
- freeMarkerTemplateEngine.generateFileFromTemplate(
- TemplateType.SPRING_BOOT_JAVA_POM, pomModel, projectDestination);
-
- mavenBuildWrapperGenerator.generateBuildWrapper(projectDestination, projectMetadata);
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaReadMeGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaReadMeGenerator.java
deleted file mode 100644
index 6961c3f..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/SpringBootMavenJavaReadMeGenerator.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.templating.TemplateType;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectDocumentationGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine;
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import org.springframework.stereotype.Component;
-
-@Component("springBootMavenJavaReadMeGenerator")
-public class SpringBootMavenJavaReadMeGenerator implements ProjectDocumentationGenerator {
-
- private final TemplateEngine freeMarkerTemplateEngine;
-
- public SpringBootMavenJavaReadMeGenerator(TemplateEngine freeMarkerTemplateEngine) {
- this.freeMarkerTemplateEngine = freeMarkerTemplateEngine;
- }
-
- @Override
- public void generateProjectDocument(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException {
- Map readMeModel = new HashMap<>();
- readMeModel.put("projectName", projectMetadata.getName());
- freeMarkerTemplateEngine.generateFileFromTemplate(
- TemplateType.SPRING_BOOT_MAVEN_JAVA_README, readMeModel, projectDestination);
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/constants/SpringBootJavaMainClassGeneratorConstants.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/constants/SpringBootJavaMainClassGeneratorConstants.java
deleted file mode 100644
index 610d1d5..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/constants/SpringBootJavaMainClassGeneratorConstants.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot.constants;
-
-public class SpringBootJavaMainClassGeneratorConstants {
-
- public static final String TEMPLATE_NAME = "springBootMainClass.java.ftl";
- public static final String FILE_NAME_POSTFIX = "Application";
- public static final String FILE_NAME_EXTENSION = ".java";
-
- private SpringBootJavaMainClassGeneratorConstants() {}
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/constants/SpringBootJavaTestClassGeneratorConstants.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/constants/SpringBootJavaTestClassGeneratorConstants.java
deleted file mode 100644
index c703031..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/framework/springboot/constants/SpringBootJavaTestClassGeneratorConstants.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.adapters.framework.springboot.constants;
-
-public class SpringBootJavaTestClassGeneratorConstants {
-
- public static final String TEMPLATE_NAME = "springBootTestClass.java.ftl";
- public static final String FILE_NAME_POSTFIX = "ApplicationTests";
- public static final String FILE_NAME_EXTENSION = ".java";
-
- private SpringBootJavaTestClassGeneratorConstants() {}
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenBuildWrapperGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenBuildWrapperGenerator.java
deleted file mode 100644
index 493c3d3..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenBuildWrapperGenerator.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.adapters.maven;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.templating.TemplateType;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectBuildWrapperGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine;
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import org.springframework.stereotype.Component;
-
-@Component("mavenBuildWrapperGenerator")
-public class MavenBuildWrapperGenerator implements ProjectBuildWrapperGenerator {
-
- private static final String MAVEN_VERSION = "3.9.11";
- private static final String WRAPPER_VERSION = "3.3.3";
- private static final String WRAPPER_FILE_DIR = ".mvn/wrapper";
-
- private final TemplateEngine freeMarkerTemplateEngine;
-
- public MavenBuildWrapperGenerator(TemplateEngine freeMarkerTemplateEngine) {
- this.freeMarkerTemplateEngine = freeMarkerTemplateEngine;
- }
-
- @Override
- public void generateBuildWrapper(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException {
- Map wrapperModel = new HashMap<>();
- wrapperModel.put("wrapperVersion", WRAPPER_VERSION);
- wrapperModel.put("mavenVersion", MAVEN_VERSION);
- File wrapperFileDestination = new File(projectDestination, WRAPPER_FILE_DIR);
- freeMarkerTemplateEngine.generateFileFromTemplate(
- TemplateType.MAVEN_WRAPPER, wrapperModel, wrapperFileDestination);
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenJavaProjectLayoutGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenJavaProjectLayoutGenerator.java
deleted file mode 100644
index 9098da3..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/maven/MavenJavaProjectLayoutGenerator.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.adapters.maven;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.configuration.properties.MavenJavaSourceFolderProperties;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectLayoutGenerator;
-import java.io.File;
-import java.io.IOException;
-import java.util.List;
-import org.springframework.stereotype.Component;
-
-@Component("mavenJavaProjectLayoutGenerator")
-public class MavenJavaProjectLayoutGenerator implements ProjectLayoutGenerator {
-
- private final MavenJavaSourceFolderProperties sourceFolder;
-
- public MavenJavaProjectLayoutGenerator(MavenJavaSourceFolderProperties sourceFolder) {
- this.sourceFolder = sourceFolder;
- }
-
- @Override
- public void generateProjectLayout(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException {
- generateSourceFolders(projectDestination);
- generatePackages(projectDestination, projectMetadata.getPackageName());
- }
-
- private void generateSourceFolders(File projectDestination) {
- sourceFolder
- .getSourceFolders()
- .forEach(
- s -> {
- File sourceDir = new File(projectDestination, s);
- if (!sourceDir.exists()) {
- sourceDir.mkdirs();
- }
- });
- }
-
- private void generatePackages(File projectDestination, String packageName) {
- String packageNamePath = packageName.replace(".", "/");
- packageNamePath = sanitizePackageName(packageNamePath);
-
- File sourceFolderMainJavaFile = new File(projectDestination, sourceFolder.srcMainJava());
- File packageJavaFile = new File(sourceFolderMainJavaFile, packageNamePath);
-
- File sourceFolderTestJavaFile = new File(projectDestination, sourceFolder.srcTestJava());
- File packageTestFile = new File(sourceFolderTestJavaFile, packageNamePath);
-
- String packageGenPath = packageNamePath + "/codegen";
- File sourceFolderGenJavaFile = new File(projectDestination, sourceFolder.srcGenJava());
- File packageGenFile = new File(sourceFolderGenJavaFile, packageGenPath);
-
- List projectPackages = List.of(packageJavaFile, packageTestFile, packageGenFile);
-
- projectPackages.forEach(
- p -> {
- if (!p.exists()) {
- p.mkdirs();
- }
- });
- }
-
- private String sanitizePackageName(String packageName) {
- return packageName.replaceAll("[\\:*?<>|\\\\\\^]", "").replaceAll("-+", "_");
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/templating/FreeMarkerTemplateEngine.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/templating/FreeMarkerTemplateEngine.java
deleted file mode 100644
index 55f2c1d..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/adapters/templating/FreeMarkerTemplateEngine.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.adapters.templating;
-
-import freemarker.template.Configuration;
-import freemarker.template.Template;
-import freemarker.template.TemplateException;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.templating.TemplateType;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.TemplateEngine;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.Writer;
-import java.util.Map;
-import org.springframework.stereotype.Component;
-
-@Component("freeMarkerTemplateEngine")
-public class FreeMarkerTemplateEngine implements TemplateEngine {
-
- private final Configuration freemarkerTemplateConfiguration;
-
- public FreeMarkerTemplateEngine(Configuration freemarkerTemplateConfiguration) {
- this.freemarkerTemplateConfiguration = freemarkerTemplateConfiguration;
- }
-
- @Override
- public void generateFileFromTemplate(
- TemplateType templateType, Map data, File destination) throws IOException {
-
- String templateFileName = templateType.getTemplateFileName();
- String fileName = templateType.getFileName();
-
- generateFileFromTemplate(templateFileName, fileName, data, destination);
- }
-
- public void generateFileFromTemplate(
- String templateFileName, String fileName, Map data, File destination)
- throws IOException {
- Template template = freemarkerTemplateConfiguration.getTemplate(templateFileName);
-
- if (!destination.exists()) {
- destination.mkdirs();
- }
-
- try (Writer writer = new FileWriter(new File(destination, fileName))) {
- template.process(data, writer);
- } catch (TemplateException e) {
- throw new IOException("Error processing template: " + e.getMessage(), e);
- }
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/FreeMarkerTemplateConfiguration.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/FreeMarkerTemplateConfiguration.java
deleted file mode 100644
index fa79066..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/FreeMarkerTemplateConfiguration.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.configuration;
-
-import freemarker.template.TemplateExceptionHandler;
-import io.github.bsayli.codegen.initializr.projectgeneration.configuration.properties.FreeMarkerTemplateProperties;
-import java.io.Serial;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-@EnableConfigurationProperties(FreeMarkerTemplateProperties.class)
-public class FreeMarkerTemplateConfiguration {
-
- private final FreeMarkerTemplateProperties freeMarkerProperties;
-
- public FreeMarkerTemplateConfiguration(FreeMarkerTemplateProperties freeMarkerProperties) {
- this.freeMarkerProperties = freeMarkerProperties;
- }
-
- @Bean
- freemarker.template.Configuration freemarkerTemplateConfiguration()
- throws FreeMarkerConfigurationException {
- return initializeFreeMarkerTemplateConfiguration();
- }
-
- public freemarker.template.Configuration initializeFreeMarkerTemplateConfiguration() {
- freemarker.template.Configuration configuration =
- new freemarker.template.Configuration(freemarker.template.Configuration.VERSION_2_3_32);
- configuration.setDefaultEncoding(freeMarkerProperties.encoding());
- setTemplateExceptionHandler(configuration);
- configuration.setClassForTemplateLoading(this.getClass(), freeMarkerProperties.templatePath());
- return configuration;
- }
-
- private void setTemplateExceptionHandler(freemarker.template.Configuration configuration) {
- try {
- TemplateExceptionHandler exceptionHandler =
- switch (freeMarkerProperties.templateExceptionHandler()) {
- case "RETHROW_HANDLER" -> TemplateExceptionHandler.RETHROW_HANDLER;
- case "DEBUG_HANDLER" -> TemplateExceptionHandler.DEBUG_HANDLER;
- case "HTML_DEBUG_HANDLER" -> TemplateExceptionHandler.HTML_DEBUG_HANDLER;
- case "IGNORE_HANDLER" -> TemplateExceptionHandler.IGNORE_HANDLER;
- default ->
- throw new IllegalArgumentException(
- "Invalid exception handler name: "
- + freeMarkerProperties.templateExceptionHandler());
- };
- configuration.setTemplateExceptionHandler(exceptionHandler);
- } catch (IllegalArgumentException e) {
- throw new FreeMarkerConfigurationException(
- "Invalid templateExceptionHandler value: "
- + freeMarkerProperties.templateExceptionHandler(),
- e);
- }
- }
-}
-
-class FreeMarkerConfigurationException extends RuntimeException {
-
- @Serial private static final long serialVersionUID = 4482627787641879716L;
-
- public FreeMarkerConfigurationException(String message, Exception e) {
- super(message, e);
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/SpringBootMavenJavaProjectConfiguration.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/SpringBootMavenJavaProjectConfiguration.java
deleted file mode 100644
index 6c20a3a..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/SpringBootMavenJavaProjectConfiguration.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.configuration;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.maven.MavenPlugin;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-public class SpringBootMavenJavaProjectConfiguration {
-
- @Bean
- List springBootMavenJavaPlugins() {
- List springBootMavenPlugins = new ArrayList<>();
-
- MavenPlugin springBootMavenPlugin =
- new MavenPlugin.MavenPluginBuilder()
- .groupId("org.springframework.boot")
- .artifactId("spring-boot-maven-plugin")
- .build();
-
- MavenPlugin mavenCompilerPlugin =
- new MavenPlugin.MavenPluginBuilder()
- .groupId("org.apache.maven.plugins")
- .artifactId("maven-compiler-plugin")
- .addConfigurationElement("generatedSourcesDirectory", "src/gen/java")
- .addConfigurationElement("compileSourceRoots", List.of("src/main/java", "src/gen/java"))
- .build();
-
- springBootMavenPlugins.add(springBootMavenPlugin);
- springBootMavenPlugins.add(mavenCompilerPlugin);
-
- return Collections.unmodifiableList(springBootMavenPlugins);
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/properties/FreeMarkerTemplateProperties.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/properties/FreeMarkerTemplateProperties.java
deleted file mode 100644
index 0e625d9..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/properties/FreeMarkerTemplateProperties.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.configuration.properties;
-
-import org.springframework.boot.context.properties.ConfigurationProperties;
-
-@ConfigurationProperties(prefix = "freemarker")
-public record FreeMarkerTemplateProperties(
- String encoding, String templateExceptionHandler, String templatePath) {}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/properties/MavenJavaSourceFolderProperties.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/properties/MavenJavaSourceFolderProperties.java
deleted file mode 100644
index cccaffd..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/configuration/properties/MavenJavaSourceFolderProperties.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.configuration.properties;
-
-import java.util.List;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-
-@ConfigurationProperties(prefix = "maven.source-folder")
-public record MavenJavaSourceFolderProperties(
- String srcMainJava,
- String srcMainResources,
- String srcTestJava,
- String srcTestResources,
- String srcGenJava) {
-
- public List getSourceFolders() {
- return List.of(srcMainJava, srcMainResources, srcTestJava, srcTestResources, srcGenJava);
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/ProjectGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/ProjectGenerator.java
deleted file mode 100644
index c528b51..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/ProjectGenerator.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.generator;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import java.io.IOException;
-import java.nio.file.Path;
-
-public interface ProjectGenerator {
-
- /**
- * Generates a project based on the provided project metadata. This method delegates the specific
- * generation tasks to appropriate collaborators (ports) based on the project type.
- *
- * @param projectMetadata The project metadata object containing information about the desired
- * project type.
- * @throws IOException If an error occurs during project generation.
- */
- Path generateProject(ProjectMetadata projectMetadata) throws IOException;
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/base/AbstractProjectGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/base/AbstractProjectGenerator.java
deleted file mode 100644
index 15e6fc3..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/base/AbstractProjectGenerator.java
+++ /dev/null
@@ -1,118 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.generator.base;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.generator.ProjectGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ApplicationYamlGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.FrameworkProjectStarterClassGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.FrameworkSpecificTestUnitGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.GitIgnoreGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectArchiver;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectBuildGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectDirectoryInitializer;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectDocumentationGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectLayoutGenerator;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.Collections;
-import java.util.List;
-
-public abstract class AbstractProjectGenerator implements ProjectGenerator {
-
- private final ProjectDirectoryInitializer projectDirectoryInitializer;
- private final GitIgnoreGenerator gitIgnoreGenerator;
- private final ProjectArchiver projectArchiver;
- private final ProjectLayoutGenerator projectLayoutGenerator;
- private final ProjectBuildGenerator projectBuildGenerator;
- private final ApplicationYamlGenerator applicationYamlGenerator;
- private final FrameworkProjectStarterClassGenerator frameworkProjectStarterClassGenerator;
- private final FrameworkSpecificTestUnitGenerator frameworkSpecificTestUnitGenerator;
- private final ProjectDocumentationGenerator projectDocumentationGenerator;
-
- protected AbstractProjectGenerator(
- ProjectDirectoryInitializer projectDirectoryInitializer,
- GitIgnoreGenerator gitIgnoreGenerator,
- ProjectArchiver projectArchiver,
- ProjectLayoutGenerator projectLayoutGenerator,
- ProjectBuildGenerator projectBuildGenerator,
- ApplicationYamlGenerator applicationYamlGenerator,
- FrameworkProjectStarterClassGenerator frameworkProjectStarterClassGenerator,
- FrameworkSpecificTestUnitGenerator frameworkSpecificTestUnitGenerator,
- ProjectDocumentationGenerator projectDocumentationGenerator) {
- this.projectDirectoryInitializer = projectDirectoryInitializer;
- this.gitIgnoreGenerator = gitIgnoreGenerator;
- this.projectArchiver = projectArchiver;
- this.projectLayoutGenerator = projectLayoutGenerator;
- this.projectBuildGenerator = projectBuildGenerator;
- this.applicationYamlGenerator = applicationYamlGenerator;
- this.frameworkProjectStarterClassGenerator = frameworkProjectStarterClassGenerator;
- this.frameworkSpecificTestUnitGenerator = frameworkSpecificTestUnitGenerator;
- this.projectDocumentationGenerator = projectDocumentationGenerator;
- }
-
- @Override
- public final Path generateProject(ProjectMetadata projectMetadata) throws IOException {
- Path projectDestinationPath = initializeProjectDirectory(projectMetadata);
- File projectDestination = projectDestinationPath.toFile();
-
- generateGitIgnoreContent(projectDestination);
- generateProjectLayout(projectDestination, projectMetadata);
- generateBuildConfiguration(projectDestination, projectMetadata);
- generateApplicationProperties(projectDestination, projectMetadata);
- generateProjectStarterClass(projectDestination, projectMetadata);
- generateTestClass(projectDestination, projectMetadata);
- generateProjectDocument(projectDestination, projectMetadata);
- return archiveProject(projectDestination, projectMetadata);
- }
-
- protected Path initializeProjectDirectory(ProjectMetadata projectMetadata) throws IOException {
- if (projectMetadata.getProjectLocation() != null) {
- return projectDirectoryInitializer.initializeProjectDirectory(
- projectMetadata.getArtifactId(), projectMetadata.getProjectLocation());
- } else {
- return projectDirectoryInitializer.initializeProjectDirectory(
- projectMetadata.getArtifactId());
- }
- }
-
- protected void generateGitIgnoreContent(File projectDestination) throws IOException {
- List ignoreList = Collections.emptyList();
- gitIgnoreGenerator.generateGitIgnoreContent(projectDestination, ignoreList);
- }
-
- protected void generateProjectLayout(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException {
- projectLayoutGenerator.generateProjectLayout(projectDestination, projectMetadata);
- }
-
- protected void generateBuildConfiguration(
- File projectDestination, ProjectMetadata projectMetadata) throws IOException {
- projectBuildGenerator.generateBuildConfiguration(projectDestination, projectMetadata);
- }
-
- protected void generateApplicationProperties(
- File projectDestination, ProjectMetadata projectMetadata) throws IOException {
- applicationYamlGenerator.generateApplicationYaml(projectDestination, projectMetadata);
- }
-
- protected void generateProjectStarterClass(
- File projectDestination, ProjectMetadata projectMetadata) throws IOException {
- frameworkProjectStarterClassGenerator.generateProjectStarterClass(
- projectDestination, projectMetadata);
- }
-
- protected void generateTestClass(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException {
- frameworkSpecificTestUnitGenerator.generateTestClass(projectDestination, projectMetadata);
- }
-
- protected void generateProjectDocument(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException {
- projectDocumentationGenerator.generateProjectDocument(projectDestination, projectMetadata);
- }
-
- protected Path archiveProject(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException {
- return projectArchiver.archiveProject(projectDestination, projectMetadata.getName());
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/springboot/maven/SpringBootMavenJavaProjectGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/springboot/maven/SpringBootMavenJavaProjectGenerator.java
deleted file mode 100644
index 41a2022..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/generator/springboot/maven/SpringBootMavenJavaProjectGenerator.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.generator.springboot.maven;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.generator.base.AbstractProjectGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ApplicationYamlGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.FrameworkProjectStarterClassGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.FrameworkSpecificTestUnitGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.GitIgnoreGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectArchiver;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectBuildGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectDirectoryInitializer;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectDocumentationGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.ports.ProjectLayoutGenerator;
-import org.springframework.stereotype.Component;
-
-@Component("springBootMavenJavaProjectGenerator")
-public class SpringBootMavenJavaProjectGenerator extends AbstractProjectGenerator {
-
- public SpringBootMavenJavaProjectGenerator(
- ProjectDirectoryInitializer projectRootDirectoryInitializer,
- GitIgnoreGenerator gitIgnoreFileGenerator,
- ProjectArchiver projectZipArchiver,
- ProjectLayoutGenerator mavenJavaProjectLayoutGenerator,
- ProjectBuildGenerator springBootMavenJavaProjectBuildGenerator,
- ApplicationYamlGenerator springBootApplicationYamlGenerator,
- FrameworkProjectStarterClassGenerator springBootJavaMainClassGenerator,
- FrameworkSpecificTestUnitGenerator springBootJavaTestClassGenerator,
- ProjectDocumentationGenerator springBootMavenJavaReadMeGenerator) {
- super(
- projectRootDirectoryInitializer,
- gitIgnoreFileGenerator,
- projectZipArchiver,
- mavenJavaProjectLayoutGenerator,
- springBootMavenJavaProjectBuildGenerator,
- springBootApplicationYamlGenerator,
- springBootJavaMainClassGenerator,
- springBootJavaTestClassGenerator,
- springBootMavenJavaReadMeGenerator);
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/Dependency.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/Dependency.java
deleted file mode 100644
index 2327d8d..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/Dependency.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.model;
-
-public class Dependency {
- private final String groupId;
- private final String artifactId;
- private final String version;
- private final String scope;
-
- private Dependency(DependencyBuilder builder) {
- this.groupId = builder.groupId;
- this.artifactId = builder.artifactId;
- this.version = builder.version;
- this.scope = builder.scope;
- }
-
- public String getGroupId() {
- return groupId;
- }
-
- public String getArtifactId() {
- return artifactId;
- }
-
- public String getVersion() {
- return version;
- }
-
- public String getScope() {
- return scope;
- }
-
- @Override
- public String toString() {
- return toShortDefinition();
- }
-
- public String toShortDefinition() {
- return "Dependency [artifactId=" + artifactId + "]";
- }
-
- public String toLongDefinition() {
- return "Dependency [groupId="
- + groupId
- + ", artifactId="
- + artifactId
- + ", version="
- + version
- + ", scope="
- + scope
- + "]";
- }
-
- public static class DependencyBuilder {
-
- private String groupId;
- private String artifactId;
- private String version;
- private String scope;
-
- public DependencyBuilder groupId(String groupId) {
- this.groupId = groupId;
- return this;
- }
-
- public DependencyBuilder artifactId(String artifactId) {
- this.artifactId = artifactId;
- return this;
- }
-
- public DependencyBuilder version(String version) {
- this.version = version;
- return this;
- }
-
- public DependencyBuilder scope(String scope) {
- this.scope = scope;
- return this;
- }
-
- public Dependency build() {
- if (groupId == null || artifactId == null) {
- throw new IllegalStateException("groupId and artifactId are required fields");
- }
- return new Dependency(this);
- }
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/ProjectMetadata.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/ProjectMetadata.java
deleted file mode 100644
index d09c2fa..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/ProjectMetadata.java
+++ /dev/null
@@ -1,103 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.model;
-
-import java.nio.file.Path;
-import java.util.List;
-
-public class ProjectMetadata {
-
- private final String name;
- private final String description;
- private final String groupId;
- private final String artifactId;
- private final String packageName;
- private final List dependencies;
- private final Path projectLocation;
-
- protected ProjectMetadata(ProjectMetadataBuilder builder) {
- this.name = builder.name;
- this.description = builder.description;
- this.groupId = builder.groupId;
- this.artifactId = builder.artifactId;
- this.packageName = builder.packageName;
- this.dependencies = builder.dependencies;
- this.projectLocation = builder.projectLocation;
- }
-
- public String getName() {
- return name;
- }
-
- public String getGroupId() {
- return groupId;
- }
-
- public String getArtifactId() {
- return artifactId;
- }
-
- public String getPackageName() {
- return packageName;
- }
-
- public String getDescription() {
- return description;
- }
-
- public List getDependencies() {
- return dependencies;
- }
-
- public Path getProjectLocation() {
- return projectLocation;
- }
-
- public static class ProjectMetadataBuilder {
-
- private String name;
- private String description;
- private String groupId;
- private String artifactId;
- private String packageName;
- private List dependencies;
- private Path projectLocation;
-
- public ProjectMetadataBuilder name(String name) {
- this.name = name;
- return this;
- }
-
- public ProjectMetadataBuilder description(String description) {
- this.description = description;
- return this;
- }
-
- public ProjectMetadataBuilder groupId(String groupId) {
- this.groupId = groupId;
- return this;
- }
-
- public ProjectMetadataBuilder artifactId(String artifactId) {
- this.artifactId = artifactId;
- return this;
- }
-
- public ProjectMetadataBuilder packageName(String packageName) {
- this.packageName = packageName;
- return this;
- }
-
- public ProjectMetadataBuilder dependencies(List dependencies) {
- this.dependencies = dependencies;
- return this;
- }
-
- public ProjectMetadataBuilder projectLocation(Path projectLocation) {
- this.projectLocation = projectLocation;
- return this;
- }
-
- public ProjectMetadata build() {
- return new ProjectMetadata(this);
- }
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/ProjectType.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/ProjectType.java
deleted file mode 100644
index b95ddfa..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/ProjectType.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.model;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.BuildTool;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Framework;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Language;
-
-public record ProjectType(Framework framework, BuildTool buildTool, Language language) {}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/maven/MavenPlugin.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/maven/MavenPlugin.java
deleted file mode 100644
index 0299129..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/maven/MavenPlugin.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.model.maven;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-public class MavenPlugin {
-
- private final String groupId;
- private final String artifactId;
- private final String version;
- private final Map configuration;
-
- private MavenPlugin(MavenPluginBuilder builder) {
- this.groupId = builder.groupId;
- this.artifactId = builder.artifactId;
- this.version = builder.version;
- this.configuration = builder.configuration;
- }
-
- public String getGroupId() {
- return groupId;
- }
-
- public String getArtifactId() {
- return artifactId;
- }
-
- public String getVersion() {
- return version;
- }
-
- public Map getConfiguration() {
- return configuration;
- }
-
- public static class MavenPluginBuilder {
- private final Map configuration = new LinkedHashMap<>();
- private String groupId;
- private String artifactId;
- private String version;
-
- public MavenPluginBuilder groupId(String groupId) {
- this.groupId = groupId;
- return this;
- }
-
- public MavenPluginBuilder artifactId(String artifactId) {
- this.artifactId = artifactId;
- return this;
- }
-
- public MavenPluginBuilder version(String version) {
- this.version = version;
- return this;
- }
-
- public MavenPluginBuilder addConfigurationElement(String key, Object value) {
- configuration.put(key, value);
- return this;
- }
-
- public MavenPlugin build() {
- if (groupId == null || artifactId == null) {
- throw new IllegalStateException("groupId, artifactId are required fields");
- }
- return new MavenPlugin(this);
- }
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/maven/MavenPom.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/maven/MavenPom.java
deleted file mode 100644
index facc982..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/maven/MavenPom.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.model.maven;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.Dependency;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import java.util.ArrayList;
-import java.util.List;
-
-public class MavenPom {
-
- private final String modelVersion;
- private final String version;
- private final ProjectMetadata projectMetadata;
- private final List dependencies;
- private final List plugins;
-
- private MavenPom(MavenPomBuilder builder) {
- this.modelVersion = builder.modelVersion;
- this.version = builder.version;
- this.projectMetadata = builder.projectMetadata;
- this.dependencies = builder.dependencies;
- this.plugins = builder.plugins;
- }
-
- public String getModelVersion() {
- return modelVersion;
- }
-
- public String getVersion() {
- return version;
- }
-
- public ProjectMetadata getProjectMetadata() {
- return projectMetadata;
- }
-
- public List getDependencies() {
- return dependencies;
- }
-
- public List getPlugins() {
- return plugins;
- }
-
- public static class MavenPomBuilder {
- private String modelVersion;
- private String version;
- private ProjectMetadata projectMetadata;
- private List dependencies = new ArrayList<>();
- private List plugins = new ArrayList<>();
-
- public MavenPomBuilder modelVersion(String modelVersion) {
- this.modelVersion = modelVersion;
- return this;
- }
-
- public MavenPomBuilder version(String version) {
- this.version = version;
- return this;
- }
-
- public MavenPomBuilder projectMetadata(ProjectMetadata projectMetadata) {
- this.projectMetadata = projectMetadata;
- return this;
- }
-
- public MavenPomBuilder addDependencies(List dependencies) {
- this.dependencies = new ArrayList<>(dependencies);
- return this;
- }
-
- public MavenPomBuilder addPlugins(List plugins) {
- this.plugins = new ArrayList<>(plugins);
- return this;
- }
-
- public MavenPom build() {
- return new MavenPom(this);
- }
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/spring/SpringBootJavaProjectMetadata.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/spring/SpringBootJavaProjectMetadata.java
deleted file mode 100644
index 29f5cb4..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/spring/SpringBootJavaProjectMetadata.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.model.spring;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-
-public class SpringBootJavaProjectMetadata extends ProjectMetadata {
-
- private String springBootVersion;
- private String javaVersion;
-
- protected SpringBootJavaProjectMetadata(
- ProjectMetadataBuilder builder, String springBootVersion, String javaVersion) {
- super(builder);
- this.springBootVersion = springBootVersion;
- this.javaVersion = javaVersion;
- }
-
- public String getSpringBootVersion() {
- return springBootVersion;
- }
-
- public void setSpringBootVersion(String springBootVersion) {
- this.springBootVersion = springBootVersion;
- }
-
- public String getJavaVersion() {
- return javaVersion;
- }
-
- public void setJavaVersion(String javaVersion) {
- this.javaVersion = javaVersion;
- }
-
- public static class SpringBootJavaProjectMetadataBuilder extends ProjectMetadataBuilder {
- private String springBootVersion;
- private String javaVersion;
-
- public SpringBootJavaProjectMetadataBuilder springBootVersion(String springBootVersion) {
- this.springBootVersion = springBootVersion;
- return this;
- }
-
- public SpringBootJavaProjectMetadataBuilder javaVersion(String javaVersion) {
- this.javaVersion = javaVersion;
- return this;
- }
-
- @Override
- public SpringBootJavaProjectMetadata build() {
- return new SpringBootJavaProjectMetadata(this, springBootVersion, javaVersion);
- }
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/BuildTool.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/BuildTool.java
deleted file mode 100644
index f0570a0..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/BuildTool.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.model.techstack;
-
-public enum BuildTool {
- MAVEN,
- GRADLE
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/Framework.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/Framework.java
deleted file mode 100644
index def9f0b..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/Framework.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.model.techstack;
-
-public enum Framework {
- SPRING_BOOT,
- QUARKUS
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/Language.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/Language.java
deleted file mode 100644
index bf619c3..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/techstack/Language.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.model.techstack;
-
-public enum Language {
- JAVA,
- KOTLIN
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/templating/TemplateType.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/templating/TemplateType.java
deleted file mode 100644
index 54b973d..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/model/templating/TemplateType.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.model.templating;
-
-public enum TemplateType {
- GITIGNORE(".gitignore", "gitignore.ftl"),
- SPRING_BOOT_JAVA_POM("pom.xml", "springBootJavaPom.xml.ftl"),
- SPRING_BOOT_MAVEN_JAVA_README("README.md", "springBootMavenJavaReadMe.ftl"),
- MAVEN_WRAPPER("maven-wrapper.properties", "maven-wrapper.properties.ftl"),
- SPRING_BOOT_APPLICATION_YAML("application.yml", "springBootApplication.yml.ftl");
-
- private final String fileName;
- private final String templateFileName;
-
- TemplateType(String fileName, String templateFileName) {
- this.fileName = fileName;
- this.templateFileName = templateFileName;
- }
-
- public String getFileName() {
- return fileName;
- }
-
- public String getTemplateFileName() {
- return templateFileName;
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/naming/NameConverter.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/naming/NameConverter.java
deleted file mode 100644
index d70e1a2..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/naming/NameConverter.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.naming;
-
-import org.springframework.stereotype.Component;
-
-@Component
-public class NameConverter {
-
- /**
- * Converts a raw project name into Java-friendly PascalCase. Examples: "codegen-demo" ->
- * "CodegenDemo" "my_service.core" -> "MyServiceCore" "123-metric" -> "App123Metric"
- */
- public String toPascalCase(String raw) {
- if (raw == null || raw.isBlank()) return "";
- String[] parts = raw.split("[^A-Za-z0-9]+");
- StringBuilder sb = new StringBuilder();
- for (String p : parts) {
- if (p.isBlank()) continue;
- String lower = p.toLowerCase();
- sb.append(Character.toUpperCase(lower.charAt(0)));
- if (lower.length() > 1) {
- sb.append(lower.substring(1));
- }
- }
- String result = sb.toString();
- if (!result.isEmpty() && Character.isDigit(result.charAt(0))) {
- result = "App" + result;
- }
- return result;
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ApplicationYamlGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ApplicationYamlGenerator.java
deleted file mode 100644
index 5a0b9e9..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ApplicationYamlGenerator.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.ports;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import java.io.File;
-import java.io.IOException;
-
-public interface ApplicationYamlGenerator {
-
- void generateApplicationYaml(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException;
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/FrameworkProjectStarterClassGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/FrameworkProjectStarterClassGenerator.java
deleted file mode 100644
index 7494230..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/FrameworkProjectStarterClassGenerator.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.ports;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import java.io.File;
-import java.io.IOException;
-
-public interface FrameworkProjectStarterClassGenerator {
-
- void generateProjectStarterClass(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException;
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/FrameworkSpecificTestUnitGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/FrameworkSpecificTestUnitGenerator.java
deleted file mode 100644
index d1fe0e2..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/FrameworkSpecificTestUnitGenerator.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.ports;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import java.io.File;
-import java.io.IOException;
-
-public interface FrameworkSpecificTestUnitGenerator {
-
- void generateTestClass(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException;
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/GitIgnoreGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/GitIgnoreGenerator.java
deleted file mode 100644
index 44a5906..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/GitIgnoreGenerator.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.ports;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.List;
-
-public interface GitIgnoreGenerator {
-
- void generateGitIgnoreContent(File projectDestination, List ignoreList)
- throws IOException;
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectArchiver.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectArchiver.java
deleted file mode 100644
index 92c679d..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectArchiver.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.ports;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Path;
-
-public interface ProjectArchiver {
-
- Path archiveProject(File projectDestination, String projectName) throws IOException;
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectBuildGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectBuildGenerator.java
deleted file mode 100644
index c9c5ada..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectBuildGenerator.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.ports;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import java.io.File;
-import java.io.IOException;
-
-public interface ProjectBuildGenerator {
-
- void generateBuildConfiguration(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException;
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectBuildWrapperGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectBuildWrapperGenerator.java
deleted file mode 100644
index 3e470c1..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectBuildWrapperGenerator.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.ports;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import java.io.File;
-import java.io.IOException;
-
-public interface ProjectBuildWrapperGenerator {
-
- void generateBuildWrapper(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException;
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectDirectoryInitializer.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectDirectoryInitializer.java
deleted file mode 100644
index bf3df58..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectDirectoryInitializer.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.ports;
-
-import java.io.IOException;
-import java.nio.file.Path;
-
-public interface ProjectDirectoryInitializer {
-
- Path initializeProjectDirectory(String projectName) throws IOException;
-
- Path initializeProjectDirectory(String projectName, Path projectLocation) throws IOException;
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectDocumentationGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectDocumentationGenerator.java
deleted file mode 100644
index 3de2d4e..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectDocumentationGenerator.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.ports;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import java.io.File;
-import java.io.IOException;
-
-public interface ProjectDocumentationGenerator {
-
- void generateProjectDocument(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException;
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectLayoutGenerator.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectLayoutGenerator.java
deleted file mode 100644
index 5262192..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/ProjectLayoutGenerator.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.ports;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import java.io.File;
-import java.io.IOException;
-
-public interface ProjectLayoutGenerator {
-
- void generateProjectLayout(File projectDestination, ProjectMetadata projectMetadata)
- throws IOException;
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/TemplateEngine.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/TemplateEngine.java
deleted file mode 100644
index 29b7b8b..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/ports/TemplateEngine.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.ports;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.templating.TemplateType;
-import java.io.File;
-import java.io.IOException;
-import java.util.Map;
-
-public interface TemplateEngine {
-
- void generateFileFromTemplate(
- TemplateType templateType, Map data, File destination) throws IOException;
-
- void generateFileFromTemplate(
- String templateFileName, String fileName, Map data, File destination)
- throws IOException;
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/ProjectGeneratorRegistry.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/ProjectGeneratorRegistry.java
deleted file mode 100644
index 8f57415..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/ProjectGeneratorRegistry.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.registry;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.generator.ProjectGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType;
-import java.util.Optional;
-
-public interface ProjectGeneratorRegistry {
-
- Optional getProjectGenerator(ProjectType projectType);
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/SimpleProjectGeneratorRegistry.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/SimpleProjectGeneratorRegistry.java
deleted file mode 100644
index 3e8527f..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/SimpleProjectGeneratorRegistry.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.registry;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.generator.ProjectGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType;
-import java.util.Map;
-import java.util.Optional;
-import org.springframework.stereotype.Component;
-
-@Component
-public class SimpleProjectGeneratorRegistry implements ProjectGeneratorRegistry {
-
- private final Map registeredProjectGenerators;
-
- public SimpleProjectGeneratorRegistry(
- Map registeredProjectGenerators) {
- this.registeredProjectGenerators = registeredProjectGenerators;
- }
-
- @Override
- public Optional getProjectGenerator(ProjectType projectType) {
- return Optional.ofNullable(registeredProjectGenerators.get(projectType));
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/configuration/ProjectGeneratorRegistryConfiguration.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/configuration/ProjectGeneratorRegistryConfiguration.java
deleted file mode 100644
index aeea919..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/registry/configuration/ProjectGeneratorRegistryConfiguration.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.registry.configuration;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.generator.ProjectGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.BuildTool;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Framework;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.techstack.Language;
-import java.util.HashMap;
-import java.util.Map;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-public class ProjectGeneratorRegistryConfiguration {
-
- private final ProjectGenerator springBootMavenJavaProjectGenerator;
-
- public ProjectGeneratorRegistryConfiguration(
- ProjectGenerator springBootMavenJavaProjectGenerator) {
- this.springBootMavenJavaProjectGenerator = springBootMavenJavaProjectGenerator;
- }
-
- @Bean
- Map registeredProjectGenerators() {
- Map generatorFactories = new HashMap<>();
-
- ProjectType springBootMavenJavaProjectType =
- new ProjectType(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA);
-
- generatorFactories.put(springBootMavenJavaProjectType, springBootMavenJavaProjectGenerator);
-
- return generatorFactories;
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/ProjectGenerationService.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/ProjectGenerationService.java
deleted file mode 100644
index eee7d9a..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/ProjectGenerationService.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.service;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType;
-import java.io.IOException;
-import java.nio.file.Path;
-
-public interface ProjectGenerationService {
-
- Path generateProject(ProjectType projectType, ProjectMetadata projectMetadata) throws IOException;
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/ProjectGenerationServiceImpl.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/ProjectGenerationServiceImpl.java
deleted file mode 100644
index d9256ed..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/ProjectGenerationServiceImpl.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.service;
-
-import io.github.bsayli.codegen.initializr.projectgeneration.generator.ProjectGenerator;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectMetadata;
-import io.github.bsayli.codegen.initializr.projectgeneration.model.ProjectType;
-import io.github.bsayli.codegen.initializr.projectgeneration.registry.ProjectGeneratorRegistry;
-import io.github.bsayli.codegen.initializr.projectgeneration.service.exception.ProjectGenerationException;
-import java.io.IOException;
-import java.nio.file.Path;
-import org.springframework.stereotype.Service;
-
-@Service
-public class ProjectGenerationServiceImpl implements ProjectGenerationService {
-
- private final ProjectGeneratorRegistry registry;
-
- public ProjectGenerationServiceImpl(ProjectGeneratorRegistry registry) {
- this.registry = registry;
- }
-
- @Override
- public Path generateProject(ProjectType projectType, ProjectMetadata projectMetadata) {
- ProjectGenerator projectGenerator =
- registry
- .getProjectGenerator(projectType)
- .orElseThrow(
- () -> new IllegalArgumentException("Unsupported project type: " + projectType));
- try {
- return projectGenerator.generateProject(projectMetadata);
- } catch (IOException e) {
- throw new ProjectGenerationException("Error generating project: " + e.getMessage(), e);
- }
- }
-}
diff --git a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/exception/ProjectGenerationException.java b/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/exception/ProjectGenerationException.java
deleted file mode 100644
index 4808e8a..0000000
--- a/src/main/java/io/github/bsayli/codegen/initializr/projectgeneration/service/exception/ProjectGenerationException.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package io.github.bsayli.codegen.initializr.projectgeneration.service.exception;
-
-public class ProjectGenerationException extends RuntimeException {
-
- public ProjectGenerationException(String message) {
- super(message);
- }
-
- public ProjectGenerationException(String message, Throwable cause) {
- super(message, cause);
- }
-}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 0171193..15a2052 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,16 +1,73 @@
spring:
application:
- name: codegen-springboot-initializr
+ name: codegen-blueprint
-freemarker:
+codegen:
+ samples:
+ standard: sample/standard
+ hexagonal: sample/hexagonal
+ basic-dir-name: basic
+ rich-dir-name: rich
+ profiles:
+ springboot-maven-java:
+ template-base-path: springboot/maven/java/
+
+ ordered-artifact-keys:
+ - build-config
+ - build-tool-metadata
+ - ignore-rules
+ - source-layout
+ - app-config
+ - main-source-entrypoint
+ - test-source-entrypoint
+ - sample-code
+ - project-documentation
+
+ artifacts:
+ build-config:
+ templates:
+ - template: pom.xml.ftl
+ output-path: pom.xml
+
+ build-tool-metadata:
+ templates:
+ - template: maven-wrapper.properties.ftl
+ output-path: .mvn/wrapper/maven-wrapper.properties
+
+ ignore-rules:
+ templates:
+ - template: gitignore.ftl
+ output-path: .gitignore
+
+ source-layout:
+ templates: []
+
+ app-config:
+ templates:
+ - template: application.yml.ftl
+ output-path: src/main/resources/application.yml
+
+ main-source-entrypoint:
+ templates:
+ - template: MainClass.java.ftl
+ output-path: src/main/java
+
+ test-source-entrypoint:
+ templates:
+ - template: MainClassTests.java.ftl
+ output-path: src/test/java
+
+ sample-code:
+ templates: [ ]
+
+ project-documentation:
+ templates:
+ - template: README.md.ftl
+ output-path: README.md
+
+templating:
encoding: UTF-8
- template-exception-handler: RETHROW_HANDLER
+ handler: RETHROW
template-path: /templates
-
-maven:
- source-folder:
- src-main-java: src/main/java
- src-main-resources: src/main/resources
- src-test-java: src/test/java
- src-test-resources: src/test/resources
- src-gen-java: src/gen/java
\ No newline at end of file
+ cache-enabled: true
+ cache-update-delay-ms: 60000
\ No newline at end of file
diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties
new file mode 100644
index 0000000..7f7e545
--- /dev/null
+++ b/src/main/resources/messages.properties
@@ -0,0 +1,79 @@
+# ================================
+# === DOMAIN LAYER MESSAGES ===
+# ================================
+# --- GROUP ID ---
+project.group-id.not.blank=GroupId is required
+project.group-id.length=GroupId must be between {0} and {1} characters
+project.group-id.segment.format=GroupId must be dot-separated segments like 'com.example' (segment: [a-z][a-z0-9]*)
+# --- PROJECT DESCRIPTION ---
+project.description.not.blank=Description must not be blank
+project.description.length=Description must be between {0} and {1} characters
+project.description.control.chars=Description contains invalid control characters
+# --- PROJECT NAME ---
+project.name.not.blank=Project name is required
+project.name.length=Project name must be between {0} and {1} characters
+project.name.invalid.chars=Project name contains invalid characters
+# --- ARTIFACT ID ---
+project.artifact-id.not.blank=ArtifactId is required
+project.artifact-id.length=ArtifactId must be between {0} and {1} characters
+project.artifact-id.invalid.chars=Only lowercase letters, digits and '-' are allowed
+project.artifact-id.starts.with.letter=ArtifactId must start with a letter
+project.artifact-id.edge.char=ArtifactId must not start or end with '-'
+# --- PACKAGE NAME ---
+project.package-name.not.blank=Package name is required
+project.package-name.length=Package name must be between {0} and {1} characters
+project.package-name.segment.format=Package name must be dot-separated segments like 'com.example' (segment: [a-z][a-z0-9]*)
+project.package-name.reserved.prefix=Reserved package prefixes are not allowed (java, javax, sun, com.sun)
+# --- PROJECT IDENTITY ---
+project.identity.not.blank=Project identity requires both GroupId and ArtifactId
+# --- TECHNOLOGY STACK ---
+project.tech-stack.not.blank=Technology stack requires Framework, Build Tool, and Language
+project.tech-stack.framework.unknown=Unknown framework: {0}. Supported values: spring-boot
+project.tech-stack.build-tool.unknown=Unknown build tool: {0}. Supported values: maven
+project.tech-stack.language.unknown=Unknown language: {0}. Supported values: java
+# --- PROJECT LAYOUT ---
+project.layout.unknown=Unknown project layout: {0}. Supported layouts: standard, hexagonal
+# --- PLATFORM TARGET ---
+platform.target.not.blank=Platform target requires both Java version and Spring Boot version
+platform.target.missing=Platform target and technology stack must be provided
+platform.target.unsupported.options=Unsupported options: framework={0}, language={1}, buildTool={2}
+platform.target.incompatible=Selected platform is incompatible (springBoot={0}, java={1})
+platform.java-version.unknown=Unknown Java version ''{0}''.
+platform.springboot-version.unknown=Unknown Spring Boot version ''{0}''.
+# --- DEPENDENCY ---
+dependency.version.not.blank=Dependency version is required
+dependency.version.invalid.chars=Dependency version contains invalid characters
+dependency.coordinates.not.blank=Dependency coordinates require both GroupId and ArtifactId
+dependency.list.not.blank=Dependency list is required
+dependency.item.not.blank=Dependency entry must not be null
+dependency.duplicate.coordinates=Duplicate dependency coordinates: {0}
+# --- PROJECT SAMPLE LEVEL ---
+project.sample.level.unknown=Unknown sample code level: {0}. Supported values: none, basic, rich
+# ================================
+# === APPLICATION LAYER MESSAGES ===
+# ================================
+application.artifact.key.unknown=Unknown artifact key ''{0}''.
+# ================================
+# === ADAPTER / BOOTSTRAP MESSAGES ===
+# ================================
+# --- TEMPLATE / PROFILE / ARTIFACT ---
+adapter.template.render.failed=Failed to render template ''{0}''.
+bootstrap.profile.not.found=Unknown profile: {0}
+bootstrap.artifact.not.found=Unknown artifact ''{0}'' for profile: {1}
+bootstrap.template.base.missing=template-base-path must be set for profile: {0}
+adapter.artifacts.port.not.found=No artifact generator adapter registered for profile ''{0}''.
+adapter.profile.unsupported=Unsupported profile combination: framework={0}, buildTool={1}, language={2}
+adapter.generator.key.mismatch=Generator artifact key mismatch (expected ''{0}'', actual ''{1}'').
+# --- CLI / INPUT VALIDATION ---
+adapter.dependency.alias.unknown=Unknown dependency alias ''{0}''. Use --help to see supported values.
+# --- PROJECT WRITE / ROOT / ARCHIVE ---
+adapter.project.write.failed=Failed to write generated file ''{0}''.
+adapter.project-root.not-directory=Project root ''{0}'' exists but is not a directory.
+adapter.project-root.already-exists=Project root ''{0}'' already exists.
+adapter.project-root.io.failed=Failed to prepare project root at ''{0}''.
+adapter.project.archive.invalid.root=Invalid project root ''{0}''.
+adapter.project.archive.io=I/O error while archiving project ''{0}''.
+# Sample code adapter
+adapter.sample-code.templates.not-found=No sample code templates found under ''{0}'' for layout ''{1}'' and level ''{2}''.
+adapter.sample-code.templates.scan.failed=Failed to scan sample code templates under ''{0}''.
+adapter.sample-code.level.unsupported=Sample code level ''{0}'' is not supported by the current adapter.
\ No newline at end of file
diff --git a/src/main/resources/templates/springBootApplication.yml.ftl b/src/main/resources/templates/springBootApplication.yml.ftl
deleted file mode 100644
index 3525bb9..0000000
--- a/src/main/resources/templates/springBootApplication.yml.ftl
+++ /dev/null
@@ -1,8 +0,0 @@
-spring:
-application:
-name: ${projectName}
-# server:
-# port: 8080
-# logging:
-# level:
-# root: INFO
\ No newline at end of file
diff --git a/src/main/resources/templates/springBootJavaPom.xml.ftl b/src/main/resources/templates/springBootJavaPom.xml.ftl
deleted file mode 100644
index 4eef2c8..0000000
--- a/src/main/resources/templates/springBootJavaPom.xml.ftl
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
- ${pom.modelVersion}
-
-
- org.springframework.boot
- spring-boot-starter-parent
- ${pom.projectMetadata.springBootVersion}
-
-
-
- ${pom.projectMetadata.groupId}
- ${pom.projectMetadata.artifactId}
- ${pom.version}
-
-
- ${pom.projectMetadata.javaVersion}
-
-
-
- <#list pom.dependencies as dependency>
-
- ${dependency.groupId}
- ${dependency.artifactId}
- <#if dependency.version??>
- ${dependency.version}
- #if>
- <#if dependency.scope??>
- ${dependency.scope}
- #if>
-
- #list>
-
-
-
-
- <#list pom.plugins as plugin>
-
- ${plugin.groupId}
- ${plugin.artifactId}
- <#if plugin.version??>
- ${plugin.version}
- #if>
- <#if plugin.configuration?has_content && plugin.configuration?size gt 0>
-
- <#list plugin.configuration! {} as key, value>
- <#if key != 'compileSourceRoots'>
- <${key}>${value}${key}>
- #if>
- #list>
- <#if plugin.configuration.compileSourceRoots?has_content && plugin.configuration.compileSourceRoots?size gt 0>
-
- <#list plugin.configuration.compileSourceRoots! {} as sourceDirectory>
- ${sourceDirectory}
- #list>
-
- #if>
-
- #if>
-
- #list>
-
-
-
-
\ No newline at end of file
diff --git a/src/main/resources/templates/springBootMavenJavaReadMe.ftl b/src/main/resources/templates/springBootMavenJavaReadMe.ftl
deleted file mode 100644
index ff10dfb..0000000
--- a/src/main/resources/templates/springBootMavenJavaReadMe.ftl
+++ /dev/null
@@ -1,53 +0,0 @@
-Project Initialization
-
-Extract the downloaded archive: Use a tool like WinZip, 7-Zip, or the unzip command to extract the downloaded archive file (e.g., ${projectName}.zip).
-
-Navigate to the project directory: Open your terminal or command prompt and navigate to the extracted project directory using the cd command (e.g., cd ${projectName}).
-
-Running the project:
-
-Option 1: With Maven (Recommended):
-
-If you already have Maven installed, you can directly use standard Maven commands like mvn package or mvn test to build and potentially run the project (refer to the project documentation for specific commands on how to run the application).
-
-Option 2: Without Maven:
-
-Pre-built Version (if available): The project might offer pre-built versions that include the mvnw and mvnw.cmd scripts for running the project without requiring Maven installation. Check the project website or documentation for download instructions for a pre-built version (if available).
-
-Build Scripts and Run the Project (if source code available):
-
-1. Download a minimal Apache Maven version from the official website: https://maven.apache.org/download.cgi
-2. Extract the downloaded Maven archive into a temporary directory.
-3. Open a terminal window (command prompt on Windows).
-4. Navigate to the extracted project's root directory using the cd command.
-5. (Optional) Run mvn package to see the build process and downloaded dependencies.
-6. Generate the wrapper scripts using a specific Maven command (check project documentation for the exact command, a common example might be: mvn wrapper:wrapper).
-7. After running the script generation command, check the project's root directory for the newly generated scripts: mvnw (Linux/macOS) and mvnw.cmd (Windows).
-8. Run the Project:
-- Linux/macOS: With the mvnw script generated, execute the following command in the terminal (assuming the script is executable): ./mvnw
-
- - Windows: With the mvnw.cmd script generated, double-click the script or run it from the command prompt: mvnw.cmd
-
-
- Replace
- with the desired action you want to perform. Here are some common examples:
-
- ./mvnw package: Builds the project and creates a package (JAR file).
- ./mvnw test: Runs unit tests for the project.
-
- Dependencies
-
- This project uses the following dependencies based on your selections during generation:
-
- (List generated dependencies here. You can access this information from the project's pom.xml file)
- Project Structure
-
- pom.xml: This file defines the project's metadata (groupId, artifactId, version) and dependencies.
- wrapper/: This directory stores the configuration for the Maven Wrapper.
- wrapper.conf: Configuration file for the wrapper executable.
- src/main/java/: Source code directory for your application's Java classes.
- src/main/resources/: Configuration files and resources used by your application.
- src/test/java/: Source code directory for your application's unit tests (optional).
- src/gen/java/: Generated code directory for the Codegen's Java classes.
-
- Additional Notes
\ No newline at end of file
diff --git a/src/main/resources/templates/springBootMainClass.java.ftl b/src/main/resources/templates/springboot/maven/java/MainClass.java.ftl
similarity index 66%
rename from src/main/resources/templates/springBootMainClass.java.ftl
rename to src/main/resources/templates/springboot/maven/java/MainClass.java.ftl
index 4b7565b..3016fe2 100644
--- a/src/main/resources/templates/springBootMainClass.java.ftl
+++ b/src/main/resources/templates/springboot/maven/java/MainClass.java.ftl
@@ -6,8 +6,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ${className} {
-public static void main(String[] args) {
-SpringApplication.run(${className}.class, args);
-}
-
-}
+ public static void main(String[] args) {
+ SpringApplication.run(${className}.class, args);
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/templates/springBootTestClass.java.ftl b/src/main/resources/templates/springboot/maven/java/MainClassTests.java.ftl
similarity index 80%
rename from src/main/resources/templates/springBootTestClass.java.ftl
rename to src/main/resources/templates/springboot/maven/java/MainClassTests.java.ftl
index dcc1b84..e6362a9 100644
--- a/src/main/resources/templates/springBootTestClass.java.ftl
+++ b/src/main/resources/templates/springboot/maven/java/MainClassTests.java.ftl
@@ -6,9 +6,9 @@ import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ${className} {
-@Test
-void contextLoads() {
-}
+ @Test
+ void contextLoads() {
+ }
}
diff --git a/src/main/resources/templates/springboot/maven/java/README.md.ftl b/src/main/resources/templates/springboot/maven/java/README.md.ftl
new file mode 100644
index 0000000..6bfaf95
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/README.md.ftl
@@ -0,0 +1,124 @@
+<#-- README Generator Template -->
+
+# ${projectName}
+
+${projectDescription}
+
+---
+
+## 🔧 Tech Stack
+
+| Category | Value |
+|---------|------|
+| **Framework** | ${framework} |
+| **Language** | ${language} |
+| **Build Tool** | ${buildTool} |
+| **Java** | ${javaVersion} |
+| **Spring Boot** | ${springBootVersion} |
+
+---
+
+## 📦 Coordinates
+
+| Key | Value |
+|-----|------|
+| `groupId` | `${groupId}` |
+| `artifactId` | `${artifactId}` |
+| `package` | `${packageName}` |
+
+---
+
+## 🚀 Quick Start
+
+```bash
+./mvnw clean package # Build (using wrapper)
+./mvnw spring-boot:run # Run the application
+```
+
+> If Maven is installed globally, you may also use `mvn` instead of `./mvnw`.
+
+---
+
+## 📁 Project Layout
+
+```
+src
+├─ main
+│ ├─ java/${packageName?replace('.', '/')}
+│ └─ resources/
+└─ test
+└─ java/${packageName?replace('.', '/')}
+```
+
+<#-- Optional hexagonal showcase -->
+<#if hasHexSample?? && hasHexSample>
+---
+
+## 🧱 Hexagonal Architecture Example
+
+This project was generated with the optional **hexagonal layout**:
+
+```bash
+--layout hexagonal
+```
+
+With this flag, your `src/main/java/${packageName?replace('.', '/')}` tree is structured for:
+
+* `domain` – core business rules (no Spring dependencies)
+* `application` – use cases orchestrating ports
+* `adapter` – inbound & outbound adapters
+* `bootstrap` – configuration and wiring
+
+If you also enabled **sample code**:
+
+```bash
+--sample-code basic
+```
+
+then the project includes a minimal but complete **greeting flow** wired end-to-end:
+
+* **Domain & ports** – greeting model and port contracts
+* **Application** – greeting use case orchestration
+* **REST adapter** – sample controller exposing:
+
+```bash
+GET /api/v1/sample/greetings/default
+→ 200 OK
+{
+"text": "Hello from hexagonal sample!"
+}
+```
+
+You can use this sample in two ways:
+
+* As a **teaching reference** for hexagonal structure in this codebase
+* As a **starting slice** to evolve into your real business modules
+
+#if>
+
+---
+
+## 📚 Selected Dependencies
+
+<#if dependencies?has_content>
+| Dependency | Scope |
+|-----------|-------|
+<#list dependencies as d>
+| `${d.groupId}:${d.artifactId}`<#if d.version?? && d.version?has_content>:`${d.version}`#if> | <#if d.scope?? && d.scope?has_content>${d.scope}<#else>default#if> |
+#list>
+<#else>
+> No additional dependencies were selected.
+#if>
+
+---
+
+## 🧩 Next Steps
+
+* ✔ Structure your domain and use case logic
+* ✔ Add CI/CD pipelines or Docker support
+* ✔ Configure profiles in `application.yml`
+* ✔ Add more Spring Boot starters if needed
+
+---
+
+🏗 Generated by **Blueprint Platform — Codegen Blueprint CLI**
diff --git a/src/main/resources/templates/springboot/maven/java/application.yml.ftl b/src/main/resources/templates/springboot/maven/java/application.yml.ftl
new file mode 100644
index 0000000..31e2c48
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/application.yml.ftl
@@ -0,0 +1,8 @@
+spring:
+ application:
+ name: ${applicationName}
+ # server:
+ # port: 8080
+ # logging:
+ # level:
+ # root: INFO
\ No newline at end of file
diff --git a/src/main/resources/templates/gitignore.ftl b/src/main/resources/templates/springboot/maven/java/gitignore.ftl
similarity index 100%
rename from src/main/resources/templates/gitignore.ftl
rename to src/main/resources/templates/springboot/maven/java/gitignore.ftl
diff --git a/src/main/resources/templates/maven-wrapper.properties.ftl b/src/main/resources/templates/springboot/maven/java/maven-wrapper.properties.ftl
similarity index 100%
rename from src/main/resources/templates/maven-wrapper.properties.ftl
rename to src/main/resources/templates/springboot/maven/java/maven-wrapper.properties.ftl
diff --git a/src/main/resources/templates/springboot/maven/java/pom.xml.ftl b/src/main/resources/templates/springboot/maven/java/pom.xml.ftl
new file mode 100644
index 0000000..50ad8eb
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/pom.xml.ftl
@@ -0,0 +1,50 @@
+
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ ${springBootVersion}
+
+
+
+ ${groupId}
+ ${artifactId}
+ 0.0.1-SNAPSHOT
+
+ ${projectName}
+ ${projectDescription}
+
+
+ ${javaVersion}
+
+
+
+ <#list dependencies as d>
+
+ ${d.groupId}
+ ${d.artifactId}
+ <#if d.version?? && d.version?has_content>
+ ${d.version}
+ #if>
+ <#if d.scope?? && d.scope?has_content>
+ ${d.scope}
+ #if>
+
+ #list>
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingController.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingController.java.ftl
new file mode 100644
index 0000000..5731012
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingController.java.ftl
@@ -0,0 +1,61 @@
+package ${projectPackageName}.adapter.sample.greeting.in.rest;
+
+import ${projectPackageName}.application.sample.greeting.usecase.GetGreetingQuery;
+import ${projectPackageName}.application.sample.greeting.usecase.GetGreetingResult;
+import ${projectPackageName}.application.sample.greeting.usecase.GetGreetingUseCase;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Inbound REST adapter for Greeting sample.
+ * Responsibilities:
+ * - translate HTTP requests into queries
+ * - invoke the application layer (use cases)
+ * - translate results into HTTP responses
+ */
+@RestController
+@RequestMapping(
+ path = "/api/v1/sample/greetings",
+ produces = MediaType.APPLICATION_JSON_VALUE
+)
+public class GreetingController {
+
+ private final GetGreetingUseCase getGreetingUseCase;
+ private final GreetingResponseMapper responseMapper;
+
+ public GreetingController(
+ GetGreetingUseCase getGreetingUseCase,
+ GreetingResponseMapper responseMapper) {
+ this.getGreetingUseCase = getGreetingUseCase;
+ this.responseMapper = responseMapper;
+ }
+
+ /**
+ * GET /api/v1/sample/greetings/default
+ *
+ * Returns a default greeting from the domain.
+ */
+ @GetMapping("/default")
+ public ResponseEntity getDefaultGreeting() {
+ GetGreetingResult result = getGreetingUseCase.getDefault();
+ return ResponseEntity.ok(responseMapper.from(result));
+ }
+
+ /**
+ * GET /api/v1/sample/greetings
+ * Example: /api/v1/sample/greetings?name=John
+ *
+ * Creates a greeting personalized with a given name.
+ */
+ @GetMapping
+ public ResponseEntity getPersonalGreeting(
+ @RequestParam(name = "name", required = false) String name) {
+ GetGreetingResult result =
+ getGreetingUseCase.getPersonal(new GetGreetingQuery(name));
+ return ResponseEntity.ok(responseMapper.from(result));
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingResponse.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingResponse.java.ftl
new file mode 100644
index 0000000..42adc03
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingResponse.java.ftl
@@ -0,0 +1,11 @@
+package ${projectPackageName}.adapter.sample.greeting.in.rest;
+
+/**
+ * REST response DTO for the Greeting sample.
+ * Kept transport-friendly:
+ * - id is a String (UUID.toString())
+ */
+public record GreetingResponse(
+ String id,
+ String text
+) {}
\ No newline at end of file
diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingResponseMapper.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingResponseMapper.java.ftl
new file mode 100644
index 0000000..9022b4f
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingResponseMapper.java.ftl
@@ -0,0 +1,22 @@
+package ${projectPackageName}.adapter.sample.greeting.in.rest;
+
+import ${projectPackageName}.application.sample.greeting.usecase.GetGreetingResult;
+
+/**
+ * Maps application-layer result to REST response DTO.
+ * GetGreetingResult → GreetingResponse
+ * Kept free of Spring annotations to stay framework-agnostic.
+ * It will be wired via a bootstrap @Configuration class.
+ */
+public class GreetingResponseMapper {
+
+ public GreetingResponse from(GetGreetingResult result) {
+ if (result == null) {
+ return null;
+ }
+ return new GreetingResponse(
+ result.id() != null ? result.id().toString() : null,
+ result.text()
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/out/logging/LoggingGreetingAuditAdapter.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/out/logging/LoggingGreetingAuditAdapter.java.ftl
new file mode 100644
index 0000000..5bb714f
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/out/logging/LoggingGreetingAuditAdapter.java.ftl
@@ -0,0 +1,34 @@
+package ${projectPackageName}.adapter.sample.greeting.out.logging;
+
+import ${projectPackageName}.domain.sample.greeting.model.Greeting;
+import ${projectPackageName}.domain.sample.greeting.port.out.GreetingAuditPort;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Outbound adapter that implements {@link GreetingAuditPort} by logging events.
+ * Hexagonal intent:
+ * - Domain only knows about GreetingAuditPort
+ * - Infrastructure decides how to implement it (here: logging)
+ * This class is intentionally kept free of Spring annotations.
+ * It will be registered as a bean from a bootstrap @Configuration class.
+ */
+public class LoggingGreetingAuditAdapter implements GreetingAuditPort {
+
+ private static final Logger log = LoggerFactory.getLogger(LoggingGreetingAuditAdapter.class);
+
+ @Override
+ public void auditCreated(Greeting greeting) {
+ // Very simple implementation for the BASIC sample:
+ // just log that a greeting was created.
+ if (greeting == null) {
+ log.warn("GreetingAuditPort.auditCreated invoked with null Greeting");
+ return;
+ }
+
+ log.info("Greeting created: id={}, text={}",
+ greeting.id(),
+ greeting.text().value()
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingHandler.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingHandler.java.ftl
new file mode 100644
index 0000000..ba3f629
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingHandler.java.ftl
@@ -0,0 +1,50 @@
+package ${projectPackageName}.application.sample.greeting.usecase;
+
+import ${projectPackageName}.domain.sample.greeting.model.Greeting;
+import ${projectPackageName}.domain.sample.greeting.port.out.GreetingAuditPort;
+import ${projectPackageName}.domain.sample.greeting.service.GreetingService;
+
+/**
+ * Application-layer handler for the GetGreeting use case.
+ * Responsibilities:
+ * - orchestrate domain services
+ * - call outbound ports (e.g. audit)
+ * - map domain objects to use case result DTOs
+ */
+public class GetGreetingHandler implements GetGreetingUseCase {
+
+ private final GreetingService greetingService;
+ private final GreetingAuditPort auditPort;
+
+ public GetGreetingHandler(GreetingService greetingService,
+ GreetingAuditPort auditPort) {
+ this.greetingService = greetingService;
+ this.auditPort = auditPort;
+ }
+
+ @Override
+ public GetGreetingResult getDefault() {
+ Greeting greeting = greetingService.defaultGreeting();
+ auditPort.auditCreated(greeting);
+ return map(greeting);
+ }
+
+ @Override
+ public GetGreetingResult getPersonal(GetGreetingQuery query) {
+ Greeting greeting;
+ if (query == null || query.name() == null) {
+ greeting = greetingService.defaultGreeting();
+ } else {
+ greeting = greetingService.createPersonalGreeting(query.name());
+ }
+ auditPort.auditCreated(greeting);
+ return map(greeting);
+ }
+
+ private GetGreetingResult map(Greeting greeting) {
+ return GetGreetingResult.of(
+ greeting.id().value(),
+ greeting.text().value()
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingQuery.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingQuery.java.ftl
new file mode 100644
index 0000000..8f5b989
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingQuery.java.ftl
@@ -0,0 +1,14 @@
+package ${projectPackageName}.application.sample.greeting.usecase;
+
+/**
+ * Input model for the GetGreeting use case.
+ * Intentionally tiny:
+ * - shows how even simple use cases use a dedicated input type
+ * - can evolve (e.g. locale, channel, tenant) without breaking the interface
+ */
+public record GetGreetingQuery(String name) {
+
+ public static GetGreetingQuery anonymous() {
+ return new GetGreetingQuery(null);
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingResult.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingResult.java.ftl
new file mode 100644
index 0000000..0ceee92
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingResult.java.ftl
@@ -0,0 +1,16 @@
+package ${projectPackageName}.application.sample.greeting.usecase;
+
+import java.util.UUID;
+
+/**
+ * Output model for the GetGreeting use case.
+ * Hexagonal intent:
+ * - application returns a simple, serializable shape
+ * - adapters (REST, CLI, etc.) can map this to their own DTOs
+ */
+public record GetGreetingResult(UUID id, String text) {
+
+ public static GetGreetingResult of(UUID id, String text) {
+ return new GetGreetingResult(id, text);
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingUseCase.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingUseCase.java.ftl
new file mode 100644
index 0000000..4bba29a
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/application/sample/greeting/usecase/GetGreetingUseCase.java.ftl
@@ -0,0 +1,14 @@
+package ${projectPackageName}.application.sample.greeting.usecase;
+
+/**
+ * Application-level use case for retrieving greetings.
+ * Kept small and focused on intent:
+ * - default greeting
+ * - personalized greeting
+ */
+public interface GetGreetingUseCase {
+
+ GetGreetingResult getDefault();
+
+ GetGreetingResult getPersonal(GetGreetingQuery query);
+}
\ No newline at end of file
diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/bootstrap/sample/greeting/GreetingSampleConfig.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/bootstrap/sample/greeting/GreetingSampleConfig.java.ftl
new file mode 100644
index 0000000..4344f9f
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/bootstrap/sample/greeting/GreetingSampleConfig.java.ftl
@@ -0,0 +1,57 @@
+package ${projectPackageName}.bootstrap.sample.greeting;
+
+import ${projectPackageName}.adapter.sample.greeting.in.rest.GreetingResponseMapper;
+import ${projectPackageName}.adapter.sample.greeting.out.logging.LoggingGreetingAuditAdapter;
+import ${projectPackageName}.application.sample.greeting.usecase.GetGreetingHandler;
+import ${projectPackageName}.application.sample.greeting.usecase.GetGreetingUseCase;
+import ${projectPackageName}.domain.sample.greeting.port.out.GreetingAuditPort;
+import ${projectPackageName}.domain.sample.greeting.service.GreetingService;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Bootstrap wiring for the Greeting BASIC sample.
+ * Principles:
+ * - Beans are exposed by their *port* types (interfaces)
+ * - Implementation classes stay framework-agnostic
+ * - Bean names reflect concrete roles
+ */
+@Configuration
+public class GreetingSampleConfig {
+
+ @Bean
+ GreetingService greetingService() {
+ return new GreetingService();
+ }
+
+ /**
+ * Outbound audit port wiring.
+ * Return type = port interface (Hexagonal rule)
+ * Bean name = concrete role (LoggingGreetingAuditAdapter)
+ */
+ @Bean
+ GreetingAuditPort loggingGreetingAuditAdapter() {
+ return new LoggingGreetingAuditAdapter();
+ }
+
+ /**
+ * Use case wiring.
+ * Return type = use case interface
+ * Bean name = concrete handler implementation
+ */
+ @Bean
+ GetGreetingUseCase getGreetingHandler(
+ GreetingService greetingService,
+ GreetingAuditPort loggingGreetingAuditAdapter
+ ) {
+ return new GetGreetingHandler(greetingService, loggingGreetingAuditAdapter);
+ }
+
+ /**
+ * REST adapter helper (mapper), kept free of Spring annotations.
+ */
+ @Bean
+ GreetingResponseMapper greetingResponseMapper() {
+ return new GreetingResponseMapper();
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/Greeting.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/Greeting.java.ftl
new file mode 100644
index 0000000..f365433
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/Greeting.java.ftl
@@ -0,0 +1,46 @@
+package ${projectPackageName}.domain.sample.greeting.model;
+
+import ${projectPackageName}.domain.sample.greeting.model.value.GreetingId;
+import ${projectPackageName}.domain.sample.greeting.model.value.GreetingText;
+
+/**
+ * Aggregate root for the Greeting sample.
+ * Intent:
+ * - show a small, self-contained aggregate
+ * - keep invariants inside value objects
+ * - keep this class as a simple composition of those values
+ */
+public class Greeting {
+
+ private final GreetingId id;
+ private final GreetingText text;
+
+ private Greeting(GreetingId id, GreetingText text) {
+ this.id = id;
+ this.text = text;
+ }
+
+ public static Greeting of(GreetingId id, GreetingText text) {
+ // Invariants are already enforced in value objects
+ return new Greeting(id, text);
+ }
+
+ public static Greeting createDefault() {
+ return new Greeting(
+ GreetingId.newId(),
+ GreetingText.of("Hello from hexagonal sample!")
+ );
+ }
+
+ public GreetingId id() {
+ return id;
+ }
+
+ public GreetingText text() {
+ return text;
+ }
+
+ public String render() {
+ return text.value();
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/value/GreetingId.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/value/GreetingId.java.ftl
new file mode 100644
index 0000000..7a2b655
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/value/GreetingId.java.ftl
@@ -0,0 +1,27 @@
+package ${projectPackageName}.domain.sample.greeting.model.value;
+
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Strongly-typed identifier for the Greeting aggregate.
+ */
+public record GreetingId(UUID value) {
+
+ public GreetingId {
+ Objects.requireNonNull(value, "value must not be null");
+ }
+
+ public static GreetingId newId() {
+ return new GreetingId(UUID.randomUUID());
+ }
+
+ public static GreetingId fromString(String raw) {
+ return new GreetingId(UUID.fromString(raw));
+ }
+
+ @Override
+ public String toString() {
+ return value.toString();
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/value/GreetingText.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/value/GreetingText.java.ftl
new file mode 100644
index 0000000..0d51842
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/model/value/GreetingText.java.ftl
@@ -0,0 +1,34 @@
+package ${projectPackageName}.domain.sample.greeting.model.value;
+
+import java.util.Objects;
+
+/**
+ * Value Object capturing the greeting text with basic invariants.
+ * Kept intentionally small and self-contained, to show:
+ * - trimming
+ * - length invariant
+ * - not-blank constraint
+ */
+public record GreetingText(String value) {
+
+ private static final int MAX_LENGTH = 140;
+
+ public GreetingText {
+ Objects.requireNonNull(value, "value must not be null");
+
+ String trimmed = value.trim();
+ if (trimmed.isEmpty()) {
+ throw new IllegalArgumentException("Greeting text must not be blank");
+ }
+ if (trimmed.length() > MAX_LENGTH) {
+ throw new IllegalArgumentException(
+ "Greeting text must not exceed " + MAX_LENGTH + " characters"
+ );
+ }
+ value = trimmed;
+ }
+
+ public static GreetingText of(String raw) {
+ return new GreetingText(raw);
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/port/out/GreetingAuditPort.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/port/out/GreetingAuditPort.java.ftl
new file mode 100644
index 0000000..0f49097
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/port/out/GreetingAuditPort.java.ftl
@@ -0,0 +1,18 @@
+package ${projectPackageName}.domain.sample.greeting.port.out;
+
+import ${projectPackageName}.domain.sample.greeting.model.Greeting;
+
+/**
+ * Outbound port for auditing greeting creations.
+ * Hexagonal intent:
+ * - Domain expresses the need for persistence/logging
+ * - Without depending on any technology (DB, File, Kafka, etc.)
+ */
+public interface GreetingAuditPort {
+
+ /**
+ * Publish or persist that a Greeting was created.
+ * Infrastructure (adapter) decides how (log, file, DB, etc.)
+ */
+ void auditCreated(Greeting greeting);
+}
\ No newline at end of file
diff --git a/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/service/GreetingService.java.ftl b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/service/GreetingService.java.ftl
new file mode 100644
index 0000000..a55c378
--- /dev/null
+++ b/src/main/resources/templates/springboot/maven/java/sample/hexagonal/basic/domain/sample/greeting/service/GreetingService.java.ftl
@@ -0,0 +1,47 @@
+package ${projectPackageName}.domain.sample.greeting.service;
+
+import ${projectPackageName}.domain.sample.greeting.model.Greeting;
+import ${projectPackageName}.domain.sample.greeting.model.value.GreetingId;
+import ${projectPackageName}.domain.sample.greeting.model.value.GreetingText;
+
+/**
+ * Domain service for the Greeting aggregate.
+ * Intent:
+ * - keep aggregate + value objects small and focused
+ * - provide simple creation/use flows that can evolve over time
+ */
+public class GreetingService {
+
+ /**
+ * Returns a default greeting using the aggregate factory.
+ */
+ public Greeting defaultGreeting() {
+ return Greeting.createDefault();
+ }
+
+ /**
+ * Creates a Greeting from a raw text value.
+ * All invariants are enforced by {@link GreetingText}.
+ */
+ public Greeting createGreeting(String rawText) {
+ return Greeting.of(
+ GreetingId.newId(),
+ GreetingText.of(rawText)
+ );
+ }
+
+ /**
+ * Creates a simple "Hello, !" style greeting.
+ * Shows a tiny bit of orchestration logic in the domain layer.
+ */
+ public Greeting createPersonalGreeting(String name) {
+ String safeName = (name == null) ? "there" : name.trim();
+ if (safeName.isEmpty()) {
+ safeName = "there";
+ }
+ return Greeting.of(
+ GreetingId.newId(),
+ GreetingText.of("Hello, " + safeName + "!")
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/io/github/blueprintplatform/codegen/CodegenBlueprintApplicationIT.java b/src/test/java/io/github/blueprintplatform/codegen/CodegenBlueprintApplicationIT.java
new file mode 100644
index 0000000..80c50a0
--- /dev/null
+++ b/src/test/java/io/github/blueprintplatform/codegen/CodegenBlueprintApplicationIT.java
@@ -0,0 +1,18 @@
+package io.github.blueprintplatform.codegen;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+@Tag("integration")
+@DisplayName("Integration Test: Spring Context Bootstrapping")
+class CodegenBlueprintApplicationIT {
+
+ @Test
+ @DisplayName("Spring context should load successfully")
+ void contextLoads() {
+ // If context fails to load, the test will fail automatically.
+ }
+}
diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/SpringBootGenerateCommandTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/SpringBootGenerateCommandTest.java
new file mode 100644
index 0000000..5b9b41e
--- /dev/null
+++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/in/cli/springboot/SpringBootGenerateCommandTest.java
@@ -0,0 +1,115 @@
+package io.github.blueprintplatform.codegen.adapter.in.cli.springboot;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.github.blueprintplatform.codegen.adapter.in.cli.CliProjectRequest;
+import io.github.blueprintplatform.codegen.adapter.in.cli.springboot.dependency.SpringBootDependencyAlias;
+import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectCommand;
+import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectResult;
+import io.github.blueprintplatform.codegen.application.usecase.project.CreateProjectUseCase;
+import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeLevel;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language;
+import java.nio.file.Path;
+import java.util.List;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("unit")
+@Tag("adapter")
+class SpringBootGenerateCommandTest {
+
+ @Test
+ @DisplayName("call() should build profile, map layout & dependencies and invoke use case")
+ void call_shouldBuildProfileAndInvokeUseCase() {
+ var mapper = new RecordingMapper();
+ var useCase = new StubCreateProjectUseCase();
+
+ var cmd = new SpringBootGenerateCommand(mapper, useCase);
+
+ cmd.groupId = "com.acme";
+ cmd.artifactId = "demo-app";
+ cmd.name = "Demo App";
+ cmd.description = "Demo application for Acme";
+ cmd.packageName = "com.acme.demo";
+ cmd.buildTool = BuildTool.MAVEN;
+ cmd.language = Language.JAVA;
+ cmd.javaVersion = JavaVersion.JAVA_21;
+ cmd.bootVersion = SpringBootVersion.V3_5;
+
+ cmd.layout = ProjectLayout.STANDARD;
+ cmd.sampleCode = SampleCodeLevel.NONE;
+ cmd.dependencies = List.of(SpringBootDependencyAlias.WEB);
+ Path expected = Path.of(".");
+ cmd.targetDirectory = expected;
+
+ Integer exitCode = cmd.call();
+
+ assertThat(exitCode).isZero();
+
+ assertThat(mapper.lastRequest).isNotNull();
+ assertThat(mapper.lastBuildTool).isEqualTo(BuildTool.MAVEN);
+ assertThat(mapper.lastLanguage).isEqualTo(Language.JAVA);
+ assertThat(mapper.lastJavaVersion).isEqualTo(JavaVersion.JAVA_21);
+ assertThat(mapper.lastBootVersion).isEqualTo(SpringBootVersion.V3_5);
+
+ assertThat(mapper.lastRequest.groupId()).isEqualTo("com.acme");
+ assertThat(mapper.lastRequest.artifactId()).isEqualTo("demo-app");
+ assertThat(mapper.lastRequest.name()).isEqualTo("Demo App");
+ assertThat(mapper.lastRequest.description()).isEqualTo("Demo application for Acme");
+ assertThat(mapper.lastRequest.packageName()).isEqualTo("com.acme.demo");
+ assertThat(mapper.lastRequest.targetDirectory()).isEqualTo(expected);
+
+ assertThat(mapper.lastRequest.profile()).isEqualTo("springboot-maven-java");
+
+ assertThat(mapper.lastRequest.layoutKey()).isEqualTo(ProjectLayout.STANDARD.key());
+ assertThat(mapper.lastRequest.sampleCodeLevelKey()).isEqualTo(SampleCodeLevel.NONE.key());
+
+ assertThat(mapper.lastRequest.dependencies())
+ .containsExactly(SpringBootDependencyAlias.WEB.name());
+
+ assertThat(useCase.lastCommand).isSameAs(mapper.returnedCommand);
+ }
+
+ static class RecordingMapper extends CreateProjectCommandMapper {
+
+ final CreateProjectCommand returnedCommand = null;
+ CliProjectRequest lastRequest;
+ BuildTool lastBuildTool;
+ Language lastLanguage;
+ JavaVersion lastJavaVersion;
+ SpringBootVersion lastBootVersion;
+
+ @Override
+ public CreateProjectCommand from(
+ CliProjectRequest request,
+ BuildTool buildTool,
+ Language language,
+ JavaVersion javaVersion,
+ SpringBootVersion bootVersion) {
+
+ this.lastRequest = request;
+ this.lastBuildTool = buildTool;
+ this.lastLanguage = language;
+ this.lastJavaVersion = javaVersion;
+ this.lastBootVersion = bootVersion;
+
+ return returnedCommand;
+ }
+ }
+
+ static class StubCreateProjectUseCase implements CreateProjectUseCase {
+
+ CreateProjectCommand lastCommand;
+
+ @Override
+ public CreateProjectResult handle(CreateProjectCommand command) {
+ this.lastCommand = command;
+ return new CreateProjectResult(Path.of("demo-app.zip"));
+ }
+ }
+}
diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependencyMapperTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependencyMapperTest.java
new file mode 100644
index 0000000..fa1f4c4
--- /dev/null
+++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/build/maven/shared/PomDependencyMapperTest.java
@@ -0,0 +1,87 @@
+package io.github.blueprintplatform.codegen.adapter.out.build.maven.shared;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyCoordinates;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyScope;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("unit")
+@Tag("adapter")
+class PomDependencyMapperTest {
+
+ private final PomDependencyMapper mapper = new PomDependencyMapper();
+
+ private static Dependency dep(
+ String groupId, String artifactId, String version, DependencyScope scope) {
+ DependencyVersion v = (version == null) ? null : new DependencyVersion(version);
+ return new Dependency(
+ new DependencyCoordinates(new GroupId(groupId), new ArtifactId(artifactId)), v, scope);
+ }
+
+ @Test
+ @DisplayName("from(empty Dependencies) should return empty list")
+ void from_emptyDependencies_shouldReturnEmptyList() {
+ Dependencies deps = Dependencies.of(List.of());
+
+ List result = mapper.from(deps);
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ @DisplayName("from(Dependencies) should map coordinates, optional version and scope correctly")
+ void from_dependencies_shouldMapFieldsCorrectly() {
+ Dependency d1 = dep("org.acme", "alpha", null, null);
+ Dependency d2 = dep("org.acme", "beta", "1.2.3", DependencyScope.RUNTIME);
+ Dependency d3 = dep("org.acme", "gamma", "2.0.0-RC1", DependencyScope.TEST);
+
+ Dependencies deps = Dependencies.of(List.of(d1, d2, d3));
+
+ List result = mapper.from(deps);
+
+ assertThat(result).hasSize(3);
+
+ Map byArtifactId =
+ result.stream().collect(Collectors.toMap(PomDependency::artifactId, p -> p));
+
+ PomDependency alpha = byArtifactId.get("alpha");
+ assertThat(alpha.groupId()).isEqualTo("org.acme");
+ assertThat(alpha.version()).isNull();
+ assertThat(alpha.scope()).isNull();
+
+ PomDependency beta = byArtifactId.get("beta");
+ assertThat(beta.groupId()).isEqualTo("org.acme");
+ assertThat(beta.version()).isEqualTo("1.2.3");
+ assertThat(beta.scope()).isEqualTo("runtime");
+
+ PomDependency gamma = byArtifactId.get("gamma");
+ assertThat(gamma.groupId()).isEqualTo("org.acme");
+ assertThat(gamma.version()).isEqualTo("2.0.0-RC1");
+ assertThat(gamma.scope()).isEqualTo("test");
+ }
+
+ @Test
+ @DisplayName("from(Dependency) should map single dependency to PomDependency")
+ void from_singleDependency_shouldMapToPomDependency() {
+ Dependency domainDep =
+ dep("com.example", "demo-lib", "0.9.0-SNAPSHOT", DependencyScope.PROVIDED);
+
+ PomDependency pomDep = mapper.from(domainDep);
+
+ assertThat(pomDep.groupId()).isEqualTo("com.example");
+ assertThat(pomDep.artifactId()).isEqualTo("demo-lib");
+ assertThat(pomDep.version()).isEqualTo("0.9.0-SNAPSHOT");
+ assertThat(pomDep.scope()).isEqualTo("provided");
+ }
+}
diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectArchiverAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectArchiverAdapterTest.java
new file mode 100644
index 0000000..52129cd
--- /dev/null
+++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectArchiverAdapterTest.java
@@ -0,0 +1,107 @@
+package io.github.blueprintplatform.codegen.adapter.out.filesystem;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectArchiveInvalidRootException;
+import io.github.blueprintplatform.codegen.application.port.out.archive.ProjectArchiverPort;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+@Tag("unit")
+@Tag("adapter")
+class FileSystemProjectArchiverAdapterTest {
+
+ private final ProjectArchiverPort archiver = new FileSystemProjectArchiverAdapter();
+
+ private static List zipEntries(ZipFile zipFile) {
+ Enumeration extends ZipEntry> entries = zipFile.entries();
+ List names = new java.util.ArrayList<>();
+ while (entries.hasMoreElements()) {
+ names.add(entries.nextElement().getName());
+ }
+ return names;
+ }
+
+ @Test
+ @DisplayName("archive() should create zip with artifactId as root directory and include files")
+ void archive_shouldCreateZipWithArtifactIdRoot(@TempDir Path tempDir) throws IOException {
+ Path projectRoot = Files.createDirectory(tempDir.resolve("demo-app"));
+
+ Path mainJava = projectRoot.resolve("src/main/java");
+ Files.createDirectories(mainJava);
+ Files.writeString(mainJava.resolve("App.java"), "class App {}", StandardCharsets.UTF_8);
+
+ Path testJava = projectRoot.resolve("src/test/java");
+ Files.createDirectories(testJava);
+ Files.writeString(testJava.resolve("AppTest.java"), "class AppTest {}", StandardCharsets.UTF_8);
+
+ Path archivePath = archiver.archive(projectRoot, "my-artifact");
+
+ assertThat(archivePath).isEqualTo(tempDir.resolve("my-artifact.zip"));
+ assertThat(Files.exists(archivePath)).isTrue();
+
+ try (ZipFile zipFile = new ZipFile(archivePath.toFile())) {
+ List entryNames = zipEntries(zipFile);
+ assertThat(entryNames)
+ .contains("my-artifact/")
+ .anySatisfy(name -> assertThat(name).startsWith("my-artifact/src/"))
+ .contains("my-artifact/src/main/java/App.java")
+ .contains("my-artifact/src/test/java/AppTest.java");
+ }
+ }
+
+ @Test
+ @DisplayName("archive() should fall back to directory name when artifactId is null")
+ void archive_shouldUseDirectoryNameWhenArtifactIdNull(@TempDir Path tempDir) throws IOException {
+ Path projectRoot = Files.createDirectory(tempDir.resolve("demo-app"));
+
+ Files.writeString(projectRoot.resolve("README.md"), "# Demo", StandardCharsets.UTF_8);
+
+ Path archivePath = archiver.archive(projectRoot, null);
+
+ assertThat(archivePath).isEqualTo(tempDir.resolve("demo-app.zip"));
+ assertThat(Files.exists(archivePath)).isTrue();
+
+ try (ZipFile zipFile = new ZipFile(archivePath.toFile())) {
+ List entryNames = zipEntries(zipFile);
+ assertThat(entryNames).contains("demo-app/").contains("demo-app/README.md");
+ }
+ }
+
+ @Test
+ @DisplayName("archive() should throw ProjectArchiveInvalidRootException when root is null")
+ void archive_shouldThrowWhenRootIsNull() {
+ assertThatThrownBy(() -> archiver.archive(null, "anything"))
+ .isInstanceOf(ProjectArchiveInvalidRootException.class);
+ }
+
+ @Test
+ @DisplayName("archive() should throw ProjectArchiveInvalidRootException when root has no parent")
+ void archive_shouldThrowWhenRootHasNoParent(@TempDir Path tempDir) {
+ Path rootWithoutParent = tempDir.getRoot();
+
+ assertThatThrownBy(() -> archiver.archive(rootWithoutParent, "artifact"))
+ .isInstanceOf(ProjectArchiveInvalidRootException.class);
+ }
+
+ @Test
+ @DisplayName(
+ "archive() should throw ProjectArchiveInvalidRootException when path is not a directory")
+ void archive_shouldThrowWhenNotDirectory(@TempDir Path tempDir) throws IOException {
+ Path fileAsRoot = Files.createFile(tempDir.resolve("not-a-directory.txt"));
+
+ assertThatThrownBy(() -> archiver.archive(fileAsRoot, "artifact"))
+ .isInstanceOf(ProjectArchiveInvalidRootException.class);
+ }
+}
diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectRootAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectRootAdapterTest.java
new file mode 100644
index 0000000..316a3b0
--- /dev/null
+++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectRootAdapterTest.java
@@ -0,0 +1,86 @@
+package io.github.blueprintplatform.codegen.adapter.out.filesystem;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectRootAlreadyExistsException;
+import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectRootIOException;
+import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectRootNotDirectoryException;
+import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectRootExistencePolicy;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+@Tag("unit")
+@Tag("adapter")
+class FileSystemProjectRootAdapterTest {
+
+ private final FileSystemProjectRootAdapter adapter = new FileSystemProjectRootAdapter();
+ @TempDir Path tempDir;
+
+ @Test
+ @DisplayName("Should create directory when project root does not exist")
+ void shouldCreateDirectoryWhenNotExists() {
+ Path result =
+ adapter.prepareRoot(tempDir, "demo-app", ProjectRootExistencePolicy.FAIL_IF_EXISTS);
+
+ assertThat(result).exists().isDirectory();
+ assertThat(result.getFileName().toString()).hasToString("demo-app");
+ }
+
+ @Test
+ @DisplayName(
+ "Should throw ProjectRootAlreadyExistsException when directory exists and policy=FAIL_IF_EXISTS")
+ void shouldFailIfExists() throws IOException {
+ Path existing = tempDir.resolve("demo-app");
+ Files.createDirectories(existing);
+
+ assertThatThrownBy(
+ () ->
+ adapter.prepareRoot(tempDir, "demo-app", ProjectRootExistencePolicy.FAIL_IF_EXISTS))
+ .isInstanceOf(ProjectRootAlreadyExistsException.class);
+ }
+
+ @Test
+ @DisplayName("Should return directory when exists and policy=OVERWRITE")
+ void shouldReturnExistingDirWhenOverwrite() throws IOException {
+ Path existing = tempDir.resolve("demo-app");
+ Files.createDirectories(existing);
+
+ Path result = adapter.prepareRoot(tempDir, "demo-app", ProjectRootExistencePolicy.OVERWRITE);
+
+ assertThat(result).isEqualTo(existing);
+ }
+
+ @Test
+ @DisplayName("Should throw ProjectRootNotDirectoryException when exists and is a file")
+ void shouldThrowIfExistsButNotDirectory() throws IOException {
+ Path file = tempDir.resolve("demo-app");
+ Files.writeString(file, "not a directory");
+
+ assertThatThrownBy(
+ () ->
+ adapter.prepareRoot(tempDir, "demo-app", ProjectRootExistencePolicy.FAIL_IF_EXISTS))
+ .isInstanceOf(ProjectRootNotDirectoryException.class);
+ }
+
+ @Test
+ @DisplayName("Should wrap IO errors in ProjectRootIOException")
+ void shouldWrapIOException() throws IOException {
+ Path targetDir = Files.createTempDirectory("locked");
+ File dir = targetDir.toFile();
+
+ assertThat(dir.setReadable(true)).isTrue();
+ assertThat(dir.setWritable(false)).isTrue();
+ assertThat(dir.setExecutable(true)).isTrue();
+
+ assertThatThrownBy(
+ () -> adapter.prepareRoot(targetDir, "demo-app", ProjectRootExistencePolicy.OVERWRITE))
+ .isInstanceOf(ProjectRootIOException.class);
+ }
+}
diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectWriterAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectWriterAdapterTest.java
new file mode 100644
index 0000000..fb2eda5
--- /dev/null
+++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/filesystem/FileSystemProjectWriterAdapterTest.java
@@ -0,0 +1,81 @@
+package io.github.blueprintplatform.codegen.adapter.out.filesystem;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import io.github.blueprintplatform.codegen.adapter.error.exception.ProjectWriteException;
+import io.github.blueprintplatform.codegen.domain.port.out.filesystem.ProjectWriterPort;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("unit")
+@Tag("adapter")
+class FileSystemProjectWriterAdapterTest {
+
+ private final ProjectWriterPort writer = new FileSystemProjectWriterAdapter();
+
+ @Test
+ @DisplayName("writeBytes() should create parent directories and write file")
+ void writeBytes_shouldCreateDirsAndWrite() throws IOException {
+ Path temp = Files.createTempDirectory("writer-test");
+ Path relative = Path.of("a/b/c.txt");
+
+ byte[] content = "hello-bytes".getBytes(StandardCharsets.UTF_8);
+
+ writer.writeBytes(temp, relative, content);
+
+ Path target = temp.resolve(relative);
+ assertThat(Files.exists(target)).isTrue();
+ assertThat(Files.readString(target)).isEqualTo("hello-bytes");
+ }
+
+ @Test
+ @DisplayName("writeBytes() should overwrite existing file")
+ void writeBytes_shouldOverwriteExistingFile() throws IOException {
+ Path temp = Files.createTempDirectory("writer-test2");
+ Path relative = Path.of("file.txt");
+
+ Files.writeString(temp.resolve(relative), "old");
+
+ writer.writeBytes(temp, relative, "new".getBytes(StandardCharsets.UTF_8));
+
+ assertThat(Files.readString(temp.resolve(relative))).isEqualTo("new");
+ }
+
+ @Test
+ @DisplayName("writeText() should write file with provided charset")
+ void writeText_shouldWriteWithCharset() throws IOException {
+ Path temp = Files.createTempDirectory("writer-test3");
+ Path relative = Path.of("utf16.txt");
+
+ writer.writeText(temp, relative, "Merhaba Dünya", StandardCharsets.UTF_16);
+
+ assertThat(Files.readString(temp.resolve(relative), StandardCharsets.UTF_16))
+ .isEqualTo("Merhaba Dünya");
+ }
+
+ @Test
+ @DisplayName("writeBytes() should wrap IOExceptions in ProjectWriteException")
+ void writeBytes_shouldWrapIOException() throws IOException {
+ Path temp = Files.createTempDirectory("writer-test4");
+
+ // Make directory read-only to force IOException
+ File root = temp.toFile();
+ assertThat(root.setWritable(false)).isTrue();
+
+ Path relative = Path.of("fail/here.txt");
+ byte[] content = "boom".getBytes(StandardCharsets.UTF_8);
+
+ assertThatThrownBy(() -> writer.writeBytes(temp, relative, content))
+ .isInstanceOf(ProjectWriteException.class);
+
+ // Restore permission for cleanup
+ assertThat(root.setWritable(true)).isTrue();
+ }
+}
diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileBasedArtifactsSelectorTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileBasedArtifactsSelectorTest.java
new file mode 100644
index 0000000..a624ce8
--- /dev/null
+++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/ProfileBasedArtifactsSelectorTest.java
@@ -0,0 +1,63 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.*;
+
+import io.github.blueprintplatform.codegen.adapter.error.exception.ArtifactsPortNotFoundException;
+import io.github.blueprintplatform.codegen.adapter.error.exception.UnsupportedProfileTypeException;
+import io.github.blueprintplatform.codegen.application.port.out.ProjectArtifactsPort;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+import java.util.Map;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("unit")
+class ProfileBasedArtifactsSelectorTest {
+
+ @Test
+ @DisplayName("Should throw UnsupportedProfileTypeException when ProfileType.from() returns null")
+ void shouldThrowWhenProfileUnsupported() {
+ TechStack options = mock(TechStack.class);
+
+ ProfileBasedArtifactsSelector selector = new ProfileBasedArtifactsSelector(Map.of());
+
+ assertThatThrownBy(() -> selector.select(options))
+ .isInstanceOf(UnsupportedProfileTypeException.class);
+ }
+
+ @Test
+ @DisplayName("Should throw ArtifactsPortNotFoundException when no port registered for type")
+ void shouldThrowWhenPortMissing() {
+ TechStack opts = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA);
+
+ ProfileType type = ProfileType.from(opts);
+ assertThat(type).isNotNull();
+
+ ProfileBasedArtifactsSelector selector =
+ new ProfileBasedArtifactsSelector(Map.of()); // empty registry
+
+ assertThatThrownBy(() -> selector.select(opts))
+ .isInstanceOf(ArtifactsPortNotFoundException.class);
+ }
+
+ @Test
+ @DisplayName("Should return registered ProjectArtifactsPort for matching profile")
+ void shouldReturnMatchingPort() {
+ TechStack opts = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA);
+
+ ProfileType type = ProfileType.from(opts);
+
+ ProjectArtifactsPort port = mock(ProjectArtifactsPort.class);
+
+ ProfileBasedArtifactsSelector selector = new ProfileBasedArtifactsSelector(Map.of(type, port));
+
+ ProjectArtifactsPort result = selector.select(opts);
+
+ assertThat(result).isSameAs(port);
+ }
+}
diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapterIT.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapterIT.java
new file mode 100644
index 0000000..3ea0e58
--- /dev/null
+++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapterIT.java
@@ -0,0 +1,90 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.github.blueprintplatform.codegen.domain.factory.ProjectBlueprintFactory;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyCoordinates;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity;
+import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName;
+import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeOptions;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+import java.util.List;
+import java.util.stream.StreamSupport;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+@Tag("integration")
+class SpringBootMavenJavaArtifactsAdapterIT {
+
+ @Autowired private SpringBootMavenJavaArtifactsAdapter adapter;
+
+ @Test
+ @DisplayName(
+ "generate() should produce artifacts for a valid Spring Boot + Maven + Java blueprint")
+ void generate_shouldProduceArtifactsForValidBlueprint() {
+ ProjectBlueprint blueprint = blueprint();
+
+ Iterable extends GeneratedResource> files = adapter.generate(blueprint);
+
+ var list = StreamSupport.stream(files.spliterator(), false).toList();
+
+ assertThat(list).isNotEmpty();
+ }
+
+ private ProjectBlueprint blueprint() {
+ ProjectIdentity identity =
+ new ProjectIdentity(new GroupId("com.example"), new ArtifactId("demo-app"));
+
+ ProjectName name = new ProjectName("demo-app");
+ ProjectDescription description = new ProjectDescription("Integration test blueprint");
+ PackageName packageName = new PackageName("com.example.demo");
+
+ TechStack techStack = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA);
+
+ PlatformTarget platformTarget =
+ new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5);
+
+ Dependency webStarter =
+ new Dependency(
+ new DependencyCoordinates(
+ new GroupId("org.springframework.boot"), new ArtifactId("spring-boot-starter")),
+ null,
+ null);
+
+ Dependencies dependencies = Dependencies.of(List.of(webStarter));
+
+ ProjectLayout layout = ProjectLayout.STANDARD;
+ SampleCodeOptions sampleCodeOptions = SampleCodeOptions.none();
+
+ return ProjectBlueprintFactory.of(
+ identity,
+ name,
+ description,
+ packageName,
+ techStack,
+ layout,
+ platformTarget,
+ dependencies,
+ sampleCodeOptions);
+ }
+}
diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapterTest.java
new file mode 100644
index 0000000..859f8ed
--- /dev/null
+++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/SpringBootMavenJavaArtifactsAdapterTest.java
@@ -0,0 +1,54 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactPort;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.StreamSupport;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("unit")
+class SpringBootMavenJavaArtifactsAdapterTest {
+
+ @Test
+ @DisplayName("Should return empty list when no artifact ports are configured")
+ void shouldReturnEmptyWhenNoPorts() {
+ SpringBootMavenJavaArtifactsAdapter adapter =
+ new SpringBootMavenJavaArtifactsAdapter(List.of());
+
+ ProjectBlueprint blueprint =
+ new ProjectBlueprint(null, null, null, null, null, null, null, null, null);
+
+ List extends GeneratedResource> result =
+ StreamSupport.stream(adapter.generate(blueprint).spliterator(), false).toList();
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ @DisplayName("Should delegate generate() exactly once to each ArtifactPort")
+ void shouldDelegateToEachArtifactPort() {
+ ProjectBlueprint blueprint =
+ new ProjectBlueprint(null, null, null, null, null, null, null, null, null);
+
+ ArtifactPort p1 = mock(ArtifactPort.class);
+ ArtifactPort p2 = mock(ArtifactPort.class);
+
+ when(p1.generate(blueprint)).thenReturn(Collections.emptyList());
+ when(p2.generate(blueprint)).thenReturn(Collections.emptyList());
+
+ SpringBootMavenJavaArtifactsAdapter adapter =
+ new SpringBootMavenJavaArtifactsAdapter(List.of(p1, p2));
+
+ adapter.generate(blueprint);
+
+ verify(p1, times(1)).generate(blueprint);
+ verify(p2, times(1)).generate(blueprint);
+ }
+}
diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/build/MavenPomBuildConfigurationAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/build/MavenPomBuildConfigurationAdapterTest.java
new file mode 100644
index 0000000..b357916
--- /dev/null
+++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/build/MavenPomBuildConfigurationAdapterTest.java
@@ -0,0 +1,160 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.build;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependency;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyCoordinates;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyScope;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity;
+import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName;
+import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeOptions;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource;
+import io.github.blueprintplatform.codegen.testsupport.build.RecordingPomDependencyMapper;
+import io.github.blueprintplatform.codegen.testsupport.templating.CapturingTemplateRenderer;
+import io.github.blueprintplatform.codegen.testsupport.templating.NoopTemplateRenderer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("unit")
+@Tag("adapter")
+class MavenPomBuildConfigurationAdapterTest {
+
+ private static final String BASE_PATH = "springboot/maven/java/";
+
+ private static ProjectBlueprint blueprintWithDependencies() {
+ ProjectIdentity identity =
+ new ProjectIdentity(new GroupId("com.acme"), new ArtifactId("demo-app"));
+
+ ProjectName name = new ProjectName("Demo App");
+ ProjectDescription description = new ProjectDescription("Sample Project");
+ PackageName pkg = new PackageName("com.acme.demo");
+ ProjectLayout layout = ProjectLayout.STANDARD;
+ TechStack techStack = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA);
+
+ PlatformTarget target = new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5);
+
+ Dependency dep =
+ new Dependency(
+ new DependencyCoordinates(new GroupId("org.acme"), new ArtifactId("custom-dep")),
+ new DependencyVersion("1.0.0"),
+ DependencyScope.RUNTIME);
+
+ Dependencies dependencies = Dependencies.of(List.of(dep));
+
+ SampleCodeOptions sampleCodeOptions = SampleCodeOptions.none();
+
+ return new ProjectBlueprint(
+ identity,
+ name,
+ description,
+ pkg,
+ techStack,
+ layout,
+ target,
+ dependencies,
+ sampleCodeOptions);
+ }
+
+ @Test
+ @DisplayName("artifactKey() should return POM")
+ void artifactKey_shouldReturnPom() {
+ MavenPomBuildConfigurationAdapter adapter =
+ new MavenPomBuildConfigurationAdapter(
+ new NoopTemplateRenderer(),
+ new ArtifactDefinition(
+ BASE_PATH, List.of(new TemplateDefinition("pom.ftl", "pom.xml"))),
+ new RecordingPomDependencyMapper(List.of()));
+
+ assertThat(adapter.artifactKey()).isEqualTo(ArtifactKey.BUILD_CONFIG);
+ }
+
+ @Test
+ @DisplayName("buildModel via generate() should populate all POM fields and dependencies")
+ void generate_shouldBuildCorrectModelForPom() {
+ CapturingTemplateRenderer renderer = new CapturingTemplateRenderer();
+ RecordingPomDependencyMapper mapper =
+ new RecordingPomDependencyMapper(
+ List.of(PomDependency.of("org.acme", "custom-dep", "1.0.0", "runtime")));
+
+ TemplateDefinition templateDefinition = new TemplateDefinition("pom.ftl", "pom.xml");
+ ArtifactDefinition artifactDefinition =
+ new ArtifactDefinition(BASE_PATH, List.of(templateDefinition));
+
+ MavenPomBuildConfigurationAdapter adapter =
+ new MavenPomBuildConfigurationAdapter(renderer, artifactDefinition, mapper);
+
+ ProjectBlueprint blueprint = blueprintWithDependencies();
+
+ Path relativePath = Path.of("pom.xml");
+ GeneratedTextResource dummyFile =
+ new GeneratedTextResource(relativePath, "", StandardCharsets.UTF_8);
+ renderer.nextFile = dummyFile;
+
+ Iterable extends GeneratedResource> result = adapter.generate(blueprint);
+
+ assertThat(result).singleElement().isSameAs(dummyFile);
+
+ assertThat(renderer.capturedOutPath).isEqualTo(relativePath);
+ assertThat(renderer.capturedTemplateName).isEqualTo(BASE_PATH + "pom.ftl");
+ assertThat(renderer.capturedModel).isNotNull();
+
+ Map model = renderer.capturedModel;
+
+ assertThat(model)
+ .containsEntry("groupId", "com.acme")
+ .containsEntry("artifactId", "demo-app")
+ .containsEntry("javaVersion", "21")
+ .containsEntry("springBootVersion", "3.5.8")
+ .containsEntry("projectName", "Demo App")
+ .containsEntry("projectDescription", "Sample Project");
+
+ assertThat(mapper.capturedDependencies).isSameAs(blueprint.getDependencies());
+
+ @SuppressWarnings("unchecked")
+ List deps = (List) model.get("dependencies");
+ assertThat(deps).hasSize(3);
+
+ PomDependency core = deps.getFirst();
+ assertThat(core.groupId()).isEqualTo("org.springframework.boot");
+ assertThat(core.artifactId()).isEqualTo("spring-boot-starter");
+ assertThat(core.version()).isNull();
+ assertThat(core.scope()).isNull();
+
+ PomDependency mapped = deps.get(1);
+ assertThat(mapped.groupId()).isEqualTo("org.acme");
+ assertThat(mapped.artifactId()).isEqualTo("custom-dep");
+ assertThat(mapped.version()).isEqualTo("1.0.0");
+ assertThat(mapped.scope()).isEqualTo("runtime");
+
+ PomDependency testStarter = deps.get(2);
+ assertThat(testStarter.groupId()).isEqualTo("org.springframework.boot");
+ assertThat(testStarter.artifactId()).isEqualTo("spring-boot-starter-test");
+ assertThat(testStarter.scope()).isEqualTo("test");
+ }
+}
diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/config/ApplicationYamlAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/config/ApplicationYamlAdapterTest.java
new file mode 100644
index 0000000..561db16
--- /dev/null
+++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/config/ApplicationYamlAdapterTest.java
@@ -0,0 +1,117 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.config;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity;
+import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName;
+import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeOptions;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource;
+import io.github.blueprintplatform.codegen.testsupport.templating.CapturingTemplateRenderer;
+import io.github.blueprintplatform.codegen.testsupport.templating.NoopTemplateRenderer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("unit")
+@Tag("adapter")
+class ApplicationYamlAdapterTest {
+
+ private static final String BASE_PATH = "springboot/maven/java/";
+
+ private static ProjectBlueprint blueprint() {
+ ProjectIdentity identity =
+ new ProjectIdentity(new GroupId("com.acme"), new ArtifactId("demo-app"));
+
+ ProjectName name = new ProjectName("Demo App");
+ ProjectDescription description = new ProjectDescription("Sample Project");
+ PackageName pkg = new PackageName("com.acme.demo");
+
+ TechStack techStack = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA);
+ ProjectLayout layout = ProjectLayout.STANDARD;
+ PlatformTarget platformTarget =
+ new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5);
+ Dependencies dependencies = Dependencies.of(List.of());
+
+ SampleCodeOptions sampleCodeOptions = SampleCodeOptions.none();
+
+ return new ProjectBlueprint(
+ identity,
+ name,
+ description,
+ pkg,
+ techStack,
+ layout,
+ platformTarget,
+ dependencies,
+ sampleCodeOptions);
+ }
+
+ @Test
+ @DisplayName("artifactKey() should return APP_CONFIG")
+ void artifactKey_shouldReturnApplicationYaml() {
+ ApplicationYamlAdapter adapter =
+ new ApplicationYamlAdapter(
+ new NoopTemplateRenderer(),
+ new ArtifactDefinition(
+ BASE_PATH,
+ List.of(new TemplateDefinition("application-yaml.ftl", "application.yml"))));
+
+ assertThat(adapter.artifactKey()).isEqualTo(ArtifactKey.APP_CONFIG);
+ }
+
+ @Test
+ @DisplayName(
+ "generate() should build model with applicationName from artifactId and render single file")
+ void generate_shouldBuildModelAndRenderFile() {
+ CapturingTemplateRenderer renderer = new CapturingTemplateRenderer();
+
+ TemplateDefinition templateDefinition =
+ new TemplateDefinition("application-yaml.ftl", "src/main/resources/application.yml");
+ ArtifactDefinition artifactDefinition =
+ new ArtifactDefinition(BASE_PATH, List.of(templateDefinition));
+
+ ApplicationYamlAdapter adapter = new ApplicationYamlAdapter(renderer, artifactDefinition);
+
+ ProjectBlueprint blueprint = blueprint();
+
+ Path relativePath = Path.of("src/main/resources/application.yml");
+ GeneratedTextResource expectedFile =
+ new GeneratedTextResource(
+ relativePath, "spring:\n application:\n name: demo-app\n", StandardCharsets.UTF_8);
+
+ renderer.nextFile = expectedFile;
+
+ Iterable extends GeneratedResource> result = adapter.generate(blueprint);
+
+ assertThat(result).singleElement().isSameAs(expectedFile);
+
+ assertThat(renderer.capturedOutPath).isEqualTo(relativePath);
+ assertThat(renderer.capturedTemplateName).isEqualTo(BASE_PATH + "application-yaml.ftl");
+
+ Map model = renderer.capturedModel;
+ assertThat(model).isNotNull().containsEntry("applicationName", "demo-app");
+ }
+}
diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/doc/ProjectDocumentationAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/doc/ProjectDocumentationAdapterTest.java
new file mode 100644
index 0000000..702816d
--- /dev/null
+++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/doc/ProjectDocumentationAdapterTest.java
@@ -0,0 +1,158 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.doc;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependency;
+import io.github.blueprintplatform.codegen.adapter.out.build.maven.shared.PomDependencyMapper;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependency;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyCoordinates;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyScope;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.DependencyVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity;
+import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName;
+import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeOptions;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource;
+import io.github.blueprintplatform.codegen.testsupport.build.RecordingPomDependencyMapper;
+import io.github.blueprintplatform.codegen.testsupport.templating.CapturingTemplateRenderer;
+import io.github.blueprintplatform.codegen.testsupport.templating.NoopTemplateRenderer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("unit")
+@Tag("adapter")
+class ProjectDocumentationAdapterTest {
+
+ private static final String BASE_PATH = "springboot/maven/java/";
+
+ private static ProjectBlueprint blueprintWithDependencies() {
+ ProjectIdentity identity =
+ new ProjectIdentity(new GroupId("com.acme"), new ArtifactId("demo-app"));
+
+ ProjectName name = new ProjectName("Demo App");
+ ProjectDescription description = new ProjectDescription("Sample Project");
+ PackageName pkg = new PackageName("com.acme.demo");
+
+ TechStack techStack = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA);
+
+ ProjectLayout layout = ProjectLayout.STANDARD;
+ PlatformTarget target = new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5);
+
+ Dependency dep =
+ new Dependency(
+ new DependencyCoordinates(new GroupId("org.acme"), new ArtifactId("custom-dep")),
+ new DependencyVersion("1.0.0"),
+ DependencyScope.RUNTIME);
+
+ Dependencies dependencies = Dependencies.of(List.of(dep));
+
+ SampleCodeOptions sampleCodeOptions = SampleCodeOptions.none();
+
+ return new ProjectBlueprint(
+ identity,
+ name,
+ description,
+ pkg,
+ techStack,
+ layout,
+ target,
+ dependencies,
+ sampleCodeOptions);
+ }
+
+ @Test
+ @DisplayName("artifactKey() should return PROJECT_DOCUMENTATION")
+ void artifactKey_shouldReturnProjectDocumentation() {
+ ProjectDocumentationAdapter adapter =
+ new ProjectDocumentationAdapter(
+ new NoopTemplateRenderer(),
+ new ArtifactDefinition(
+ BASE_PATH, List.of(new TemplateDefinition("README.ftl", "README.md"))),
+ new PomDependencyMapper());
+
+ assertThat(adapter.artifactKey()).isEqualTo(ArtifactKey.PROJECT_DOCUMENTATION);
+ }
+
+ @Test
+ @DisplayName(
+ "generate() should build correct project documentation model and delegate dependencies mapping")
+ void generate_shouldBuildCorrectModelForProjectDocumentation() {
+ CapturingTemplateRenderer renderer = new CapturingTemplateRenderer();
+
+ List mappedDeps =
+ List.of(PomDependency.of("org.acme", "custom-dep", "1.0.0", "runtime"));
+ RecordingPomDependencyMapper mapper = new RecordingPomDependencyMapper(mappedDeps);
+
+ TemplateDefinition templateDefinition = new TemplateDefinition("README.ftl", "README.md");
+ ArtifactDefinition artifactDefinition =
+ new ArtifactDefinition(BASE_PATH, List.of(templateDefinition));
+
+ ProjectDocumentationAdapter adapter =
+ new ProjectDocumentationAdapter(renderer, artifactDefinition, mapper);
+
+ ProjectBlueprint blueprint = blueprintWithDependencies();
+
+ Path relativePath = Path.of("README.md");
+ GeneratedTextResource dummyFile =
+ new GeneratedTextResource(relativePath, "# Readme", StandardCharsets.UTF_8);
+ renderer.nextFile = dummyFile;
+
+ Iterable extends GeneratedResource> result = adapter.generate(blueprint);
+
+ assertThat(result).singleElement().isSameAs(dummyFile);
+
+ assertThat(renderer.capturedOutPath).isEqualTo(relativePath);
+ assertThat(renderer.capturedTemplateName).isEqualTo(BASE_PATH + "README.ftl");
+ assertThat(renderer.capturedModel).isNotNull();
+
+ Map model = renderer.capturedModel;
+
+ assertThat(model)
+ .containsEntry("projectName", "Demo App")
+ .containsEntry("projectDescription", "Sample Project")
+ .containsEntry("groupId", "com.acme")
+ .containsEntry("artifactId", "demo-app")
+ .containsEntry("packageName", "com.acme.demo")
+ .containsEntry("buildTool", "maven")
+ .containsEntry("language", "java")
+ .containsEntry("framework", "spring-boot")
+ .containsEntry("javaVersion", "21")
+ .containsEntry("springBootVersion", "3.5.8")
+ .containsEntry("hasHexSample", false);
+
+ assertThat(mapper.capturedDependencies).isSameAs(blueprint.getDependencies());
+
+ @SuppressWarnings("unchecked")
+ List deps = (List) model.get("dependencies");
+ assertThat(deps).isSameAs(mappedDeps).hasSize(1);
+
+ PomDependency d = deps.getFirst();
+ assertThat(d.groupId()).isEqualTo("org.acme");
+ assertThat(d.artifactId()).isEqualTo("custom-dep");
+ assertThat(d.version()).isEqualTo("1.0.0");
+ assertThat(d.scope()).isEqualTo("runtime");
+ }
+}
diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/ignore/GitIgnoreAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/ignore/GitIgnoreAdapterTest.java
new file mode 100644
index 0000000..d6ec3d0
--- /dev/null
+++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/ignore/GitIgnoreAdapterTest.java
@@ -0,0 +1,68 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.ignore;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.bootstrap.config.TemplateDefinition;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource;
+import io.github.blueprintplatform.codegen.testsupport.templating.CapturingTemplateRenderer;
+import io.github.blueprintplatform.codegen.testsupport.templating.NoopTemplateRenderer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("unit")
+@Tag("adapter")
+class GitIgnoreAdapterTest {
+
+ private static final String BASE_PATH = "springboot/maven/java/";
+
+ @Test
+ @DisplayName("artifactKey() should return IGNORE_RULES")
+ void artifactKey_shouldReturnIgnoreRules() {
+ GitIgnoreAdapter adapter =
+ new GitIgnoreAdapter(
+ new NoopTemplateRenderer(),
+ new ArtifactDefinition(
+ BASE_PATH, List.of(new TemplateDefinition("gitignore.ftl", ".gitignore"))));
+
+ assertThat(adapter.artifactKey()).isEqualTo(ArtifactKey.IGNORE_RULES);
+ }
+
+ @Test
+ @DisplayName("generate() should render ignore rules with an empty ignoreList model")
+ void generate_shouldRenderGitignoreWithEmptyIgnoreList() {
+ CapturingTemplateRenderer renderer = new CapturingTemplateRenderer();
+
+ TemplateDefinition templateDefinition = new TemplateDefinition("gitignore.ftl", ".gitignore");
+ ArtifactDefinition artifactDefinition =
+ new ArtifactDefinition(BASE_PATH, List.of(templateDefinition));
+
+ GitIgnoreAdapter adapter = new GitIgnoreAdapter(renderer, artifactDefinition);
+
+ ProjectBlueprint blueprint =
+ new ProjectBlueprint(null, null, null, null, null, null, null, null, null);
+
+ Path relativePath = Path.of(".gitignore");
+ GeneratedTextResource expectedFile =
+ new GeneratedTextResource(relativePath, "# gitignore", StandardCharsets.UTF_8);
+ renderer.nextFile = expectedFile;
+
+ Iterable extends GeneratedResource> result = adapter.generate(blueprint);
+
+ assertThat(result).singleElement().isSameAs(expectedFile);
+
+ assertThat(renderer.capturedOutPath).isEqualTo(relativePath);
+ assertThat(renderer.capturedTemplateName).isEqualTo(BASE_PATH + "gitignore.ftl");
+
+ Map model = renderer.capturedModel;
+ assertThat(model).isNotNull().containsEntry("ignoreList", List.of());
+ }
+}
diff --git a/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/sample/SampleCodeAdapterTest.java b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/sample/SampleCodeAdapterTest.java
new file mode 100644
index 0000000..a3090e2
--- /dev/null
+++ b/src/test/java/io/github/blueprintplatform/codegen/adapter/out/profile/springboot/maven/java/sample/SampleCodeAdapterTest.java
@@ -0,0 +1,179 @@
+package io.github.blueprintplatform.codegen.adapter.out.profile.springboot.maven.java.sample;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.github.blueprintplatform.codegen.adapter.out.templating.TemplateRenderer;
+import io.github.blueprintplatform.codegen.application.port.out.artifact.ArtifactKey;
+import io.github.blueprintplatform.codegen.bootstrap.config.ArtifactDefinition;
+import io.github.blueprintplatform.codegen.bootstrap.config.SamplesProperties;
+import io.github.blueprintplatform.codegen.domain.model.ProjectBlueprint;
+import io.github.blueprintplatform.codegen.domain.model.value.dependency.Dependencies;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ArtifactId;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.GroupId;
+import io.github.blueprintplatform.codegen.domain.model.value.identity.ProjectIdentity;
+import io.github.blueprintplatform.codegen.domain.model.value.layout.ProjectLayout;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectDescription;
+import io.github.blueprintplatform.codegen.domain.model.value.naming.ProjectName;
+import io.github.blueprintplatform.codegen.domain.model.value.pkg.PackageName;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeLevel;
+import io.github.blueprintplatform.codegen.domain.model.value.sample.SampleCodeOptions;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.JavaVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.PlatformTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootJvmTarget;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.platform.SpringBootVersion;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.BuildTool;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Framework;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.Language;
+import io.github.blueprintplatform.codegen.domain.model.value.tech.stack.TechStack;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedResource;
+import io.github.blueprintplatform.codegen.domain.port.out.artifact.GeneratedTextResource;
+import io.github.blueprintplatform.codegen.testsupport.templating.NoopTemplateRenderer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.StreamSupport;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("unit")
+@Tag("adapter")
+class SampleCodeAdapterTest {
+
+ private static final String BASE_PATH = "springboot/maven/java/";
+ private static final String STANDARD_SAMPLES_ROOT = "sample/standard";
+ private static final String HEXAGONAL_SAMPLES_ROOT = "sample/hexagonal";
+ private static final String BASIC_DIR_NAME = "basic";
+ private static final String RICH_DIR_NAME = "rich";
+
+ private static final String BASE_PACKAGE = "com.acme.demo";
+ private static final String BASE_PACKAGE_PATH = "com/acme/demo";
+
+ private static final ArtifactDefinition DUMMY_ARTIFACT_DEFINITION =
+ new ArtifactDefinition(BASE_PATH, List.of());
+
+ private static final SamplesProperties SAMPLES_PROPERTIES =
+ new SamplesProperties(
+ STANDARD_SAMPLES_ROOT, HEXAGONAL_SAMPLES_ROOT, BASIC_DIR_NAME, RICH_DIR_NAME);
+
+ private static ProjectBlueprint blueprint(ProjectLayout layout, SampleCodeLevel level) {
+
+ ProjectIdentity identity =
+ new ProjectIdentity(new GroupId("com.acme"), new ArtifactId("demo-app"));
+
+ ProjectName name = new ProjectName("Demo App");
+ ProjectDescription description = new ProjectDescription("Sample Project");
+ PackageName pkg = new PackageName(BASE_PACKAGE);
+
+ TechStack techStack = new TechStack(Framework.SPRING_BOOT, BuildTool.MAVEN, Language.JAVA);
+ PlatformTarget platformTarget =
+ new SpringBootJvmTarget(JavaVersion.JAVA_21, SpringBootVersion.V3_5);
+
+ Dependencies dependencies = Dependencies.of(List.of());
+ SampleCodeOptions sampleCodeOptions = new SampleCodeOptions(level);
+
+ return new ProjectBlueprint(
+ identity,
+ name,
+ description,
+ pkg,
+ techStack,
+ layout,
+ platformTarget,
+ dependencies,
+ sampleCodeOptions);
+ }
+
+ private static List toRelativePaths(Iterable extends GeneratedResource> resources) {
+ List result = new ArrayList<>();
+ StreamSupport.stream(resources.spliterator(), false).forEach(r -> result.add(r.relativePath()));
+ return result;
+ }
+
+ @Test
+ @DisplayName("artifactKey() should return SAMPLE_CODE")
+ void artifactKey_shouldReturnSampleCode() {
+ SampleCodeAdapter adapter =
+ new SampleCodeAdapter(
+ new NoopTemplateRenderer(), DUMMY_ARTIFACT_DEFINITION, SAMPLES_PROPERTIES);
+
+ assertThat(adapter.artifactKey()).isEqualTo(ArtifactKey.SAMPLE_CODE);
+ }
+
+ @Test
+ @DisplayName("generate() should return empty when sample level is NONE")
+ void generate_noneLevel_shouldReturnEmpty() {
+ SampleCodeAdapter adapter =
+ new SampleCodeAdapter(
+ new NoopTemplateRenderer(), DUMMY_ARTIFACT_DEFINITION, SAMPLES_PROPERTIES);
+
+ ProjectBlueprint blueprint = blueprint(ProjectLayout.HEXAGONAL, SampleCodeLevel.NONE);
+
+ Iterable extends GeneratedResource> resources = adapter.generate(blueprint);
+
+ assertThat(resources).isEmpty();
+ }
+
+ @Test
+ @DisplayName("generate() should return empty when layout is STANDARD even if level is BASIC")
+ void generate_standardLayoutBasicLevel_shouldReturnEmpty() {
+ SampleCodeAdapter adapter =
+ new SampleCodeAdapter(
+ new NoopTemplateRenderer(), DUMMY_ARTIFACT_DEFINITION, SAMPLES_PROPERTIES);
+
+ ProjectBlueprint blueprint = blueprint(ProjectLayout.STANDARD, SampleCodeLevel.BASIC);
+
+ Iterable extends GeneratedResource> resources = adapter.generate(blueprint);
+
+ assertThat(resources).isEmpty();
+ }
+
+ @Test
+ @DisplayName("generate() should resolve hexagonal BASIC templates and map them to Java sources")
+ void generate_hexagonalBasic_shouldGenerateSampleSources() {
+ RecordingTemplateRenderer renderer = new RecordingTemplateRenderer();
+
+ SampleCodeAdapter adapter =
+ new SampleCodeAdapter(renderer, DUMMY_ARTIFACT_DEFINITION, SAMPLES_PROPERTIES);
+
+ ProjectBlueprint blueprint = blueprint(ProjectLayout.HEXAGONAL, SampleCodeLevel.BASIC);
+
+ Iterable extends GeneratedResource> resources = adapter.generate(blueprint);
+ List paths = toRelativePaths(resources);
+
+ assertThat(paths).isNotEmpty();
+
+ Path expectedGreetingControllerPath =
+ Path.of("src/main/java")
+ .resolve(BASE_PACKAGE_PATH)
+ .resolve("adapter/sample/greeting/in/rest/GreetingController.java");
+
+ assertThat(paths).contains(expectedGreetingControllerPath);
+
+ String expectedTemplateName =
+ "springboot/maven/java/sample/hexagonal/basic/adapter/sample/greeting/in/rest/GreetingController.java.ftl";
+
+ assertThat(renderer.capturedTemplateNames).contains(expectedTemplateName);
+
+ assertThat(renderer.capturedModels)
+ .allMatch(model -> BASE_PACKAGE.equals(model.get("projectPackageName")));
+ }
+
+ private static final class RecordingTemplateRenderer implements TemplateRenderer {
+
+ private final List capturedTemplateNames = new ArrayList<>();
+ private final List