Skip to content

Commit af4adb2

Browse files
committed
feat(phase-7): generate_pattern tool with 5 parametrised templates
Adds the fourth MCP tool — generate_pattern — which renders a tailored Java implementation of a chosen design pattern with the user's package name and type names baked in. 5 patterns wired in this milestone (Singleton, Builder, Strategy, Observer, Factory Method); the remaining 18 return a clear tool-error pointing the caller to `pattern_examples`. Generation engine (com.javapatterns.mcp.generate): - PatternGenerator: holder-idiom singleton. Loads a small /templates/<slug>/template-index.json per pattern that lists template files and resolved file names (the index entries may themselves use placeholders like ${TYPE_NAME}.java). The actual substitution is a hand-rolled walk over the source so we avoid both regex pitfalls and the build complexity of pulling in JTE precompiled templates inside a shaded jar. - GeneratedFile: result record with fileName + packageName + source. generate_pattern tool (com.javapatterns.mcp.tools.GeneratePatternTool): - Required input: `pattern` (case-insensitive, accepts enum names, slugs and display names) + `typeName` (validated as a Java identifier; rejects numerics, hyphens, etc.). - Optional input: `packageName` (validated as a dotted Java package; empty falls back to the default package, and the template's ${PACKAGE_DECL} placeholder collapses to nothing in that case). - Output JSON shape mirrors pattern_examples but carries the generated sources instead of bundled examples: { pattern, category, fileCount, files: [{fileName, packageName, source}] } - Unsupported pattern → isError=true with the supported slug list and a hint to use `pattern_examples`. Bundled templates this phase: - singleton/ → 1 file (Bill-Pugh holder idiom; rejects reflective re-entry; uses ${TYPE_NAME} for class + holder name). - builder/ → 1 file (immutable Car-style record with a fluent static-nested Builder, mandatory ctor arg + 2 optional fields; build() validates). - strategy/ → 3 files: ${TYPE_NAME}Strategy interface + Default${TYPE_NAME}Strategy (identity) + Reverse${TYPE_NAME}Strategy. - observer/ → 2 files: ${TYPE_NAME}Listener (functional iface) + ${TYPE_NAME}Bus pub/sub hub with re-entrant-safe snapshot dispatch. - factory-method/ → 3 files: ${TYPE_NAME} product interface + ${TYPE_NAME}Creator abstract creator (template method + factory method) + Default${TYPE_NAME}Creator concrete creator with a private inner product. Wiring: - JavaPatternsMcpServer.buildServer() now registers 4 tools: ping, list_patterns, pattern_examples, generate_pattern. Tests (86 total, all green): - GeneratePatternToolTest covers metadata, exact rendered placeholders for Singleton (typeName + package), empty PACKAGE_DECL on default package, error paths (missing/invalid typeName, unsupported pattern), and — critically — that every supported pattern produces sources that compile standalone with javac --release 21. Strategy and Observer are compiled with typeName=String so the generated generic bounds stay self-contained. Live stdio verification: generate_pattern(pattern="singleton", typeName="Configuration", packageName="com.demo.config") returns a 42-line Configuration.java in package com.demo.config that starts with the correct package declaration and class definition. Coverage status update — 23 patterns examples + 5 patterns templated.
1 parent 6291690 commit af4adb2

20 files changed

Lines changed: 825 additions & 3 deletions

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ This MCP server fills that gap with **deterministic, AST-backed tooling**.
3030
| 2 | Pattern catalog model (23 GoF + metadata) | ✅ done |
3131
| 3 | `list_patterns` tool | ✅ done |
3232
| 4 | `pattern_examples` tool (canonical, compilable) | ✅ all 23 patterns |
33-
| 5 | `generate_pattern` tool (JTE templates) | ⏳ planned |
33+
| 5 | `generate_pattern` tool (JTE templates) | 🟡 5 patterns wired (Singleton, Builder, Strategy, Observer, Factory Method) |
3434
| 6-8 | All 23 GoF patterns implemented + tested | ⏳ planned |
3535
| 9 | `detect_pattern` (JavaParser AST visitors) | ⏳ planned |
3636
| 10 | `validate_pattern` (pattern-specific rules) | ⏳ planned |

‎src/main/java/com/javapatterns/mcp/JavaPatternsMcpServer.java‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import com.javapatterns.mcp.catalog.PatternExamplesLoader;
44
import com.javapatterns.mcp.catalog.PatternRegistry;
5+
import com.javapatterns.mcp.generate.PatternGenerator;
6+
import com.javapatterns.mcp.tools.GeneratePatternTool;
57
import com.javapatterns.mcp.tools.ListPatternsTool;
68
import com.javapatterns.mcp.tools.PatternExamplesTool;
79
import com.javapatterns.mcp.tools.PingTool;
@@ -82,7 +84,8 @@ static McpSyncServer buildServer(McpServerTransportProvider transport, McpJsonMa
8284
List<String> registeredTools = List.of(
8385
PingTool.NAME,
8486
ListPatternsTool.NAME,
85-
PatternExamplesTool.NAME
87+
PatternExamplesTool.NAME,
88+
GeneratePatternTool.NAME
8689
);
8790

8891
McpServerFeatures.SyncToolSpecification ping =
@@ -94,9 +97,12 @@ static McpSyncServer buildServer(McpServerTransportProvider transport, McpJsonMa
9497
McpServerFeatures.SyncToolSpecification patternExamples =
9598
new PatternExamplesTool(PatternExamplesLoader.getInstance(), jsonMapper).specification();
9699

100+
McpServerFeatures.SyncToolSpecification generatePattern =
101+
new GeneratePatternTool(PatternGenerator.getInstance(), jsonMapper).specification();
102+
97103
return McpServer.sync(transport)
98104
.serverInfo(SERVER_NAME, SERVER_VERSION)
99-
.tools(ping, listPatterns, patternExamples)
105+
.tools(ping, listPatterns, patternExamples, generatePattern)
100106
.build();
101107
}
102108
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package com.javapatterns.mcp.generate;
2+
3+
import com.javapatterns.mcp.catalog.Pattern;
4+
5+
import java.io.IOException;
6+
import java.io.InputStream;
7+
import java.nio.charset.StandardCharsets;
8+
import java.util.LinkedHashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.Objects;
12+
13+
/**
14+
* Generates fully parameterised Java sources for a design pattern.
15+
*
16+
* <p>Each supported pattern has a template index file at
17+
* {@code /templates/<slug>/template-index.json} listing the template files
18+
* and their resolved file names. Each template file lives next to the
19+
* index and uses {@code ${PLACEHOLDER}} markers (no escaping, no nesting —
20+
* intentional: keeps generation deterministic and easy to debug).</p>
21+
*
22+
* <p>The set of placeholders varies per pattern. The common ones are:</p>
23+
* <ul>
24+
* <li>{@code ${PACKAGE_NAME}} — the target package (no trailing dot).</li>
25+
* <li>{@code ${PACKAGE_DECL}} — either {@code "package x.y;"} or empty if
26+
* the user passes the empty / default package.</li>
27+
* <li>{@code ${TYPE_NAME}} — the user's chosen "main" type (e.g.
28+
* {@code "Logger"} for Singleton, {@code "Pizza"} for Builder).</li>
29+
* </ul>
30+
*
31+
* <p>Patterns without a template return a structured "not generated yet"
32+
* response from the calling tool — the registry below is the single source of
33+
* truth for which patterns have generators wired.</p>
34+
*/
35+
public final class PatternGenerator {
36+
37+
/** Patterns whose templates are bundled in this build. */
38+
public static final java.util.Set<Pattern> SUPPORTED = java.util.EnumSet.of(
39+
Pattern.SINGLETON,
40+
Pattern.FACTORY_METHOD,
41+
Pattern.BUILDER,
42+
Pattern.STRATEGY,
43+
Pattern.OBSERVER
44+
);
45+
46+
/** Singleton instance — templates are loaded lazily on first use. */
47+
private static final class Holder {
48+
private static final PatternGenerator INSTANCE = new PatternGenerator();
49+
}
50+
51+
public static PatternGenerator getInstance() {
52+
return Holder.INSTANCE;
53+
}
54+
55+
private PatternGenerator() { /* nothing to init — templates are loaded on demand */ }
56+
57+
/**
58+
* Generates the file set for the given pattern.
59+
*
60+
* @param pattern one of {@link #SUPPORTED}; other patterns throw {@link UnsupportedOperationException}.
61+
* @param params placeholder values. Must contain {@code TYPE_NAME} and {@code PACKAGE_NAME}.
62+
* {@code PACKAGE_DECL} is auto-derived from {@code PACKAGE_NAME}.
63+
* @return ordered list of generated files, ready to write to disk.
64+
*/
65+
public List<GeneratedFile> generate(Pattern pattern, Map<String, String> params) {
66+
Objects.requireNonNull(pattern, "pattern");
67+
Objects.requireNonNull(params, "params");
68+
if (!SUPPORTED.contains(pattern)) {
69+
throw new UnsupportedOperationException(
70+
"No template wired yet for pattern " + pattern.name() +
71+
". Use the pattern_examples tool for canonical reference sources, " +
72+
"or contribute a template under /templates/" + pattern.slug() + "/.");
73+
}
74+
75+
// Normalise + augment params.
76+
Map<String, String> merged = new LinkedHashMap<>(params);
77+
merged.computeIfAbsent("TYPE_NAME", k -> "MyType");
78+
String pkg = merged.getOrDefault("PACKAGE_NAME", "").trim();
79+
merged.put("PACKAGE_NAME", pkg);
80+
merged.put("PACKAGE_DECL", pkg.isEmpty() ? "" : "package " + pkg + ";");
81+
82+
TemplateIndex index = loadIndex(pattern);
83+
List<GeneratedFile> out = new java.util.ArrayList<>(index.files.size());
84+
for (TemplateFile tf : index.files) {
85+
String body = readTemplate(pattern, tf.template);
86+
String rendered = substitute(body, merged);
87+
String fileName = substitute(tf.fileName, merged);
88+
out.add(new GeneratedFile(fileName, pkg, rendered));
89+
}
90+
return out;
91+
}
92+
93+
/** Plain {@code ${KEY}} substitution. Unknown keys are left as-is. */
94+
static String substitute(String template, Map<String, String> params) {
95+
// Walk the string once. We avoid regex to make behaviour
96+
// (and performance) trivially predictable.
97+
StringBuilder out = new StringBuilder(template.length());
98+
int i = 0;
99+
while (i < template.length()) {
100+
int dollar = template.indexOf("${", i);
101+
if (dollar < 0) {
102+
out.append(template, i, template.length());
103+
break;
104+
}
105+
int end = template.indexOf('}', dollar + 2);
106+
if (end < 0) { // no closing — emit the rest verbatim
107+
out.append(template, i, template.length());
108+
break;
109+
}
110+
out.append(template, i, dollar);
111+
String key = template.substring(dollar + 2, end);
112+
String value = params.get(key);
113+
if (value != null) {
114+
out.append(value);
115+
} else {
116+
out.append(template, dollar, end + 1); // keep placeholder untouched
117+
}
118+
i = end + 1;
119+
}
120+
return out.toString();
121+
}
122+
123+
// ─── resource loading ──────────────────────────────────────────
124+
125+
private TemplateIndex loadIndex(Pattern pattern) {
126+
String indexPath = "/templates/" + pattern.slug() + "/template-index.json";
127+
try (InputStream in = PatternGenerator.class.getResourceAsStream(indexPath)) {
128+
if (in == null) {
129+
throw new IllegalStateException(
130+
"Template index missing on classpath: " + indexPath);
131+
}
132+
byte[] bytes = in.readAllBytes();
133+
return parseIndex(bytes);
134+
} catch (IOException e) {
135+
throw new IllegalStateException("Failed to read " + indexPath, e);
136+
}
137+
}
138+
139+
private String readTemplate(Pattern pattern, String templateName) {
140+
String path = "/templates/" + pattern.slug() + "/" + templateName;
141+
try (InputStream in = PatternGenerator.class.getResourceAsStream(path)) {
142+
if (in == null) {
143+
throw new IllegalStateException("Template file missing: " + path);
144+
}
145+
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
146+
} catch (IOException e) {
147+
throw new IllegalStateException("Failed to read template " + path, e);
148+
}
149+
}
150+
151+
private static TemplateIndex parseIndex(byte[] bytes) throws IOException {
152+
io.modelcontextprotocol.json.McpJsonMapper mapper =
153+
com.javapatterns.mcp.McpJsonMappers.defaultMapper();
154+
return mapper.readValue(bytes, new io.modelcontextprotocol.json.TypeRef<TemplateIndex>() { });
155+
}
156+
157+
// ─── JSON DTOs ─────────────────────────────────────────────────
158+
159+
static final class TemplateIndex {
160+
public List<TemplateFile> files;
161+
}
162+
163+
static final class TemplateFile {
164+
/** File name written to disk; may itself contain placeholders. */
165+
public String fileName;
166+
/** Template resource name under {@code /templates/<slug>/}. */
167+
public String template;
168+
}
169+
170+
/** Result of a {@link #generate(Pattern, Map)} call — one generated file. */
171+
public record GeneratedFile(String fileName, String packageName, String source) { }
172+
}

0 commit comments

Comments
 (0)