This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git
The following commit(s) were added to refs/heads/master by this push:
new 4f075a46d CAY-2986 Cgen to run unconditionally
4f075a46d is described below
commit 4f075a46d79741aa3547e3898ab91600410b1c49
Author: Andrus Adamchik <[email protected]>
AuthorDate: Sat Aug 8 08:53:07 2026 -0400
CAY-2986 Cgen to run unconditionally
also updating MCP skill docs. There were discrepancies
---
RELEASE-NOTES.txt | 1 +
UPGRADE.md | 6 ++
ai-plugin/references/mcp-tools.md | 18 ++--
ai-plugin/skills/cayenne-cgen/SKILL.md | 10 +-
.../apache/cayenne/tools/CayenneGeneratorTask.java | 35 +++---
.../org/apache/cayenne/gen/CgenConfiguration.java | 26 ++---
.../apache/cayenne/gen/ClassGenerationAction.java | 119 ++++++++++++---------
.../cayenne/gen/ClassGenerationActionTest.java | 89 +++++++--------
.../java/org/apache/cayenne/tools/CgenTask.java | 27 +++--
.../apache/cayenne/tools/CayenneGeneratorMojo.java | 30 +++---
cayenne-mcp-server/PROTOCOL.md | 5 +-
.../apache/cayenne/mcp/tools/cgen/CgenRunTool.java | 12 ---
.../cgen/InstrumentedClassGenerationAction.java | 40 ++-----
.../apache/cayenne/mcp/tools/cgen/CgenRunIT.java | 57 +++++-----
.../_cayenne-guide/part6/maven-plugin.adoc | 2 +-
.../editor/datadomain/cgen/DataDomainCgenTab.java | 1 -
.../ui/project/editor/datamap/cgen/CgenPanel.java | 2 -
17 files changed, 243 insertions(+), 237 deletions(-)
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index aa94aa8f3..117a6340e 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -32,6 +32,7 @@ CAY-2982 Modeler: when upgrading projects allow upgraders to
send outcome messag
CAY-2983 CayenneDataSource: Public-facing DataSource builder
CAY-2984 Remove "DataChannel" injectable dependency
CAY-2985 DataDomain init and structure cleanup
+CAY-2986 Cgen to run unconditionally
Bug Fixes:
diff --git a/UPGRADE.md b/UPGRADE.md
index f6931d43a..15583e4a4 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -111,6 +111,12 @@ DataMaps, filters and listeners can still be added (and
removed) after creation.
- `setMaxIdQualifierSize(int)` — use the `cayenne.max_id_qualifier_size`
property
(`Constants.MAX_ID_QUALIFIER_SIZE_PROPERTY`).
+* Per [CAY-2986](https://issues.apache.org/jira/browse/CAY-2986) cgen now runs
unconditionally. Previously it compared
+ the DataMap file mtime against the mtime of the generated classes and
skipped generation when the classes looked
+ newer. That optimization saved very little (cgen is idempotent and fast)
while regularly producing stale classes
+ after project upgrades or when switching between machines and branches.
Consequences:
+ - The `force` flag is now a deprecated no-op — its former behavior is the
only behavior.
+
## Upgrading to 5.0-M2
* Per [CAY-2947](https://issues.apache.org/jira/browse/CAY-2947) the
`cayenne-commitlog` artifact has been removed. Commit log support is now part
of the
diff --git a/ai-plugin/references/mcp-tools.md
b/ai-plugin/references/mcp-tools.md
index cad3c0c8a..9b628bf87 100644
--- a/ai-plugin/references/mcp-tools.md
+++ b/ai-plugin/references/mcp-tools.md
@@ -45,18 +45,20 @@ Runs Cayenne's class generator for one DataMap. Uses the
`<cgen>` config block e
```json
{
- "status": "ok" | "error",
- "summary": { "writtenCount": 3, "skippedCount": 12, "errorCount": 0 },
- "resolvedConfig": { "destDir": "..." },
- "writtenFiles": [{ "path": "...", "size": 1234 }],
- "skippedFiles": [{ "path": "...", "reason": "up-to-date" }],
- "errors": []
+ "status": "generated" | "up_to_date" | "validation_failed" | "error",
+ "summary": { "filesConsidered": 15, "filesWritten": 3 },
+ "files": [{ "path": "...", "kind": "entity_super", "sourceEntity": "Artist"
}],
+ "resolved": { "destDir": "..." },
+ "warnings": [],
+ "validation": { "projectFound": true, "dataMapFound": true,
"cgenConfigPresent": true,
+ "destDirSpecified": true, "destDirWritable": true },
+ "error": null
}
```
-Surface the `summary` to the user verbatim. List the first few `writtenFiles`
paths; full list is informational. If `errors` is non-empty, those are blocking
— show them.
+Surface the `summary` to the user verbatim. List the first few `files` paths;
full list is informational.
-When the DataMap had no `<cgen>` block, the tool generated from a synthesized
default config. The default destination is derived from the Maven layout
(`src/main/resources` → `src/main/java`); for non-Maven layouts it falls back
to the DataMap's own directory, so check `resolvedConfig.destDir` and confirm
with the user if it looks wrong. Offer to persist a `<cgen>` block (via
`cayenne-modeling` or the Modeler) if they want to customize destination,
templates, or filtering.
+When the DataMap had no `<cgen>` block, the tool generated from a synthesized
default config. The default destination is derived from the Maven layout
(`src/main/resources` → `src/main/java`); for non-Maven layouts it falls back
to the DataMap's own directory, so check `resolved.destDir` and confirm with
the user if it looks wrong. Offer to persist a `<cgen>` block (via
`cayenne-modeling` or the Modeler) if they want to customize destination,
templates, or filtering.
**Failure modes:**
diff --git a/ai-plugin/skills/cayenne-cgen/SKILL.md
b/ai-plugin/skills/cayenne-cgen/SKILL.md
index 73b833ee9..0997708da 100644
--- a/ai-plugin/skills/cayenne-cgen/SKILL.md
+++ b/ai-plugin/skills/cayenne-cgen/SKILL.md
@@ -59,12 +59,12 @@ If the tool is not available (MCP server not registered),
surface `cayenne-mcp-s
The tool returns structured JSON. Report:
-- `summary.writtenCount`, `summary.skippedCount`, `summary.errorCount`
verbatim — these are the headline.
-- The first few entries in `writtenFiles` (relative paths). Full list is
informational; offer to dump it if the user asks.
-- Any entries in `errors` — these are blocking. Read the messages and explain
in user terms (a missing entity class name, a bad template path, an invalid
`<destDir>`, etc.).
-- `resolvedConfig.destDir` — the absolute output directory. If the DataMap had
no `<cgen>` block, the tool generated from a synthesized default; report this
destination so the user can confirm it's right, and offer to persist a `<cgen>`
block (via `cayenne-modeling`) if they want to customize destination,
templates, or entity filtering.
+- `summary.filesConsidered` and `summary.filesWritten` verbatim — these are
the headline.
+- The first few entries in `files` (relative paths). Full list is
informational; offer to dump it if the user asks.
+- `error`, when non-null — this is blocking. Read the message and explain in
user terms (a missing entity class name, a bad template path, an invalid
`<destDir>`, etc.).
+- `resolved.destDir` — the absolute output directory. If the DataMap had no
`<cgen>` block, the tool generated from a synthesized default; report this
destination so the user can confirm it's right, and offer to persist a `<cgen>`
block (via `cayenne-modeling`) if they want to customize destination,
templates, or entity filtering.
-If `writtenCount` is 0 and `skippedCount` covers everything, say so — it means
everything is already up-to-date and no work was needed.
+cgen always runs in full, but rewrites a file only when the generated contents
differ from what is on disk. So `files` is exactly what changed, and a `status`
of `up_to_date` means the Java was already in sync with the model — say so
plainly rather than implying work was skipped.
## Step 4 — Next steps
diff --git
a/cayenne-ant/src/main/java/org/apache/cayenne/tools/CayenneGeneratorTask.java
b/cayenne-ant/src/main/java/org/apache/cayenne/tools/CayenneGeneratorTask.java
index be63d2ce2..cc063ab60 100644
---
a/cayenne-ant/src/main/java/org/apache/cayenne/tools/CayenneGeneratorTask.java
+++
b/cayenne-ant/src/main/java/org/apache/cayenne/tools/CayenneGeneratorTask.java
@@ -18,27 +18,27 @@
****************************************************************/
package org.apache.cayenne.tools;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
import org.apache.cayenne.configuration.xml.DataChannelMetaData;
import org.apache.cayenne.dbsync.filter.NamePatternMatcher;
import org.apache.cayenne.dbsync.reverse.configuration.ToolsModule;
import org.apache.cayenne.di.Injector;
import org.apache.cayenne.gen.ArtifactsGenerationMode;
-import org.apache.cayenne.gen.CgenConfiguration;
import org.apache.cayenne.gen.CgenConfigList;
+import org.apache.cayenne.gen.CgenConfiguration;
+import org.apache.cayenne.gen.CgenTemplate;
import org.apache.cayenne.gen.ClassGenerationAction;
import org.apache.cayenne.gen.ClassGenerationActionFactory;
-import org.apache.cayenne.gen.CgenTemplate;
import org.apache.cayenne.gen.TemplateType;
import org.apache.cayenne.map.DataMap;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.types.Path;
import org.slf4j.LoggerFactory;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
/**
* An Ant task to perform class generation based on CayenneDataMap.
*
@@ -73,11 +73,6 @@ public class CayenneGeneratorTask extends CayenneTask {
protected Boolean usepkgpath;
protected Boolean createpropertynames;
- /**
- * @since 4.1
- */
- private boolean force;
-
private boolean useConfigFromDataMap;
private transient Injector injector;
@@ -127,11 +122,6 @@ public class CayenneGeneratorTask extends CayenneTask {
CayenneGeneratorEmbeddableFilterAction filterEmbeddableAction
= new CayenneGeneratorEmbeddableFilterAction();
filterEmbeddableAction.setNameFilter(NamePatternMatcher.build(logger, null,
excludeEmbeddablesPattern));
generatorAction.setLogger(logger);
- if (force) {
- // will (re-)generate all files
- generatorAction.getCgenConfiguration().setForce(true);
- }
-
generatorAction.getCgenConfiguration().setTimestamp(map.lastModified());
if (!hasConfig() && useConfigFromDataMap) {
generatorAction.prepareArtifacts();
} else {
@@ -161,7 +151,7 @@ public class CayenneGeneratorTask extends CayenneTask {
makepairs != null || mode != null || outputPattern != null ||
overwrite != null || superpkg != null ||
supertemplate != null || template != null ||
embeddabletemplate != null || embeddablesupertemplate != null ||
usepkgpath != null || createpropertynames != null ||
datamaptemplate != null ||
- datamapsupertemplate != null || createpkproperties != null ||
force || externaltoolconfig != null;
+ datamapsupertemplate != null || createpkproperties != null ||
externaltoolconfig != null;
}
List<CgenConfiguration> buildConfigurations(DataMap dataMap) {
@@ -390,8 +380,15 @@ public class CayenneGeneratorTask extends CayenneTask {
this.createpkproperties = createpkproperties;
}
+ /**
+ * does nothing
+ *
+ * @param force not used
+ * @deprecated cgen runs unconditionally
+ */
+ @Deprecated(since = "5.0", forRemoval = true)
public void setForce(boolean force) {
- this.force = force;
+ log("'force' is deprecated and ignored. cgen always regenerates
classes.");
}
/**
diff --git
a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java
b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java
index 6d2fd810e..b19121edd 100644
--- a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java
@@ -86,11 +86,9 @@ public class CgenConfiguration implements Serializable,
XMLSerializable {
* @since 5.0 renamed from querySuperTemplate
*/
private CgenTemplate dataMapSuperTemplate;
- private long timestamp;
private String outputPattern;
private String encoding;
private boolean createPropertyNames;
- private boolean force; // force run generator
/**
* @since 4.1
*/
@@ -106,7 +104,6 @@ public class CgenConfiguration implements Serializable,
XMLSerializable {
// isDefault() method should be in sync with the following values
this.outputPattern = "*.java";
- this.timestamp = 0L;
this.usePkgPath = true;
this.makePairs = true;
this.createPKProperties = true;
@@ -357,14 +354,6 @@ public class CgenConfiguration implements Serializable,
XMLSerializable {
this.dataMapSuperTemplate = dataMapSuperTemplate;
}
- public long getTimestamp() {
- return timestamp;
- }
-
- public void setTimestamp(long timestamp) {
- this.timestamp = timestamp;
- }
-
public String getOutputPattern() {
return outputPattern;
}
@@ -389,12 +378,23 @@ public class CgenConfiguration implements Serializable,
XMLSerializable {
this.createPropertyNames = createPropertyNames;
}
+ /**
+ * @return false
+ * @deprecated cgen always regenerates classes, so there is nothing left
to force
+ */
+ @Deprecated(since = "5.0", forRemoval = true)
public boolean isForce() {
- return force;
+ return true;
}
+ /**
+ * does nothing
+ *
+ * @param force not used
+ * @deprecated cgen always regenerates classes, so there is nothing left
to force
+ */
+ @Deprecated(since = "5.0", forRemoval = true)
public void setForce(boolean force) {
- this.force = force;
}
public boolean isCreatePKProperties() {
diff --git
a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClassGenerationAction.java
b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClassGenerationAction.java
index 93b081f82..450e92a33 100644
---
a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClassGenerationAction.java
+++
b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClassGenerationAction.java
@@ -37,12 +37,12 @@ import
org.apache.velocity.tools.config.FactoryConfiguration;
import org.slf4j.Logger;
import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.OutputStreamWriter;
import java.io.Writer;
+import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@@ -308,34 +308,33 @@ public class ClassGenerationAction {
return null;
}
- if (logger != null) {
- String label = templateType.isSuperclass() ? "superclass" :
"class";
- logger.info("Generating {} file: {}", label,
outFile.getCanonicalPath());
- }
-
- // return writer with specified encoding
- FileOutputStream out = new FileOutputStream(outFile);
+ return new GeneratedFileWriter(outFile, templateType);
+ }
- return (cgenConfiguration.getEncoding() != null) ? new
OutputStreamWriter(out, cgenConfiguration.getEncoding()) : new
OutputStreamWriter(out);
+ /**
+ * A callback invoked after a generated file was written to disk. Files
whose generated contents match
+ * what is already on disk are left alone, so this is only called for
files that actually changed.
+ *
+ * @since 5.0
+ */
+ protected void fileWritten(File file, TemplateType templateType) {
}
/**
- * Returns a target file where a generated superclass must be saved. If
null
- * is returned, class shouldn't be generated.
+ * Returns a target file where a generated superclass must be saved.
Superclasses are always
+ * regenerated, overwriting any previous version.
*/
private File fileForSuperclass() throws Exception {
String packageName = (String) context.get(Artifact.SUPER_PACKAGE_KEY);
String className = (String) context.get(Artifact.SUPER_CLASS_KEY);
- String filename =
StringUtils.getInstance().replaceWildcardInStringWithString(WILDCARD,
cgenConfiguration.getOutputPattern(), className);
- File dest = new
File(mkpath(cgenConfiguration.buildOutputPath().toFile(), packageName),
filename);
-
- if (dest.exists() && !fileNeedUpdate(dest,
cgenConfiguration.getSuperTemplate().getData())) {
- return null;
- }
+ File dir = mkpath(cgenConfiguration.buildOutputPath().toFile(),
packageName);
+ String fileName = StringUtils
+ .getInstance()
+ .replaceWildcardInStringWithString(WILDCARD,
cgenConfiguration.getOutputPattern(), className);
- return dest;
+ return new File(dir, fileName);
}
/**
@@ -360,43 +359,11 @@ public class ClassGenerationAction {
if (!cgenConfiguration.isOverwrite()) {
return null;
}
-
- if (!fileNeedUpdate(dest,
cgenConfiguration.getTemplate().getData())) {
- return null;
- }
}
return dest;
}
- /**
- * Ignore if the destination is newer than the map
- * (internal timestamp), i.e. has been generated after the map was
- * last saved AND the template is older than the destination file
- */
- protected boolean fileNeedUpdate(File dest, String templateFileName) {
- if (cgenConfiguration.isForce()) {
- return true;
- }
-
- if (isOld(dest)) {
- if (templateFileName == null) {
- return false;
- }
-
- File templateFile = new File(templateFileName);
- return templateFile.lastModified() >= dest.lastModified();
- }
- return true;
- }
-
- /**
- * Is file modified after internal timestamp (usually equal to mtime of
datamap file)
- */
- protected boolean isOld(File file) {
- return file.lastModified() > cgenConfiguration.getTimestamp();
- }
-
/**
* Returns a File object corresponding to a directory where files that
* belong to <code>pkgName</code> package should reside. Creates any
missing
@@ -462,4 +429,54 @@ public class ClassGenerationAction {
public MetadataUtils getMetadataUtils() {
return metadataUtils;
}
+
+ /**
+ * Collects generated class text in memory, and on close writes it to the
target file, but only if it
+ * differs from what the file already contains.
+ */
+ private class GeneratedFileWriter extends Writer {
+
+ private final File file;
+ private final TemplateType templateType;
+ private final StringBuilder buffer;
+
+ GeneratedFileWriter(File file, TemplateType templateType) {
+ this.file = file;
+ this.templateType = templateType;
+ this.buffer = new StringBuilder();
+ }
+
+ @Override
+ public void write(char[] chars, int offset, int length) {
+ buffer.append(chars, offset, length);
+ }
+
+ @Override
+ public void flush() {
+ // nothing to flush - the file is written on close
+ }
+
+ @Override
+ public void close() throws IOException {
+
+ String encoding = cgenConfiguration.getEncoding();
+ Charset charset = encoding != null ? Charset.forName(encoding) :
Charset.defaultCharset();
+ byte[] generated = buffer.toString().getBytes(charset);
+
+ if (file.isFile()
+ && file.length() == generated.length
+ && Arrays.equals(generated,
Files.readAllBytes(file.toPath()))) {
+ return;
+ }
+
+ Files.write(file.toPath(), generated);
+
+ if (logger != null) {
+ String label = templateType.isSuperclass() ? "superclass" :
"class";
+ logger.info("Generating {} file: {}", label,
file.getCanonicalPath());
+ }
+
+ fileWritten(file, templateType);
+ }
+ }
}
diff --git
a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java
b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java
index 39e96cee1..963a40622 100644
---
a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java
+++
b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java
@@ -20,6 +20,7 @@
package org.apache.cayenne.gen;
import java.io.*;
+import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
@@ -38,8 +39,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
public class ClassGenerationActionTest extends CgenCase {
@@ -225,64 +224,62 @@ public class ClassGenerationActionTest extends CgenCase {
}
@Test
- public void isOld() {
- File file = mock(File.class);
- when(file.lastModified()).thenReturn(1000L);
-
- cgenConfiguration.setTimestamp(0);
- assertTrue(action.isOld(file));
-
- cgenConfiguration.setTimestamp(2000L);
- assertFalse(action.isOld(file));
- }
-
- @Test
- public void fileNeedUpdate() {
- File file = mock(File.class);
- when(file.lastModified()).thenReturn(1000L);
-
- cgenConfiguration.setTimestamp(0);
- cgenConfiguration.setForce(false);
-
- assertFalse(action.fileNeedUpdate(file, null));
-
- cgenConfiguration.setTimestamp(2000L);
- cgenConfiguration.setForce(false);
+ public void fileForSuperclass() throws Exception {
- assertTrue(action.fileNeedUpdate(file, null));
+ TemplateType templateType = TemplateType.DATAMAP_SUPERCLASS;
- cgenConfiguration.setTimestamp(0);
- cgenConfiguration.setForce(true);
+ cgenConfiguration.setRootPath(tempFolder.toPath());
+ cgenConfiguration.updateOutputPath(Paths.get("."));
+ action = new ClassGenerationAction(cgenConfiguration);
+ ObjEntity testEntity1 = new ObjEntity("TEST");
+ testEntity1.setClassName("TestClass1");
+ action.context.put(Artifact.SUPER_PACKAGE_KEY, "");
+ action.context.put(Artifact.SUPER_CLASS_KEY, "TestClass1");
- assertTrue(action.fileNeedUpdate(file, null));
+ File outFile = new File(tempFolder + "/TestClass1.java");
+ assertFalse(outFile.exists());
- cgenConfiguration.setTimestamp(2000L);
- cgenConfiguration.setForce(true);
+ try(Writer out = action.openWriter(templateType)) {
+ out.write("// generated");
+ // the file is written on close, not on open
+ assertFalse(outFile.exists());
+ }
+ assertEquals("// generated",
Files.readString(outFile.toPath()));
- assertTrue(action.fileNeedUpdate(file, null));
+ // superclasses are regenerated unconditionally
+ try(Writer out = action.openWriter(templateType)) {
+ assertNotNull(out);
+ out.write("// regenerated");
+ }
+ assertEquals("// regenerated",
Files.readString(outFile.toPath()));
}
@Test
- public void fileForSuperclass() throws Exception {
+ public void unchangedFileLeftAlone() throws Exception {
TemplateType templateType = TemplateType.DATAMAP_SUPERCLASS;
cgenConfiguration.setRootPath(tempFolder.toPath());
cgenConfiguration.updateOutputPath(Paths.get("."));
action = new ClassGenerationAction(cgenConfiguration);
- ObjEntity testEntity1 = new ObjEntity("TEST");
- testEntity1.setClassName("TestClass1");
action.context.put(Artifact.SUPER_PACKAGE_KEY, "");
action.context.put(Artifact.SUPER_CLASS_KEY, "TestClass1");
File outFile = new File(tempFolder + "/TestClass1.java");
- assertFalse(outFile.exists());
+ try(Writer out = action.openWriter(templateType)) {
+ out.write("// generated");
+ }
+
+ // backdate the file, so that an actual rewrite would be
detectable
+ assertTrue(outFile.setLastModified(outFile.lastModified() -
5_000L));
+ long backdated = outFile.lastModified();
- try(Writer ignored = action.openWriter(templateType)) {
- assertTrue(outFile.exists());
+ try(Writer out = action.openWriter(templateType)) {
+ out.write("// generated");
}
- assertNull(action.openWriter(templateType));
+ assertEquals(backdated, outFile.lastModified(), "Identical file
must not be rewritten");
+ assertEquals("// generated",
Files.readString(outFile.toPath()));
}
@Test
@@ -301,16 +298,24 @@ public class ClassGenerationActionTest extends CgenCase {
File outFile = new File(tempFolder + "/TestClass1.java");
assertFalse(outFile.exists());
- try(Writer ignored = action.openWriter(templateType)) {
- assertTrue(outFile.exists());
+ try(Writer out = action.openWriter(templateType)) {
+ out.write("// generated");
}
+ assertTrue(outFile.exists());
+ // existing subclasses are preserved when generating class
pairs...
assertNull(action.openWriter(templateType));
+ // ... and when generating single classes without "overwrite"
cgenConfiguration.setMakePairs(false);
assertNull(action.openWriter(templateType));
+ // "overwrite" regenerates the subclass unconditionally
cgenConfiguration.setOverwrite(true);
- assertNull(action.openWriter(templateType));
+ try(Writer out = action.openWriter(templateType)) {
+ assertNotNull(out);
+ out.write("// regenerated");
+ }
+ assertEquals("// regenerated",
Files.readString(outFile.toPath()));
}
}
diff --git
a/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/CgenTask.java
b/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/CgenTask.java
index b5100bf00..227834b57 100644
--- a/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/CgenTask.java
+++ b/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/CgenTask.java
@@ -198,10 +198,12 @@ public class CgenTask extends BaseCayenneTask {
private Boolean createPropertyNames;
/**
- * Force run (skip check for files modification time)
+ * No longer has any effect - cgen always regenerates classes.
*
* @since 4.1
+ * @deprecated cgen runs unconditionally
*/
+ @Deprecated(since = "5.0", forRemoval = true)
@Input
private boolean force;
@@ -260,6 +262,10 @@ public class CgenTask extends BaseCayenneTask {
public void generate() {
File dataMapFile = getDataMapFile();
+ if (force || getProject().hasProperty("force")) {
+ getLogger().warn("'force' is deprecated and ignored. cgen always
regenerates classes.");
+ }
+
injector = new ToolsInjectorBuilder()
.addModule(new
ToolsModule(LoggerFactory.getLogger(CgenTask.class)))
.create();
@@ -280,11 +286,6 @@ public class CgenTask extends BaseCayenneTask {
filterEmbeddableAction.setNameFilter(NamePatternMatcher.build(getLogger(),
null, excludeEmbeddables));
generator.setLogger(getLogger());
- if (this.force || getProject().hasProperty("force")) {
- generator.getCgenConfiguration().setForce(true);
- }
-
generator.getCgenConfiguration().setTimestamp(dataMapFile.lastModified());
-
if (!hasConfig() && useConfigFromDataMap) {
generator.prepareArtifacts();
} else {
@@ -399,7 +400,7 @@ public class CgenTask extends BaseCayenneTask {
return destDir != null || destDirName != null || encoding != null ||
excludeEntities != null || excludeEmbeddables != null || includeEntities !=
null ||
makePairs != null || mode != null || outputPattern != null ||
overwrite != null || superPkg != null ||
superTemplate != null || template != null ||
embeddableTemplate != null || embeddableSuperTemplate != null ||
- usePkgPath != null || createPropertyNames != null || force ||
dataMapTemplate != null ||
+ usePkgPath != null || createPropertyNames != null ||
dataMapTemplate != null ||
dataMapSuperTemplate != null || createPKProperties != null ||
externalToolConfig != null;
}
@@ -723,14 +724,26 @@ public class CgenTask extends BaseCayenneTask {
setCreatePropertyNames(createPropertyNames);
}
+ /**
+ * @deprecated cgen runs unconditionally
+ */
+ @Deprecated(since = "5.0", forRemoval = true)
public boolean isForce() {
return force;
}
+ /**
+ * @deprecated cgen runs unconditionally
+ */
+ @Deprecated(since = "5.0", forRemoval = true)
public void setForce(boolean force) {
this.force = force;
}
+ /**
+ * @deprecated cgen runs unconditionally
+ */
+ @Deprecated(since = "5.0", forRemoval = true)
public void force(boolean force) {
setForce(force);
}
diff --git
a/cayenne-maven-plugin/src/main/java/org/apache/cayenne/tools/CayenneGeneratorMojo.java
b/cayenne-maven-plugin/src/main/java/org/apache/cayenne/tools/CayenneGeneratorMojo.java
index bd6bf2326..b7a6576cc 100644
---
a/cayenne-maven-plugin/src/main/java/org/apache/cayenne/tools/CayenneGeneratorMojo.java
+++
b/cayenne-maven-plugin/src/main/java/org/apache/cayenne/tools/CayenneGeneratorMojo.java
@@ -19,21 +19,16 @@
package org.apache.cayenne.tools;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
import org.apache.cayenne.configuration.xml.DataChannelMetaData;
import org.apache.cayenne.dbsync.filter.NamePatternMatcher;
import org.apache.cayenne.dbsync.reverse.configuration.ToolsModule;
import org.apache.cayenne.di.Injector;
import org.apache.cayenne.gen.ArtifactsGenerationMode;
-import org.apache.cayenne.gen.CgenConfiguration;
import org.apache.cayenne.gen.CgenConfigList;
+import org.apache.cayenne.gen.CgenConfiguration;
+import org.apache.cayenne.gen.CgenTemplate;
import org.apache.cayenne.gen.ClassGenerationAction;
import org.apache.cayenne.gen.ClassGenerationActionFactory;
-import org.apache.cayenne.gen.CgenTemplate;
import org.apache.cayenne.gen.TemplateType;
import org.apache.cayenne.map.DataMap;
import org.apache.maven.plugin.AbstractMojo;
@@ -45,6 +40,11 @@ import org.apache.maven.plugins.annotations.Parameter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
/**
* Maven mojo to perform class generation from data cgenConfiguration. This
class is an Maven
* adapter to DefaultClassGenerator class.
@@ -201,11 +201,12 @@ public class CayenneGeneratorMojo extends AbstractMojo {
private Boolean createPropertyNames;
/**
- * If set to <code>true</code>, will skip file modification time
validation and regenerate all.
- * Default is <code>false</code>.
+ * No longer has any effect - cgen always regenerates classes.
*
* @since 4.1
+ * @deprecated cgen runs unconditionally
*/
+ @Deprecated(since = "5.0", forRemoval = true)
@Parameter(defaultValue = "false", property = "force")
private boolean force;
@@ -264,6 +265,10 @@ public class CayenneGeneratorMojo extends AbstractMojo {
.create();
Logger logger = new MavenLogger(this);
+ if (force) {
+ logger.warn("'force' is deprecated and ignored. cgen always
regenerates classes.");
+ }
+
CayenneGeneratorMapLoaderAction loaderAction = new
CayenneGeneratorMapLoaderAction(injector);
loaderAction.setMainDataMapFile(map);
@@ -278,11 +283,6 @@ public class CayenneGeneratorMojo extends AbstractMojo {
filterEmbeddableAction.setNameFilter(NamePatternMatcher.build(logger, null,
excludeEmbeddables));
generator.setLogger(logger);
- if (force) {
- // will (re-)generate all files
- generator.getCgenConfiguration().setForce(true);
- }
-
generator.getCgenConfiguration().setTimestamp(map.lastModified());
if (!hasConfig() && useConfigFromDataMap) {
generator.prepareArtifacts();
} else {
@@ -319,7 +319,7 @@ public class CayenneGeneratorMojo extends AbstractMojo {
return destDir != null || encoding != null || excludeEntities != null
|| excludeEmbeddables != null || includeEntities != null ||
makePairs != null || mode != null || outputPattern != null ||
overwrite != null || superPkg != null ||
superTemplate != null || template != null ||
embeddableTemplate != null || embeddableSuperTemplate != null ||
- usePkgPath != null || createPropertyNames != null || force ||
dataMapTemplate != null ||
+ usePkgPath != null || createPropertyNames != null ||
dataMapTemplate != null ||
dataMapSuperTemplate != null || createPKProperties != null ||
externalToolConfig != null;
}
diff --git a/cayenne-mcp-server/PROTOCOL.md b/cayenne-mcp-server/PROTOCOL.md
index 3d3e7b4e9..6aea9a33d 100644
--- a/cayenne-mcp-server/PROTOCOL.md
+++ b/cayenne-mcp-server/PROTOCOL.md
@@ -57,8 +57,9 @@ Both fields are required.
```
`status` semantics:
-- `generated` — at least one file was written; `files` is non-empty
-- `up_to_date` — all candidate files were already current; `files` is empty
+- `generated` — at least one file changed on disk; `files` lists exactly those
files
+- `up_to_date` — cgen ran in full, but every generated file was byte-identical
to what was already there,
+ so nothing was touched; `files` is empty
- `validation_failed` — a pre-flight check failed; the failing slot in
`validation` is `false`, later slots are `null`
- `error` — cgen started but threw mid-run; `files` lists what was written
before the failure
diff --git
a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/CgenRunTool.java
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/CgenRunTool.java
index 56017064f..26a73ad45 100644
---
a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/CgenRunTool.java
+++
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/CgenRunTool.java
@@ -167,18 +167,6 @@ public class CgenRunTool {
? CgenConfiguration.createDefault(dataMap,
CgenConfiguration.defaultOutputDir(dataMap))
: configList.getAll().getFirst();
- // Set the DataMap file's mtime as the timestamp so fileNeedUpdate()
can detect DataMap changes correctly.
- if (dataMap.getConfigurationSource() != null) {
- try {
- Path dataMapFile =
Path.of(dataMap.getConfigurationSource().getURL().toURI());
-
cgenConfig.setTimestamp(Files.getLastModifiedTime(dataMapFile).toMillis());
- } catch (Exception e) {
- // URI conversion failed (e.g. non-file: URL) or some problems
with file mtime read,
- // better to regen all, than silently fail.
- cgenConfig.setForce(true);
- }
- }
-
// Step 5 — destDir specified?
Path destDir = cgenConfig.buildOutputPath();
if (destDir == null) {
diff --git
a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/InstrumentedClassGenerationAction.java
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/InstrumentedClassGenerationAction.java
index d37cc0064..e8c3b9845 100644
---
a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/InstrumentedClassGenerationAction.java
+++
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/InstrumentedClassGenerationAction.java
@@ -22,7 +22,6 @@ import org.apache.cayenne.gen.Artifact;
import org.apache.cayenne.gen.ArtifactGenerationMode;
import org.apache.cayenne.gen.CgenConfiguration;
import org.apache.cayenne.gen.ClassGenerationAction;
-import org.apache.cayenne.gen.StringUtils;
import org.apache.cayenne.gen.TemplateType;
import org.apache.cayenne.map.Embeddable;
import org.apache.cayenne.map.ObjEntity;
@@ -30,15 +29,13 @@ import
org.apache.cayenne.mcp.tools.cgen.protocol.CgenFileEntry;
import org.apache.cayenne.mcp.tools.cgen.protocol.CgenFileKind;
import java.io.File;
-import java.io.Writer;
-import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/**
- * Extends {@link ClassGenerationAction} to record which files cgen wrote and
how many
- * were considered. Placed in {@code org.apache.cayenne.gen} so it shares the
same Java
- * package as {@link ClassGenerationAction} and can read its protected fields.
+ * Extends {@link ClassGenerationAction} to record which files cgen changed
and how many were
+ * considered. Cgen rewrites a file only when its generated contents differ
from what is on disk,
+ * so the recorded files are the ones that actually changed.
*
* @since 5.0
*/
@@ -63,12 +60,11 @@ class InstrumentedClassGenerationAction extends
ClassGenerationAction {
}
@Override
- protected Writer openWriter(TemplateType templateType) throws Exception {
- Writer w = super.openWriter(templateType);
- if (w != null && currentArtifact != null) {
- writtenFiles.add(buildFileEntry(currentArtifact, templateType));
+ protected void fileWritten(File file, TemplateType templateType) {
+ if (currentArtifact != null) {
+ writtenFiles.add(new CgenFileEntry(
+ file.getAbsolutePath(), toKind(templateType),
sourceName(currentArtifact)));
}
- return w;
}
public List<CgenFileEntry> getWrittenFiles() {
@@ -90,28 +86,6 @@ class InstrumentedClassGenerationAction extends
ClassGenerationAction {
return count;
}
- private CgenFileEntry buildFileEntry(Artifact artifact, TemplateType
templateType) {
- String packageName;
- String className;
- if (templateType.isSuperclass()) {
- packageName = (String) context.get(Artifact.SUPER_PACKAGE_KEY);
- className = (String) context.get(Artifact.SUPER_CLASS_KEY);
- } else {
- packageName = (String) context.get(Artifact.SUB_PACKAGE_KEY);
- className = (String) context.get(Artifact.SUB_CLASS_KEY);
- }
-
- String filename =
StringUtils.getInstance().replaceWildcardInStringWithString(
- "*", cgenConfiguration.getOutputPattern(), className);
- Path dir = cgenConfiguration.buildOutputPath();
- if (cgenConfiguration.isUsePkgPath() && packageName != null) {
- dir = dir.resolve(packageName.replace('.', File.separatorChar));
- }
- Path filePath = dir.resolve(filename).toAbsolutePath();
-
- return new CgenFileEntry(filePath.toString(), toKind(templateType),
sourceName(artifact));
- }
-
private static CgenFileKind toKind(TemplateType type) {
return switch (type) {
case ENTITY_SUPERCLASS -> CgenFileKind.entity_super;
diff --git
a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunIT.java
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunIT.java
index 3401918c2..30a00281f 100644
---
a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunIT.java
+++
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunIT.java
@@ -28,7 +28,6 @@ import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.attribute.FileTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@@ -88,7 +87,10 @@ public class CgenRunIT {
@Test
public void upToDateOnSecondRun() {
- tool.run(projectFile.toString(), "PersonMap");
+ CgenRunResult first = tool.run(projectFile.toString(), "PersonMap");
+
+ // cgen re-runs in full, but the regenerated content is identical, so
nothing is touched on disk
+ long[] mtimes = first.files().stream().mapToLong(e ->
Path.of(e.path()).toFile().lastModified()).toArray();
CgenRunResult second = tool.run(projectFile.toString(), "PersonMap");
@@ -97,6 +99,33 @@ public class CgenRunIT {
assertTrue(second.files().isEmpty());
assertTrue(second.summary().filesConsidered() > 0);
assertNull(second.error());
+
+ for (int i = 0; i < mtimes.length; i++) {
+ assertEquals(mtimes[i],
Path.of(first.files().get(i).path()).toFile().lastModified(),
+ "Unchanged file must not be rewritten: " +
first.files().get(i).path());
+ }
+ }
+
+ @Test
+ public void regeneratesAfterDataMapChange() throws IOException {
+ CgenRunResult first = tool.run(projectFile.toString(), "PersonMap");
+ assertEquals("generated", first.status());
+
+ // add an attribute to the entity - the superclass must pick it up
+ Path dataMapFile = tempDir.resolve("PersonMap.map.xml");
+ Files.writeString(dataMapFile, Files.readString(dataMapFile).replace(
+ "<obj-entity name=\"Person\"
className=\"com.example.Person\"/>",
+ """
+ <obj-entity name="Person" className="com.example.Person">
+ <obj-attribute name="nickname"
type="java.lang.String"/>
+ </obj-entity>"""));
+
+ CgenRunResult second = tool.run(projectFile.toString(), "PersonMap");
+
+ assertEquals("generated", second.status());
+ assertEquals(List.of(CgenFileKind.entity_super),
+ second.files().stream().map(CgenFileEntry::kind).toList());
+
assertTrue(Files.readString(Path.of(second.files().getFirst().path())).contains("nickname"));
}
@Test
@@ -118,30 +147,6 @@ public class CgenRunIT {
assertTrue(skipped >= 1, "At least one file (the existing subclass)
should have been skipped");
}
- @Test
- public void regeneratesAfterDataMapChange() throws IOException {
- // First run — generates files
- CgenRunResult first = tool.run(projectFile.toString(), "PersonMap");
- assertEquals("generated", first.status());
-
- // Bump the DataMap's mtime to be clearly newer than the generated
files.
- // Use setLastModifiedTime rather than a wall-clock sleep to avoid
- // filesystem mtime granularity issues (Windows has 1-second
resolution).
- long maxGeneratedMtime = first.files().stream()
- .mapToLong(e -> Path.of(e.path()).toFile().lastModified())
- .max()
- .orElseThrow();
- Path dataMapFile = tempDir.resolve("PersonMap.map.xml");
- Files.setLastModifiedTime(dataMapFile,
FileTime.fromMillis(maxGeneratedMtime + 5_000L));
-
- // Second run — must detect that the DataMap is newer than the
generated files
- // and regenerate the superclass(es).
- CgenRunResult second = tool.run(projectFile.toString(), "PersonMap");
- assertEquals("generated", second.status(),
- "Expected regeneration after DataMap mtime was bumped past
generated files");
- assertTrue(second.summary().filesWritten() > 0);
- }
-
@Test
public void generatesWithDefaultConfigWhenNoCgenBlock() throws IOException
{
// A DataMap with no <cgen> block, laid out in a standard Maven
structure. The tool must
diff --git
a/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part6/maven-plugin.adoc
b/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part6/maven-plugin.adoc
index 5d8b2674f..56be88a8e 100644
---
a/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part6/maven-plugin.adoc
+++
b/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part6/maven-plugin.adoc
@@ -113,7 +113,7 @@ ${project.basedir}/src/main/resources/my.map.xml
.^|force
.^|boolean
-.^|If set to "true", will force run from maven/gradle.
+.^|_Deprecated and ignored since 5.0._ cgen always regenerates classes, so
there is nothing left to force.
.^|createPKProperties
.^|boolean
diff --git
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datadomain/cgen/DataDomainCgenTab.java
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datadomain/cgen/DataDomainCgenTab.java
index 01f37b8aa..c8e5bf755 100644
---
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datadomain/cgen/DataDomainCgenTab.java
+++
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datadomain/cgen/DataDomainCgenTab.java
@@ -64,7 +64,6 @@ public class DataDomainCgenTab extends
DataDomainGeneratorsTab<CgenConfiguration
cgenConfigList.add(createConfiguration(dataMap));
}
for (CgenConfiguration cgenConfiguration :
cgenConfigList.getAll()) {
- cgenConfiguration.setForce(true);
ClassGenerationAction action =
actionFactory.createAction(cgenConfiguration);
action.prepareArtifacts();
action.execute();
diff --git
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datamap/cgen/CgenPanel.java
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datamap/cgen/CgenPanel.java
index 3e360faaa..f153b2fb7 100644
---
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datamap/cgen/CgenPanel.java
+++
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ui/project/editor/datamap/cgen/CgenPanel.java
@@ -249,7 +249,6 @@ public class CgenPanel extends ProjectPanel implements
ObjEntityListener, Embedd
cgenConfiguration.resolveExcludedEmbeddables();
addToSelectedEntities(cgenConfiguration.getEntities());
addToSelectedEmbeddables(cgenConfiguration.getEmbeddables());
- cgenConfiguration.setForce(true);
return;
}
@@ -438,7 +437,6 @@ public class CgenPanel extends ProjectPanel implements
ObjEntityListener, Embedd
private CgenConfiguration createDefaultCgenConfiguration(DataMap map) {
Path basePath = map.getLocation() != null ? CgenOps.baseDir(session) :
null;
CgenConfiguration configuration = CgenConfiguration.createDefault(map,
basePath);
- configuration.setForce(true);
configuration.setEncoding(new
GeneralPrefs(app.getPrefsLocator().appNode(GeneralPrefs.NODE)).getEncoding());
return configuration;
}