TestJarGenerator.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.codehaus.plexus.classworlds;
import javax.tools.DiagnosticCollector;
import javax.tools.FileObject;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
/**
* Generates test fixture JARs at test time using the JDK's {@code javac} and {@code jar} tools,
* eliminating the need to commit pre-compiled binary JARs to version control.
*
* <p>All JARs are written to {@code target/test-jars/} under the module base directory.
* The generator is idempotent: existing JARs are overwritten on each run. Note that
* JARs from a previous run whose names are no longer generated are not deleted.
*
* <p>Called once per test run from {@link AbstractClassWorldsTestCase#generateTestJars()}.
*/
public class TestJarGenerator {
private final Path outputDir;
private final JavaCompiler compiler;
/** Map of compiled class name ��� bytecode, accumulated across compile() calls. */
private final Map<String, byte[]> compiledClasses = new LinkedHashMap<>();
public TestJarGenerator(Path outputDir) throws IOException {
this.outputDir = outputDir;
Files.createDirectories(outputDir);
this.compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new IllegalStateException("No system Java compiler available ��� tests must run on a JDK, not a JRE.");
}
}
// -------------------------------------------------------------------------
// Public entry point
// -------------------------------------------------------------------------
/** Generate all test fixture JARs into {@link #outputDir}. */
public void generate() throws Exception {
generateSimpleJars();
generateComponentJars();
generateMiscJars();
}
/** Simple single-class and deadlock fixture JARs. */
private void generateSimpleJars() throws Exception {
// Simple stubs ��� no external dependencies
compileClasses(
null,
src("a/A.java", "package a; public class A { public static void main(String[] a) {} }"),
src("a/Aa.java", "package a; public class Aa { public static int main(String[] a) { return 15; } }"));
jar(
"a.jar",
res("a.properties", "a properties\n"),
res("common.properties", "common.properties"),
cls("a/A.class"),
cls("a/Aa.class"));
compiledClasses.clear();
compileClasses(
null,
src(
"b/B.java",
"package b;\npublic class B {\n"
+ " public static void main(String[] a, org.codehaus.plexus.classworlds.ClassWorld w) {}\n"
+ "}"),
src(
"b/Bb.java",
"package b;\npublic class Bb {\n"
+ " public static int main(String[] a, org.codehaus.plexus.classworlds.ClassWorld w) { return 45; }\n"
+ "}"));
jar(
"b.jar",
res("b.properties", "name = b.properties"),
res("common.properties", "name = common.properties"),
cls("b/B.class"),
cls("b/Bb.class"));
compiledClasses.clear();
// b_old uses the legacy org.codehaus.classworlds.ClassWorld adapter
compileClasses(
null,
src(
"b/B.java",
"package b;\npublic class B {\n"
+ " public static void main(String[] a, org.codehaus.classworlds.ClassWorld w) {}\n"
+ "}"),
src(
"b/Bb.java",
"package b;\npublic class Bb {\n"
+ " public static int main(String[] a, org.codehaus.classworlds.ClassWorld w) { return 0; }\n"
+ "}"));
jar("b_old.jar", cls("b/B.class"), cls("b/Bb.class"));
compiledClasses.clear();
compileClasses(null, src("c/C.java", "package c; public class C {}"));
jar("c.jar", cls("c/C.class"));
compiledClasses.clear();
compileClasses(null, src("d/D.java", "package d; public class D {}"));
jar("d.jar", cls("d/D.class"));
compiledClasses.clear();
// deadlock: cross-extends to trigger classloader deadlock test
compileClasses(
null,
src("deadlock/B.java", "package deadlock; public class B {}"),
src("deadlock/D.java", "package deadlock; public class D {}"),
src("deadlock/A.java", "package deadlock; public class A extends B {}"),
src("deadlock/C.java", "package deadlock; public class C extends D {}"));
jar(
"deadlock.jar",
cls("deadlock/A.class"),
cls("deadlock/B.class"),
cls("deadlock/C.class"),
cls("deadlock/D.class"));
compiledClasses.clear();
}
/** Component0���5 plexus fixture JARs. */
private void generateComponentJars() throws Exception {
// circular: cross-reference between A and B, A has inner class C
compileClasses(
null,
src(
"A.java",
"public class A {\n"
+ " public static B b = new B();\n"
+ " public static C c = new C();\n"
+ " public B foo(B arg) { return new B(); }\n"
+ " public C foo(C arg) { return new C(); }\n"
+ " public static A create(boolean arg) { return arg ? new A() : new C(); }\n"
+ " public static class C extends A {}\n"
+ "}"),
src(
"B.java",
"public class B {\n"
+ " public A a;\n"
+ " public A foo(A arg) { return new A(); }\n"
+ "}"));
jar(
"circular-0.1.jar",
res("pom.xml", circularPom()),
res("src/main/java/org/A.java", circularSourceA()),
res("src/main/java/org/B.java", circularSourceB()),
res("META-INF/maven/test/circular/pom.xml", circularPom()),
res(
"META-INF/maven/test/circular/pom.properties",
"#Generated by Maven\nversion=0.1\ngroupId=test\nartifactId=circular\n"),
cls("A.class"),
cls("A$C.class"),
cls("B.class"));
compiledClasses.clear();
// component0..4: interface + impl pairs in org.codehaus.plexus package
for (int i = 0; i <= 4; i++) {
String iface = "Component" + i;
String impl = "Default" + iface;
compileClasses(
null,
src(
"org/codehaus/plexus/" + iface + ".java",
"package org.codehaus.plexus; public interface " + iface + " {}"),
src(
"org/codehaus/plexus/" + impl + ".java",
"package org.codehaus.plexus; public class " + impl + " implements " + iface + " {}"));
jar(
"component" + i + "-1.0.jar",
res(
"META-INF/plexus/components.xml",
plexusComponents("org.codehaus.plexus." + iface, "org.codehaus.plexus." + impl)),
cls("org/codehaus/plexus/" + iface + ".class"),
cls("org/codehaus/plexus/" + impl + ".class"));
compiledClasses.clear();
}
// component5: two versions with different method signatures (realm isolation test)
compileClasses(
null,
src("test/Component5.java", "package test; public interface Component5 { String printOld(); }"),
src(
"test/DefaultComponent5.java",
"package test; public class DefaultComponent5 implements Component5 {"
+ " public String printOld() { return \"old\"; } }"));
jar(
"component5-1.0.jar",
res("META-INF/plexus/components.xml", plexusComponents("test.Component5", "test.DefaultComponent5")),
cls("test/Component5.class"),
cls("test/DefaultComponent5.class"));
compiledClasses.clear();
compileClasses(
null,
src("test/Component5.java", "package test; public interface Component5 { String printNew(); }"),
src(
"test/DefaultComponent5.java",
"package test; public class DefaultComponent5 implements Component5 {"
+ " public String printNew() { return \"new\"; } }"));
jar(
"component5-2.0.jar",
res("META-INF/plexus/components.xml", plexusComponents("test.Component5", "test.DefaultComponent5")),
cls("test/Component5.class"),
cls("test/DefaultComponent5.class"));
compiledClasses.clear();
}
/** from-from, nested, and werkflow JARs. */
private void generateMiscJars() throws Exception {
// from-from: simple main class + META-INF/maven metadata
compileClasses(
null,
src(
"com/from/from/from/Main.java",
"package com.from.from.from; public class Main { public static void main(String[] a) {} }"));
jar(
"from-from-0.0.1-from-load-import.jar",
res("META-INF/maven/com.from/from-from/pom.xml", fromFromPom()),
res(
"META-INF/maven/com.from/from-from/pom.properties",
"artifactId=from-from\ngroupId=com.from\nversion=0.0.1-from-load-import"),
cls("com/from/from/from/Main.class"));
compiledClasses.clear();
// a.jar must exist before nested.jar (which embeds it)
buildNestedJar();
// werkflow: resource-only jar (no classes)
buildWerkflowJar();
}
// -------------------------------------------------------------------------
// Compilation helpers
// -------------------------------------------------------------------------
/**
* Compile one or more in-memory Java source units.
* Results are accumulated into {@link #compiledClasses}.
*
* @param extraClasspath additional JAR paths to add to the compile classpath (may be null)
*/
private void compileClasses(List<Path> extraClasspath, SourceUnit... sources) throws Exception {
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
try (StandardJavaFileManager fm = compiler.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8)) {
// Build classpath: current test classpath + any extras
List<File> classpathFiles = currentClasspath();
if (extraClasspath != null) {
for (Path p : extraClasspath) {
classpathFiles.add(p.toFile());
}
}
fm.setLocation(StandardLocation.CLASS_PATH, classpathFiles);
// In-memory output file manager
InMemoryFileManager memFm = new InMemoryFileManager(fm, compiledClasses);
List<JavaFileObject> units = new ArrayList<>();
for (SourceUnit su : sources) {
units.add(su);
}
JavaCompiler.CompilationTask task = compiler.getTask(null, memFm, diagnostics, null, null, units);
boolean ok = task.call();
if (!ok) {
StringBuilder sb = new StringBuilder("Compilation failed:\n");
for (var d : diagnostics.getDiagnostics()) {
sb.append(d).append('\n');
}
throw new RuntimeException(sb.toString());
}
}
}
/** Collect the current JVM classpath entries as File objects. */
private static List<File> currentClasspath() {
List<File> result = new ArrayList<>();
String cp = System.getProperty("java.class.path", "");
for (String entry : cp.split(File.pathSeparator)) {
if (!entry.isEmpty()) {
result.add(new File(entry));
}
}
return result;
}
// -------------------------------------------------------------------------
// JAR assembly helpers
// -------------------------------------------------------------------------
/**
* Assemble a JAR from a mix of {@link ClassEntry} (bytecode from compiledClasses)
* and {@link ResourceEntry} (inline text content).
*/
private void jar(String name, JarContent... entries) throws IOException {
Path out = outputDir.resolve(name);
Manifest mf = new Manifest();
mf.getMainAttributes().putValue("Manifest-Version", "1.0");
try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(out), mf)) {
for (JarContent e : entries) {
jos.putNextEntry(new JarEntry(e.entryName()));
jos.write(e.bytes(compiledClasses));
jos.closeEntry();
}
}
}
/** Build nested.jar: a.properties + lib/a.jar (a.jar embedded). */
private void buildNestedJar() throws IOException {
Path aJar = outputDir.resolve("a.jar");
byte[] aJarBytes = Files.readAllBytes(aJar);
Path out = outputDir.resolve("nested.jar");
Manifest mf = new Manifest();
mf.getMainAttributes().putValue("Manifest-Version", "1.0");
try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(out), mf)) {
jos.putNextEntry(new JarEntry("nested.properties"));
jos.write("nested.properties\n".getBytes(StandardCharsets.UTF_8));
jos.closeEntry();
jos.putNextEntry(new JarEntry("lib/a.jar"));
jos.write(aJarBytes);
jos.closeEntry();
}
}
/** Build werkflow.jar: resource-only (XML files, no classes). */
private void buildWerkflowJar() throws IOException {
Path out = outputDir.resolve("resources/werkflow.jar");
Files.createDirectories(out.getParent());
Manifest mf = new Manifest();
mf.getMainAttributes().putValue("Manifest-Version", "1.0");
try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(out), mf)) {
addXmlEntry(jos, "org/codehaus/werkflow/personality/basic/basic-syntax.xml", "<syntax/>");
addXmlEntry(jos, "org/codehaus/werkflow/personality/bpel4ws/bpel4ws-syntax.xml", "<syntax/>");
addXmlEntry(jos, "org/codehaus/werkflow/personality/common/core-idioms.xml", "<idioms/>");
}
}
private static void addXmlEntry(JarOutputStream jos, String name, String content) throws IOException {
jos.putNextEntry(new JarEntry(name));
jos.write(content.getBytes(StandardCharsets.UTF_8));
jos.closeEntry();
}
// -------------------------------------------------------------------------
// Static resource strings
// -------------------------------------------------------------------------
private static String circularPom() {
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n"
+ " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
+ " xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0"
+ " http://maven.apache.org/maven-v4_0_0.xsd\">\n"
+ " <modelVersion>4.0.0</modelVersion>\n"
+ " <groupId>test</groupId>\n"
+ " <artifactId>circular</artifactId>\n"
+ " <version>0.1</version>\n"
+ "</project>\n";
}
private static String circularSourceA() {
return "public class A {\n"
+ " public static B b = new B();\n"
+ " public static C c = new C();\n"
+ " public B foo( B arg ) { return new B(); }\n"
+ " public C foo( C arg ) { return new C(); }\n"
+ " public static A create( boolean arg) { return arg ? new A() : new C(); }\n"
+ " public static class C extends A {}\n"
+ "}\n";
}
private static String circularSourceB() {
return "public class B {\n" + " public A a;\n" + " public A foo( A arg ) { return new A(); }\n" + "}\n";
}
private static String plexusComponents(String role, String impl) {
return "<component-set>\n"
+ " <components>\n"
+ " <component>\n"
+ " <role>" + role + "</role>\n"
+ " <implementation>" + impl + "</implementation>\n"
+ " </component>\n"
+ " </components>\n"
+ "</component-set>\n";
}
private static String fromFromPom() {
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n"
+ " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
+ " xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0"
+ " http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n"
+ " <modelVersion>4.0.0</modelVersion>\n"
+ " <groupId>com.from</groupId>\n"
+ " <artifactId>from-from</artifactId>\n"
+ " <version>0.0.1-from-load-import</version>\n"
+ "</project>\n";
}
// -------------------------------------------------------------------------
// DSL helpers for jar() entries
// -------------------------------------------------------------------------
interface JarContent {
String entryName();
byte[] bytes(Map<String, byte[]> compiled) throws IOException;
}
/** Bytecode entry ��� key is the slash-separated class name + ".class". */
record ClassEntry(String name) implements JarContent {
@Override
public String entryName() {
return name;
}
@Override
public byte[] bytes(Map<String, byte[]> compiled) {
// Strip ".class" to get the map key used by InMemoryFileManager
String key = name.endsWith(".class") ? name.substring(0, name.length() - 6) : name;
byte[] b = compiled.get(key);
if (b == null) {
throw new RuntimeException(
"No compiled bytecode for: " + key + " (available: " + compiled.keySet() + ")");
}
return b;
}
}
/** Inline text resource entry. */
record ResourceEntry(String name, String content) implements JarContent {
@Override
public String entryName() {
return name;
}
@Override
public byte[] bytes(Map<String, byte[]> compiled) {
return content.getBytes(StandardCharsets.UTF_8);
}
}
private static ClassEntry cls(String name) {
return new ClassEntry(name);
}
private static ResourceEntry res(String name, String content) {
return new ResourceEntry(name, content);
}
// -------------------------------------------------------------------------
// In-memory source unit
// -------------------------------------------------------------------------
static class SourceUnit extends SimpleJavaFileObject {
private final String src;
SourceUnit(String className, String src) {
super(URI.create("mem:///" + className.replace('.', '/') + ".java"), Kind.SOURCE);
this.src = src;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return src;
}
}
private static SourceUnit src(String path, String src) {
// path like "a/A.java" ��� class name "a.A"
String className = path.replace('/', '.').replace(".java", "");
return new SourceUnit(className, src);
}
// -------------------------------------------------------------------------
// In-memory output file manager
// -------------------------------------------------------------------------
/**
* Routes compiled {@code .class} output into a {@code Map<String, byte[]>}
* instead of writing to disk.
*/
static class InMemoryFileManager extends javax.tools.ForwardingJavaFileManager<StandardJavaFileManager> {
private final Map<String, byte[]> classBytes;
InMemoryFileManager(StandardJavaFileManager delegate, Map<String, byte[]> classBytes) {
super(delegate);
this.classBytes = classBytes;
}
@Override
public JavaFileObject getJavaFileForOutput(
Location location, String className, JavaFileObject.Kind kind, FileObject sibling) {
return new SimpleJavaFileObject(URI.create("mem:///" + className.replace('.', '/') + ".class"), kind) {
@Override
public OutputStream openOutputStream() {
return new ByteArrayOutputStream() {
@Override
public void close() throws IOException {
super.close();
// key uses '/' separator (matches JarEntry names without ".class")
classBytes.put(className.replace('.', '/'), toByteArray());
}
};
}
};
}
}
}