From 82df32def508e3a04d475929d4febb0017902278 Mon Sep 17 00:00:00 2001 From: Stefan Bischof Date: Mon, 11 May 2026 09:46:42 +0200 Subject: [PATCH] Fix cross-module EPackage state leak in EmfGenerateMojo Clear nsURIs this plugin added to EPackage.Registry.INSTANCE at the start of each mojo invocation so modules don't see stale EPackages via the ResourceSet delegate. Add diagnostic logging. Signed-off-by: Stefan Bischof --- .../model/consumer.ecore | 47 ++++ .../pom.xml | 111 ++++++++++ emf/codegen.maven.example/pom.xml | 1 + .../tooling/emf/codegen/EmfGenerateMojo.java | 208 +++++++++++++++++- 4 files changed, 361 insertions(+), 6 deletions(-) create mode 100644 emf/codegen.maven.example/ecore.dependencies.subpackages.consumer.subpackages/model/consumer.ecore create mode 100644 emf/codegen.maven.example/ecore.dependencies.subpackages.consumer.subpackages/pom.xml diff --git a/emf/codegen.maven.example/ecore.dependencies.subpackages.consumer.subpackages/model/consumer.ecore b/emf/codegen.maven.example/ecore.dependencies.subpackages.consumer.subpackages/model/consumer.ecore new file mode 100644 index 0000000..1554c80 --- /dev/null +++ b/emf/codegen.maven.example/ecore.dependencies.subpackages.consumer.subpackages/model/consumer.ecore @@ -0,0 +1,47 @@ + + + +
+
+ + + +
+
+ + + + + + + + + + + +
+
+ + + + + + + + + + diff --git a/emf/codegen.maven.example/ecore.dependencies.subpackages.consumer.subpackages/pom.xml b/emf/codegen.maven.example/ecore.dependencies.subpackages.consumer.subpackages/pom.xml new file mode 100644 index 0000000..e279cc6 --- /dev/null +++ b/emf/codegen.maven.example/ecore.dependencies.subpackages.consumer.subpackages/pom.xml @@ -0,0 +1,111 @@ + + + + 4.0.0 + + + org.eclipse.daanse + org.eclipse.daanse.tooling.emf.codegen.maven.example + 0.0.1-SNAPSHOT + + + + org.eclipse.daanse.tooling.emf.codegen.maven.example.ecore.dependencies.subpackages.consumer.subpackages + jar + + Daanse EMF Codegen Example - Consumer with Subpackages and Cross-Module + Nested Reference + Regression test for the case where the consumer ecore has its + own sub-packages AND references classifiers in nested sub-packages of an + external dependency, with sibling-subpackage cross-references inside the + consumer (mirrors the cwm.governance.gdpr structure). + + + + org.eclipse.emf + org.eclipse.emf.common + + + org.eclipse.emf + org.eclipse.emf.ecore + + + + org.eclipse.daanse + + org.eclipse.daanse.tooling.emf.codegen.maven.example.ecore.simple.subpackages + ${project.version} + + + org.eclipse.fennec.emf + org.eclipse.fennec.emf.osgi.api + 0.1.1 + + + org.eclipse.fennec.emf + org.eclipse.fennec.emf.osgi.component + runtime + + + + + + + org.eclipse.daanse + org.eclipse.daanse.tooling.emf.codegen.maven + ${project.version} + + + + generate + + + model/consumer.ecore + org.eclipse.daanse.example.consumer + target/generated-sources/emf + + + + + + biz.aQute.bnd + biz.aQute.bndlib + 7.1.0 + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.0 + + + generate-sources + + add-source + + + + target/generated-sources/emf + + + + + + + + + diff --git a/emf/codegen.maven.example/pom.xml b/emf/codegen.maven.example/pom.xml index 62207e0..99b7787 100644 --- a/emf/codegen.maven.example/pom.xml +++ b/emf/codegen.maven.example/pom.xml @@ -40,6 +40,7 @@ ecore.dependencies ecore.dependencies.subpackages + ecore.dependencies.subpackages.consumer.subpackages genmodel.dependencies diff --git a/emf/codegen.maven/src/main/java/org/eclipse/daanse/tooling/emf/codegen/EmfGenerateMojo.java b/emf/codegen.maven/src/main/java/org/eclipse/daanse/tooling/emf/codegen/EmfGenerateMojo.java index f81d6b0..694f6f5 100644 --- a/emf/codegen.maven/src/main/java/org/eclipse/daanse/tooling/emf/codegen/EmfGenerateMojo.java +++ b/emf/codegen.maven/src/main/java/org/eclipse/daanse/tooling/emf/codegen/EmfGenerateMojo.java @@ -40,6 +40,7 @@ import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; import org.eclipse.emf.codegen.ecore.generator.Generator; +import org.eclipse.emf.codegen.ecore.genmodel.GenClassifier; import org.eclipse.emf.codegen.ecore.genmodel.GenModel; import org.eclipse.emf.codegen.ecore.genmodel.GenModelFactory; import org.eclipse.emf.codegen.ecore.genmodel.GenModelPackage; @@ -128,6 +129,18 @@ public class EmfGenerateMojo extends AbstractMojo { */ private static final String MODEL_FOLDER = "model"; + /** + * Tracks every nsURI this plugin has put into {@link EPackage.Registry#INSTANCE} + * across mojo invocations in the same JVM. At the start of each invocation we + * remove these entries so that the next module starts with a clean view: its + * fresh ResourceSet's package registry no longer reports {@code containsKey=true} + * via the delegate for nsURIs registered by an earlier session, and EMF's proxy + * resolution can find the freshly loaded EPackages instead of stale ones. + * Static so it survives the per-lookup mojo instantiation strategy. + */ + private static final java.util.Set pluginRegisteredNsURIs = + java.util.Collections.synchronizedSet(new java.util.HashSet<>()); + /** * Registry mapping EPackage nsURI to GenPackage for dependency resolution. */ @@ -429,9 +442,25 @@ private Optional generateFromEcore(File baseDir) { } } + getLog().info("Before saveGenModelToResources: getGenPackages=" + genModel.getGenPackages().size() + + " usedGenPackages=" + genModel.getUsedGenPackages().size()); + // Save the GenModel to resources for inclusion in JAR saveGenModelToResources(genModel, ePackage); + getLog().info("After saveGenModelToResources: getGenPackages=" + genModel.getGenPackages().size() + + " usedGenPackages=" + genModel.getUsedGenPackages().size()); + + // Verbose diagnostic — only emitted under -X (debug). Dumps the genmodel's + // GenPackage tree and probes every EClassifier referenced from main to + // verify it's resolvable. Skips EMF's protected findGenClassifier + // (reflective invocation has been seen to perturb XMI parser state for + // very large models like CWM). + if (getLog().isDebugEnabled()) { + debugDumpGenModel(genModel); + debugProbeFindGenClassifier(genModel, ePackage); + } + // Collect referenced package names (to delete their generated files after // generation) List referencedPackageNames = new ArrayList<>(); @@ -568,6 +597,24 @@ private void configureEMF(ResourceSet resourceSet) { // Clear the genPackageRegistry for a fresh run genPackageRegistry.clear(); + // Drop nsURIs this plugin previously registered into the JVM-global + // EPackage.Registry.INSTANCE in earlier mojo invocations within the same + // Maven session. Without this, ResourceSetImpl#getPackageRegistry()'s + // delegation would report containsKey=true for those nsURIs, the local + // map would never be populated, and EMF cross-ref resolution would walk + // stale EClassifier instances detached from this module's resourceSet — + // surfacing as findGenClassifier() == null deep inside codegen. + synchronized (pluginRegisteredNsURIs) { + if (!pluginRegisteredNsURIs.isEmpty()) { + getLog().info("Removing " + pluginRegisteredNsURIs.size() + + " stale nsURIs from EPackage.Registry.INSTANCE (from earlier mojo invocations in this JVM)"); + for (String nsURI : pluginRegisteredNsURIs) { + EPackage.Registry.INSTANCE.remove(nsURI); + } + pluginRegisteredNsURIs.clear(); + } + } + resourceSet.getResourceFactoryRegistry().getContentTypeToFactoryMap().put(GenModelPackage.eCONTENT_TYPE, new XMIResourceFactoryImpl()); resourceSet.getResourceFactoryRegistry().getContentTypeToFactoryMap().put("application/xmi", @@ -745,7 +792,8 @@ private GenModel createGenModel(EPackage ePackage, String projectName, ResourceS // Find referenced external packages (transitively) Set referencedPackages = findReferencedExternalPackages(ePackage); for (EPackage ref : referencedPackages) { - getLog().info("Referenced external package: " + ref.getName() + " (" + ref.getNsURI() + ")"); + getLog().info("Referenced external package: " + ref.getName() + " (" + ref.getNsURI() + ") with " + + ref.getEClassifiers().size() + " EClassifiers"); } // Initialize GenModel with ALL packages (main + referenced) — EMF still @@ -756,6 +804,22 @@ private GenModel createGenModel(EPackage ePackage, String projectName, ResourceS allPackages.addAll(referencedPackages); genModel.initialize(allPackages); + getLog().info("After genModel.initialize, GenPackages count: " + genModel.getGenPackages().size()); + if (getLog().isDebugEnabled()) { + for (GenPackage gp : genModel.getGenPackages()) { + EPackage ep = gp.getEcorePackage(); + getLog().debug(" top-level GenPackage: " + gp.getPackageName() + " (" + + (ep != null ? ep.getNsURI() : "null") + ") classifiers=" + + gp.getGenClassifiers().size() + " sub=" + gp.getSubGenPackages().size()); + for (GenPackage sub : gp.getSubGenPackages()) { + EPackage sep = sub.getEcorePackage(); + getLog().debug(" sub GenPackage: " + sub.getPackageName() + " (" + + (sep != null ? sep.getNsURI() : "null") + ") classifiers=" + + sub.getGenClassifiers().size()); + } + } + } + // Configure referenced GenPackages (indices 1+) and MOVE them from // getGenPackages() to getUsedGenPackages() — this prevents EMF from // (re-)generating Java code for the external types (those classes already @@ -1142,12 +1206,24 @@ private void configureGenPackageFromAnnotations(GenPackage genPackage, EPackage */ private void registerEPackage(ResourceSet resourceSet, EPackage ePackage, String source) { String nsURI = ePackage.getNsURI(); - if (nsURI != null && !EPackage.Registry.INSTANCE.containsKey(nsURI)) { - EPackage.Registry.INSTANCE.put(nsURI, ePackage); - resourceSet.getPackageRegistry().put(nsURI, ePackage); - getLog().info("Registered EPackage" + source + ": " + ePackage.getName() + " (" + nsURI + ")"); + if (nsURI != null) { + // Putting into EPackage.Registry.INSTANCE happens only the first time + // a session sees this nsURI. The resourceSet.getPackageRegistry() also + // gets the freshly loaded ePackage on first encounter. ResourceSet + // PackageRegistry delegates to INSTANCE for misses, so subsequent + // sessions would otherwise short-circuit on the stale INSTANCE entry. + if (!EPackage.Registry.INSTANCE.containsKey(nsURI)) { + EPackage.Registry.INSTANCE.put(nsURI, ePackage); + pluginRegisteredNsURIs.add(nsURI); + resourceSet.getPackageRegistry().put(nsURI, ePackage); + getLog().info("Registered EPackage" + source + ": " + ePackage.getName() + " (" + nsURI + ")"); + } - // Also create and register a GenPackage if GenModel annotations are present + // Populate the per-mojo GenPackage registry independently of the JVM-global + // EPackage.Registry. The latter persists across modules in the same Maven + // session, so on the second consumer module the outer guard would + // short-circuit and leave genPackageRegistry empty — breaking cross-package + // codegen lookups (manifests as NPE in getPackageFamilyTreeDependencies). if (!genPackageRegistry.containsKey(nsURI)) { GenPackage genPackage = createGenPackageFromEcore(resourceSet, ePackage); if (genPackage != null) { @@ -1607,6 +1683,126 @@ private void printDiagnostic(Diagnostic diagnostic, String prefix) { /** * Recursively deletes a directory and all its contents. */ + private void debugDumpGenModel(GenModel genModel) { + getLog().debug("==== GenModel state before generation ===="); + getLog().debug("getGenPackages():"); + for (GenPackage gp : genModel.getGenPackages()) { + dumpGenPackage(gp, " "); + } + getLog().debug("getUsedGenPackages():"); + for (GenPackage gp : genModel.getUsedGenPackages()) { + dumpGenPackage(gp, " "); + } + getLog().debug("=========================================="); + } + + private void dumpGenPackage(GenPackage gp, String indent) { + EPackage ep = gp.getEcorePackage(); + String nsURI = ep != null ? ep.getNsURI() : "null"; + int classifiers = gp.getGenClassifiers().size(); + Resource res = ep != null ? ep.eResource() : null; + getLog().debug(indent + gp.getPackageName() + " (" + nsURI + ") classifiers=" + classifiers + + " eResource=" + (res != null ? res.getURI() : "null")); + for (GenPackage sub : gp.getSubGenPackages()) { + dumpGenPackage(sub, indent + " "); + } + } + + private void debugProbeFindGenClassifier(GenModel genModel, EPackage mainEPackage) { + getLog().debug("==== Probing findGenClassifier for external EClassifier refs ===="); + Set probed = new HashSet<>(); + probeEPackage(genModel, mainEPackage, probed); + getLog().debug("=========================================="); + } + + private void probeEPackage(GenModel genModel, EPackage ePackage, Set probed) { + String ownNsURI = ePackage.getNsURI(); + for (EClassifier classifier : ePackage.getEClassifiers()) { + if (classifier instanceof EClass eClass) { + for (EClass sup : eClass.getESuperTypes()) { + probeOne(genModel, sup, ownNsURI, "supertype of " + eClass.getName(), probed); + } + for (EStructuralFeature f : eClass.getEStructuralFeatures()) { + EClassifier ft = f.getEType(); + if (ft != null) { + probeOne(genModel, ft, ownNsURI, eClass.getName() + "." + f.getName(), + probed); + } + } + } + } + for (EPackage sub : ePackage.getESubpackages()) { + probeEPackage(genModel, sub, probed); + } + } + + private void probeOne(GenModel genModel, EClassifier ec, String ownRootNsURI, String origin, + Set probed) { + if (!probed.add(ec)) { + return; + } + EPackage ep = ec.getEPackage(); + String nsURI = ep != null ? ep.getNsURI() : "null"; + // Manual walk: find a GenClassifier whose getEcoreClassifier() == ec, by name + // fallback. This mimics what the EMF codegen does without invoking the + // protected findGenClassifier method directly (reflection on it can perturb + // the state of large generated packages like CWM during their initial parse). + GenPackage manualMatch = null; + for (GenPackage gp : genModel.getGenPackages()) { + manualMatch = matchClassifier(gp, ec); + if (manualMatch != null) { + break; + } + } + if (manualMatch == null) { + for (GenPackage gp : genModel.getUsedGenPackages()) { + manualMatch = matchClassifier(gp, ec); + if (manualMatch != null) { + break; + } + } + } + getLog().debug(" ref " + origin + " -> " + (ec.getName() == null ? "null" : ec.getName()) + + " in " + nsURI + + " instanceTypeName=" + ec.getInstanceTypeName() + + " manualMatch=" + (manualMatch != null ? manualMatch.getPackageName() : "NONE")); + } + + private GenPackage matchClassifier(GenPackage gp, EClassifier ec) { + EPackage gpEP = gp.getEcorePackage(); + if (gpEP != null && ec.getEPackage() != null + && gpEP.getNsURI().equals(ec.getEPackage().getNsURI())) { + for (GenClassifier gc : gp.getGenClassifiers()) { + if (gc.getEcoreClassifier() == ec) { + return gp; + } + } + // EClassifier may be a different instance with same name + for (GenClassifier gc : gp.getGenClassifiers()) { + if (gc.getEcoreClassifier() != null + && ec.getName() != null + && ec.getName().equals(gc.getEcoreClassifier().getName())) { + EClassifier other = gc.getEcoreClassifier(); + Resource ecRes = ec.eResource(); + Resource otherRes = other.eResource(); + getLog().warn(" IDENTITY MISMATCH for " + ec.getName() + + ": ec@" + System.identityHashCode(ec) + " in " + + (ecRes != null ? ecRes.getURI() : "null") + + " vs GenClassifier.ec@" + System.identityHashCode(other) + + " in " + (otherRes != null ? otherRes.getURI() : "null")); + return gp; + } + } + } + for (GenPackage sub : gp.getSubGenPackages()) { + GenPackage m = matchClassifier(sub, ec); + if (m != null) { + return m; + } + } + return null; + } + private void deleteDirectory(File dir) { if (dir.isDirectory()) { File[] files = dir.listFiles();