diff --git a/annot/src/test/java/com/predic8/membrane/annot/ParsingTest.java b/annot/src/test/java/com/predic8/membrane/annot/ParsingTest.java
new file mode 100644
index 0000000000..a0997571f4
--- /dev/null
+++ b/annot/src/test/java/com/predic8/membrane/annot/ParsingTest.java
@@ -0,0 +1,79 @@
+package com.predic8.membrane.annot;
+
+import com.predic8.membrane.annot.util.CompilerHelper;
+import org.junit.jupiter.api.Test;
+
+import static com.predic8.membrane.annot.SpringConfigurationXSDGeneratingAnnotationProcessorTest.MC_MAIN_DEMO;
+import static com.predic8.membrane.annot.util.CompilerHelper.*;
+
+public class ParsingTest {
+
+ private String wrapSpring(String content) {
+ return """
+
+ """ + content + """
+
+ """;
+ }
+
+ @Test
+ public void simple() {
+ var sources = splitSources(MC_MAIN_DEMO + """
+ package com.predic8.membrane.demo;
+ import com.predic8.membrane.annot.*;
+ import java.util.List;
+ @MCElement(name="demo")
+ public class DemoElement {
+ }
+ """);
+ var result = CompilerHelper.compile(sources, false);
+ assertCompilerResult(true, result);
+
+ parse(result, wrapSpring("""
+
+ """));
+ }
+
+ @Test
+ public void childElements() {
+ var sources = splitSources(MC_MAIN_DEMO + """
+ package com.predic8.membrane.demo;
+ import com.predic8.membrane.annot.*;
+ import java.util.List;
+ @MCElement(name="root")
+ public class DemoElement {
+ @MCChildElement
+ public void setChild(AbstractDemoChildElement s) {}
+ }
+ ---
+ package com.predic8.membrane.demo;
+ public abstract class AbstractDemoChildElement {
+ }
+ ---
+ package com.predic8.membrane.demo;
+ import com.predic8.membrane.annot.*;
+ @MCElement(name="child1")
+ public class Child1 extends AbstractDemoChildElement {
+ }
+ ---
+ package com.predic8.membrane.demo;
+ import com.predic8.membrane.annot.*;
+ @MCElement(name="child2")
+ public class Child2 extends AbstractDemoChildElement {
+ }
+ """);
+ var result = CompilerHelper.compile(sources, false);
+ assertCompilerResult(true, result);
+
+ parse(result, wrapSpring("""
+
+
+
+ """));
+ }
+}
diff --git a/annot/src/test/java/com/predic8/membrane/annot/SpringConfigXSDErrorsTest.java b/annot/src/test/java/com/predic8/membrane/annot/SpringConfigXSDErrorsTest.java
index e907077147..6ab796ebd8 100644
--- a/annot/src/test/java/com/predic8/membrane/annot/SpringConfigXSDErrorsTest.java
+++ b/annot/src/test/java/com/predic8/membrane/annot/SpringConfigXSDErrorsTest.java
@@ -27,7 +27,7 @@
public class SpringConfigXSDErrorsTest {
@Test
public void mcMainMissing() {
- List sources = splitSources("""
+ var sources = splitSources("""
package com.predic8.membrane.demo;
public class Demo {
}
@@ -47,7 +47,7 @@ public class DemoElement {
@Test
public void mcElementNameMissing() {
- List sources = splitSources("""
+ var sources = splitSources("""
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.MCElement;
@MCElement
@@ -63,7 +63,7 @@ public class DemoElement {
@Test
public void mcElementMissing() {
- List sources = splitSources(MC_MAIN_DEMO);
+ var sources = splitSources(MC_MAIN_DEMO);
var result = CompilerHelper.compile(sources, false);
assertCompilerResult(false, of(
@@ -74,7 +74,7 @@ public void mcElementMissing() {
@Test
public void duplicateMcElementId() {
- List sources = splitSources(MC_MAIN_DEMO + """
+ var sources = splitSources(MC_MAIN_DEMO + """
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.MCElement;
@MCElement(name="demo")
@@ -97,7 +97,7 @@ public class DemoElement2 {
@Test
public void duplicateMcElementName() {
- List sources = splitSources(MC_MAIN_DEMO + """
+ var sources = splitSources(MC_MAIN_DEMO + """
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.MCElement;
@MCElement(name="demo", id="demo1")
@@ -123,7 +123,7 @@ class NoEnvelope {
@Test
public void topLevel() {
- List sources = splitSources(MC_MAIN_DEMO + """
+ var sources = splitSources(MC_MAIN_DEMO + """
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.MCElement;
@MCElement(name="demo", noEnvelope=true)
@@ -139,7 +139,7 @@ public class DemoElement {
@Test
public void mixed() {
- List sources = splitSources(MC_MAIN_DEMO + """
+ var sources = splitSources(MC_MAIN_DEMO + """
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.MCElement;
@MCElement(name="demo", noEnvelope=true, topLevel=false, mixed=true)
@@ -155,7 +155,7 @@ public class DemoElement {
@Test
public void noChildElements() {
- List sources = splitSources(MC_MAIN_DEMO + """
+ var sources = splitSources(MC_MAIN_DEMO + """
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.MCElement;
@MCElement(name="demo", noEnvelope=true, topLevel=false)
@@ -171,7 +171,7 @@ public class DemoElement {
@Test
public void twoChildElements() {
- List sources = splitSources(MC_MAIN_DEMO + """
+ var sources = splitSources(MC_MAIN_DEMO + """
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.MCElement;
import com.predic8.membrane.annot.MCChildElement;
@@ -193,7 +193,7 @@ public void setChild2(List s) {}
@Test
public void childIsNotAList() {
- List sources = splitSources(MC_MAIN_DEMO + """
+ var sources = splitSources(MC_MAIN_DEMO + """
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.MCElement;
import com.predic8.membrane.annot.MCChildElement;
@@ -212,7 +212,7 @@ public void setChild1(DemoElement s) {}
@Test
public void hasAttributes() {
- List sources = splitSources(MC_MAIN_DEMO + """
+ var sources = splitSources(MC_MAIN_DEMO + """
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.*;
import java.util.List;
@@ -233,7 +233,7 @@ public void setAttribute1(String s) {}
@Test
public void otherAttributes() {
- List sources = splitSources(MC_MAIN_DEMO + """
+ var sources = splitSources(MC_MAIN_DEMO + """
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.*;
import java.util.List;
@@ -255,7 +255,7 @@ public void setAttributes(Map attributes) {}
@Test
public void textContent() {
- List sources = splitSources(MC_MAIN_DEMO + """
+ var sources = splitSources(MC_MAIN_DEMO + """
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.*;
import java.util.List;
@@ -280,7 +280,7 @@ public void setContent(String content) {}
class TextContent {
@Test
public void mcTextContentMissing() {
- List sources = splitSources(MC_MAIN_DEMO + """
+ var sources = splitSources(MC_MAIN_DEMO + """
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.*;
@MCElement(name="demo", mixed=true)
@@ -295,7 +295,7 @@ public class DemoElement {
}
@Test
public void mixedMissing() {
- List sources = splitSources(MC_MAIN_DEMO + """
+ var sources = splitSources(MC_MAIN_DEMO + """
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.*;
@MCElement(name="demo")
@@ -315,7 +315,7 @@ public void setContent(String content) {}
@Test
public void noConcreteChildMcElement() {
- List sources = splitSources(MC_MAIN_DEMO + """
+ var sources = splitSources(MC_MAIN_DEMO + """
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.*;
import java.util.List;
@@ -338,7 +338,7 @@ public abstract class AbstractDemoChildElement {
@Test
public void childNameNotUnique() {
- List sources = splitSources(MC_MAIN_DEMO + """
+ var sources = splitSources(MC_MAIN_DEMO + """
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.*;
import java.util.List;
diff --git a/annot/src/test/java/com/predic8/membrane/annot/SpringConfigurationXSDGeneratingAnnotationProcessorTest.java b/annot/src/test/java/com/predic8/membrane/annot/SpringConfigurationXSDGeneratingAnnotationProcessorTest.java
index 8a8e1cdef1..92406ea75d 100644
--- a/annot/src/test/java/com/predic8/membrane/annot/SpringConfigurationXSDGeneratingAnnotationProcessorTest.java
+++ b/annot/src/test/java/com/predic8/membrane/annot/SpringConfigurationXSDGeneratingAnnotationProcessorTest.java
@@ -23,6 +23,12 @@
public class SpringConfigurationXSDGeneratingAnnotationProcessorTest {
public static final String MC_MAIN_DEMO = """
+ resource META-INF/spring.handlers
+ http\\://membrane-soa.org/demo/1/=com.predic8.membrane.demo.config.spring.NamespaceHandler
+ ---
+ resource META-INF/spring.schemas
+ http\\://membrane-soa.org/schemas/demo-1.xsd=com/predic8/membrane/demo/config/spring/router-conf.xsd
+ ---
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.MCMain;
@MCMain(
@@ -45,7 +51,7 @@ public void init() {
@Test
public void simpleTest() {
- List sources = splitSources(MC_MAIN_DEMO + """
+ var sources = splitSources(MC_MAIN_DEMO + """
package com.predic8.membrane.demo;
import com.predic8.membrane.annot.MCElement;
@MCElement(name="demo")
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java b/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java
index c61ca3d115..2ddc981b7c 100644
--- a/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java
+++ b/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java
@@ -18,31 +18,45 @@
import org.hamcrest.collection.IsIterableContainingInAnyOrder;
import javax.tools.*;
+import java.io.IOException;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
+import static java.util.List.of;
+import static java.util.stream.StreamSupport.stream;
+import static javax.tools.StandardLocation.CLASS_OUTPUT;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CompilerHelper {
- public static CompilerResult compile(Iterable extends JavaFileObject> sources, boolean logCompilerOutput) {
+ /**
+ * Compile the given source files.
+ *
+ * @param sourceFiles the source files to compile
+ * @param logCompilerOutput if true, print the compiler output to stderr
+ */
+ public static CompilerResult compile(Iterable extends FileObject> sourceFiles, boolean logCompilerOutput) {
+ var javaSources = getJavaSources(sourceFiles);
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new IllegalStateException("No system Java compiler found. Run tests with a JDK, not a JRE.");
}
DiagnosticCollector diagnostics = new DiagnosticCollector<>();
- JavaFileManager fileManager = new CustomJavaFileManager(compiler.getStandardFileManager(diagnostics, null, null));
+ JavaFileManager fileManager = new LoggingInMemoryJavaFileManager(compiler.getStandardFileManager(diagnostics, null, null));
+
+ copyResourcesToOutput(getResources(sourceFiles), fileManager);
JavaCompiler.CompilationTask task = compiler.getTask(
null,
fileManager,
diagnostics,
- List.of("-processor", "com.predic8.membrane.annot.SpringConfigurationXSDGeneratingAnnotationProcessor"),
+ of("-processor", "com.predic8.membrane.annot.SpringConfigurationXSDGeneratingAnnotationProcessor"),
null,
- sources
+ javaSources
);
boolean success = task.call();
@@ -50,20 +64,85 @@ public static CompilerResult compile(Iterable extends JavaFileObject> sources,
if (logCompilerOutput)
diagnostics.getDiagnostics().forEach(System.err::println);
- return new CompilerResult(success, diagnostics);
+ return new CompilerResult(success, diagnostics, fileManager.getClassLoader(CLASS_OUTPUT));
+ }
+
+ /**
+ * Parse the given XML Spring config.
+ */
+ public static void parse(CompilerResult cr, String xmlSpringConfig) {
+ ClassLoader originalClassloader = Thread.currentThread().getContextClassLoader();
+ try {
+ InMemoryClassLoader loaderA = (InMemoryClassLoader) cr.classLoader();
+ loaderA.defineOverlay(new OverlayInMemoryFile("/demo.xml", xmlSpringConfig));
+ CompositeClassLoader cl = new CompositeClassLoader(loaderA, CompilerHelper.class.getClassLoader());
+ Thread.currentThread().setContextClassLoader(cl);
+ Class> c = cl.loadClass("org.springframework.context.support.ClassPathXmlApplicationContext");
+ c.getConstructor(String.class).newInstance("/demo.xml");
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ } finally {
+ Thread.currentThread().setContextClassLoader(originalClassloader);
+ }
+ }
+
+ private static List getJavaSources(Iterable extends FileObject> sources) {
+ return stream(sources.spliterator(), false)
+ .filter(i -> i instanceof JavaFileObject)
+ .map(i -> (JavaFileObject) i)
+ .toList();
+ }
+
+ private static List getResources(Iterable extends FileObject> sources) {
+ return stream(sources.spliterator(), false)
+ .filter(i -> i instanceof OverlayInMemoryFile)
+ .map(i -> (OverlayInMemoryFile) i)
+ .toList();
+ }
+
+ private static void copyResourcesToOutput(List extends OverlayInMemoryFile> sources, JavaFileManager fileManager) {
+ sources.forEach(i -> {
+ PrintWriter pw = null;
+ try {
+ pw = new PrintWriter(fileManager.getFileForOutput(CLASS_OUTPUT, "", i.getName(), null)
+ .openWriter());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ pw.write(i.getCharContent(true).toString());
+ pw.close();
+ });
}
- public static List splitSources(String sources) {
+ public static List splitSources(String sources) {
return Stream.of(sources.split("---"))
.filter(s -> !s.isBlank())
- .map(CompilerHelper::toInMemoryJavaFile)
+ .map(CompilerHelper::toFile)
.toList();
}
+ private static FileObject toFile( String content) {
+ if (!content.trim().startsWith("resource"))
+ return toInMemoryJavaFile(content);
+
+ String[] parts;
+ while(true) {
+ parts = content.split("\n", 2);
+ if (parts.length != 2)
+ throw new RuntimeException("Invalid resource file: " + content + ". The resource is expected to have the format 'resource \n'.");
+ if (!parts[0].isEmpty())
+ break;
+ content = parts[1];
+ };
+
+ String name = parts[0].substring(9).trim();
+ return new OverlayInMemoryFile(name, parts[1]);
+ }
+
private static JavaFileObject toInMemoryJavaFile(String source) {
String pkg = extractPackage(source);
String cls = extractName(source);
- return new InMemoryJavaFile(pkg + "." + cls, source);
+ return new OverlayInMemoryJavaFile(pkg + "." + cls, source);
}
private static String extractName(String source) {
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/CompilerResult.java b/annot/src/test/java/com/predic8/membrane/annot/util/CompilerResult.java
index 2c6240e1ff..35e3a5fb5a 100644
--- a/annot/src/test/java/com/predic8/membrane/annot/util/CompilerResult.java
+++ b/annot/src/test/java/com/predic8/membrane/annot/util/CompilerResult.java
@@ -18,5 +18,6 @@
public record CompilerResult(
boolean compilationSuccess,
- DiagnosticCollector diagnostics) {
+ DiagnosticCollector diagnostics,
+ ClassLoader classLoader) {
}
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/CompositeClassLoader.java b/annot/src/test/java/com/predic8/membrane/annot/util/CompositeClassLoader.java
new file mode 100644
index 0000000000..60313d8599
--- /dev/null
+++ b/annot/src/test/java/com/predic8/membrane/annot/util/CompositeClassLoader.java
@@ -0,0 +1,59 @@
+package com.predic8.membrane.annot.util;
+
+import java.net.URL;
+import java.util.Enumeration;
+import java.io.IOException;
+
+/**
+ * The CompositeClassLoader delegates calls to loaderA first, and if that fails, to loaderB second.
+ */
+public class CompositeClassLoader extends ClassLoader {
+
+ private ClassLoader loaderA;
+ private ClassLoader loaderB;
+
+ public CompositeClassLoader(ClassLoader loaderA, ClassLoader loaderB) {
+ super(null);
+ this.loaderA = loaderA;
+ this.loaderB = loaderB;
+ }
+
+ @Override
+ protected Class> findClass(String name) throws ClassNotFoundException {
+ try {
+ return loaderA.loadClass(name);
+ } catch (ClassNotFoundException e) {
+ return loaderB.loadClass(name);
+ }
+ }
+
+ @Override
+ protected URL findResource(String name) {
+ URL resource = loaderA.getResource(name);
+ if (resource == null) {
+ resource = loaderB.getResource(name);
+ }
+ return resource;
+ }
+
+ @Override
+ protected Enumeration findResources(String name) throws IOException {
+ return new Enumeration() {
+ private final Enumeration enumA = loaderA.getResources(name);
+ private final Enumeration enumB = loaderB.getResources(name);
+
+ @Override
+ public boolean hasMoreElements() {
+ return enumA.hasMoreElements() || enumB.hasMoreElements();
+ }
+
+ @Override
+ public URL nextElement() {
+ if (enumA.hasMoreElements()) {
+ return enumA.nextElement();
+ }
+ return enumB.nextElement();
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/ConcatenatingEnumeration.java b/annot/src/test/java/com/predic8/membrane/annot/util/ConcatenatingEnumeration.java
new file mode 100644
index 0000000000..acfc95d658
--- /dev/null
+++ b/annot/src/test/java/com/predic8/membrane/annot/util/ConcatenatingEnumeration.java
@@ -0,0 +1,32 @@
+package com.predic8.membrane.annot.util;
+
+import java.util.Enumeration;
+
+public class ConcatenatingEnumeration implements Enumeration {
+ private final T[] additionalElements;
+ private final Enumeration base;
+
+ public ConcatenatingEnumeration(T[] additionalElements, Enumeration base) {
+ this.additionalElements = additionalElements;
+ this.base = base;
+ }
+
+ int nextIndex = 0;
+
+ @Override
+ public boolean hasMoreElements() {
+ if (nextIndex < additionalElements.length) {
+ return true;
+ }
+ return base.hasMoreElements();
+ }
+
+ @Override
+ public T nextElement() {
+ if (nextIndex < additionalElements.length) {
+ return additionalElements[nextIndex++];
+ }
+ return base.nextElement();
+ }
+
+}
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/CustomJavaFileManager.java b/annot/src/test/java/com/predic8/membrane/annot/util/CustomJavaFileManager.java
deleted file mode 100644
index a7f600c046..0000000000
--- a/annot/src/test/java/com/predic8/membrane/annot/util/CustomJavaFileManager.java
+++ /dev/null
@@ -1,396 +0,0 @@
-/* Copyright 2025 predic8 GmbH, www.predic8.com
-
- Licensed 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 com.predic8.membrane.annot.util;
-
-import org.slf4j.Logger;
-
-import javax.lang.model.element.Modifier;
-import javax.lang.model.element.NestingKind;
-import javax.tools.*;
-import java.io.*;
-import java.net.URI;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Set;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-/**
- * This implementation of JavaFileManager can either proxy the standard JavaFileManager
- * (USE_IN_MEM=false) logging API calls but modifying the existing file system,
- * or direct everything into memory (USE_IN_MEM=true).
- *
- * Not all possible use case are implemented for USE_IN_MEM=true.
- */
-public class CustomJavaFileManager implements JavaFileManager {
- /**
- * If true, this class directs file system 'write' requests to an in-memory file system. (This has only been tested
- * for certain use cases.)
- *
- * If false, this class logs file system 'write' requests and directs them to the underlying file system.
- */
- private static final boolean USE_IN_MEM = true;
-
- private static final Logger log = org.slf4j.LoggerFactory.getLogger(CustomJavaFileManager.class);
-
- private final JavaFileManager fm;
-
- public CustomJavaFileManager(JavaFileManager fm) {
- this.fm = fm;
- }
-
- @Override
- public ClassLoader getClassLoader(Location location) {
- log.debug("getClassLoader(" + location + ")");
- return fm.getClassLoader(location);
- }
-
- @Override
- public Iterable list(Location location, String packageName, Set kinds, boolean recurse) throws IOException {
- StringBuilder l = new StringBuilder("list(" + location + ", " + packageName + ", " + kinds + ", " + recurse + ") -> ");
- Iterable res = fm.list(location, packageName, kinds, recurse);
- l.append("[");
- for (JavaFileObject re : res) {
- l.append(re).append(",");
- }
- log.debug("{}]", l);
- return res;
- }
-
- @Override
- public String inferBinaryName(Location location, JavaFileObject file) {
- log.debug("inferBinaryName(" + location + ", " + file + ")");
- return fm.inferBinaryName(location, file);
- }
-
- @Override
- public boolean isSameFile(FileObject a, FileObject b) {
- log.debug("isSameFile(" + a + ", " + b + ")");
- return fm.isSameFile(a, b);
- }
-
- @Override
- public boolean handleOption(String current, Iterator remaining) {
- log.debug("handleOption(" + current + ", " + remaining + ")");
- return fm.handleOption(current, remaining);
- }
-
- @Override
- public boolean hasLocation(Location location) {
- log.debug("hasLocation(" + location + ")");
- return fm.hasLocation(location);
- }
-
- @Override
- public JavaFileObject getJavaFileForInput(Location location, String className, JavaFileObject.Kind kind) throws IOException {
- String l = ("getJavaFileForInput(" + location + ", " + className + ", " + kind + ") -> ");
- JavaFileObject res = fm.getJavaFileForInput(location, className, kind);
- log.debug("{}{}", l, res);
- return res;
- }
-
- @Override
- public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
- String l = ("getJavaFileForOutput(" + location + ", " + className + ", " + kind + ", " + sibling + ") -> ");
- JavaFileObject res = USE_IN_MEM ?
- new InMemoryJavaFileObject(location, className.replace('.', '/') + kind.extension, kind) :
- new LoggingJavaFileObject(fm.getJavaFileForOutput(location, className, kind, sibling));
- log.debug("{}{}", l, res);
- return res;
- }
-
- @Override
- public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
- String l = ("getFileForInput(" + location + ", " + packageName + ", " + relativeName + ") -> ");
- FileObject res = fm.getFileForInput(location, packageName, relativeName);
- log.debug("{}{}", l, res);
- return res;
- }
-
- @Override
- public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) throws IOException {
- String l = ("getFileForOutput(" + location + ", " + packageName + ", " + relativeName + ", " + sibling + ") -> ");
- String path = packageName.isEmpty() ? "" : packageName.replace('.', '/') + "/";
- FileObject res = USE_IN_MEM ? new InMemoryFileObject(location, path + relativeName) :
- new LoggingFileObject(fm.getFileForOutput(location, packageName, relativeName, sibling));
- log.debug("{}{}", l, res);
- return res;
- }
-
- @Override
- public void flush() throws IOException {
- log.debug("flush()");
- fm.flush();
- }
-
- @Override
- public void close() throws IOException {
- log.debug("close()");
- fm.close();
- }
-
- @Override
- public int isSupportedOption(String option) {
- log.debug("isSupportedOption(" + option + ")");
- return fm.isSupportedOption(option);
- }
-
- public Iterable> listLocationsForModules(Location location) throws IOException {
- log.debug("listLocationsForModules(" + location + ")");
- return fm.listLocationsForModules(location);
- }
-
- @Override
- public String inferModuleName(Location location) throws IOException {
- log.debug("inferModuleName(" + location + ")");
- return fm.inferModuleName(location);
- }
-
- private Map content = new HashMap<>();
-
- public class InnerFileObject extends SimpleJavaFileObject {
- public InnerFileObject(URI uri, Kind kind) {
- super(uri, kind);
- }
-
- @Override
- public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
- byte[] bytes = content.get(toUri());
- if (bytes == null)
- return "";
- return new String(bytes, UTF_8);
- }
-
- @Override
- public OutputStream openOutputStream() throws IOException {
- return new ByteArrayOutputStream() {
- @Override
- public void flush() throws IOException {
- super.flush();
- content.put(toUri(), toByteArray());
- log.debug("wrote " + toUri() + " : " + new String(toByteArray(), UTF_8));
- }
-
- @Override
- public void close() throws IOException {
- super.close();
- content.put(toUri(), toByteArray());
- log.debug("wrote " + toUri() + " : " + new String(toByteArray(), UTF_8));
- }
- };
- }
- }
-
- public class InMemoryFileObject implements FileObject {
- protected final SimpleJavaFileObject inner;
- private final JavaFileManager.Location location;
-
- public InMemoryFileObject(JavaFileManager.Location location, String path) {
- inner = new InnerFileObject(URI.create("string:///" + path), JavaFileObject.Kind.OTHER);
- this.location = location;
- }
-
- public InMemoryFileObject(JavaFileManager.Location location, String path, JavaFileObject.Kind kind) {
- inner = new InnerFileObject(URI.create("string:///" + path), kind);
- this.location = location;
- }
-
- @Override
- public URI toUri() {
- log.debug("toUri() " + inner.toUri());
- return inner.toUri();
- }
-
- @Override
- public String getName() {
- log.debug("getName() " + inner.toUri());
- return inner.getName();
- }
-
- @Override
- public InputStream openInputStream() throws IOException {
- log.debug("openInputStream() " + inner.toUri());
- return inner.openInputStream();
- }
-
- @Override
- public OutputStream openOutputStream() throws IOException {
- log.debug("openOutputStream() " + inner.toUri());
- return inner.openOutputStream();
- }
-
- @Override
- public Reader openReader(boolean ignoreEncodingErrors) throws IOException {
- log.debug("openReader(" + ignoreEncodingErrors + ") " + inner.toUri());
- return inner.openReader(ignoreEncodingErrors);
- }
-
- @Override
- public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
- log.debug("getCharContent(" + ignoreEncodingErrors + ") " + inner.toUri());
- return inner.getCharContent(ignoreEncodingErrors);
- }
-
- @Override
- public Writer openWriter() throws IOException {
- log.debug("openWriter() " + inner.toUri());
- return inner.openWriter();
- }
-
- @Override
- public long getLastModified() {
- log.debug("getLastModified() " + inner.toUri());
- return inner.getLastModified();
- }
-
- @Override
- public boolean delete() {
- log.debug("delete() " + inner.toUri());
- return inner.delete();
- }
-
- @Override
- public String toString() {
- return "InMemoryFileObject[" + inner.toString() + "]";
- }
- }
-
- public class InMemoryJavaFileObject extends InMemoryFileObject implements JavaFileObject {
- public InMemoryJavaFileObject(JavaFileManager.Location location, String path, Kind kind) {
- super(location, path, kind);
- }
-
- @Override
- public Kind getKind() {
- log.debug("getKind() " + inner.toUri());
- return inner.getKind();
- }
-
- @Override
- public boolean isNameCompatible(String simpleName, Kind kind) {
- log.debug("isNameCompatible(" + simpleName + ", " + kind + ") " + inner.toUri());
- return inner.isNameCompatible(simpleName, kind);
- }
-
- @Override
- public NestingKind getNestingKind() {
- log.debug("getNestingKind() " + inner.toUri());
- return inner.getNestingKind();
- }
-
- @Override
- public Modifier getAccessLevel() {
- log.debug("getAccessLevel() " + inner.toUri());
- return inner.getAccessLevel();
- }
- }
-
- public class LoggingFileObject implements FileObject {
- private final FileObject inner;
-
- public LoggingFileObject(FileObject inner) {
- this.inner = inner;
- }
-
- @Override
- public URI toUri() {
- log.debug("toUri() " + inner.toUri());
- return inner.toUri();
- }
-
- @Override
- public String getName() {
- log.debug("getName() " + inner.toUri());
- return inner.getName();
- }
-
- @Override
- public InputStream openInputStream() throws IOException {
- log.debug("openInputStream() " + inner.toUri());
- return inner.openInputStream();
- }
-
- @Override
- public OutputStream openOutputStream() throws IOException {
- log.debug("openOutputStream() " + inner.toUri());
- return inner.openOutputStream();
- }
-
- @Override
- public Reader openReader(boolean ignoreEncodingErrors) throws IOException {
- log.debug("openReader(" + ignoreEncodingErrors + ") " + inner.toUri());
- return inner.openReader(ignoreEncodingErrors);
- }
-
- @Override
- public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
- log.debug("getCharContent(" + ignoreEncodingErrors + ") " + inner.toUri());
- return inner.getCharContent(ignoreEncodingErrors);
- }
-
- @Override
- public Writer openWriter() throws IOException {
- log.debug("openWriter() " + inner.toUri());
- return inner.openWriter();
- }
-
- @Override
- public long getLastModified() {
- log.debug("getLastModified() " + inner.toUri());
- return inner.getLastModified();
- }
-
- @Override
- public boolean delete() {
- log.debug("delete() " + inner.toUri());
- return inner.delete();
- }
- }
-
- public class LoggingJavaFileObject extends LoggingFileObject implements JavaFileObject {
- private final JavaFileObject inner;
-
- public LoggingJavaFileObject(JavaFileObject inner) {
- super(inner);
- this.inner = inner;
- }
-
- @Override
- public Kind getKind() {
- log.debug("getKind() " + inner.toUri());
- return inner.getKind();
- }
-
- @Override
- public boolean isNameCompatible(String simpleName, Kind kind) {
- log.debug("isNameCompatible(" + simpleName + ", " + kind + ") " + inner.toUri());
- return inner.isNameCompatible(simpleName, kind);
- }
-
- @Override
- public NestingKind getNestingKind() {
- log.debug("getNestingKind() " + inner.toUri());
- return inner.getNestingKind();
- }
-
- @Override
- public Modifier getAccessLevel() {
- log.debug("getAccessLevel() " + inner.toUri());
- return inner.getAccessLevel();
- }
-
- }
-
-}
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryClassLoader.java b/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryClassLoader.java
new file mode 100644
index 0000000000..3f71044e05
--- /dev/null
+++ b/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryClassLoader.java
@@ -0,0 +1,163 @@
+package com.predic8.membrane.annot.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.util.Enumeration;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.springframework.util.StreamUtils.copyToByteArray;
+
+/**
+ * Implements a class loader based on in-memory files. Also implements an 'overlay file system' which can on-the-fly
+ * define resources which should also appear in the file system.
+ */
+class InMemoryClassLoader extends ClassLoader {
+ private static final Logger log = LoggerFactory.getLogger(InMemoryClassLoader.class);
+
+ private final InMemoryData data;
+ private InMemoryData overlay = new InMemoryData();
+
+ public InMemoryClassLoader(InMemoryData data) {
+ super(InMemoryClassLoader.class.getClassLoader());
+ this.data = data;
+ }
+
+ @Override
+ public Class> loadClass(String name) throws ClassNotFoundException {
+ synchronized (getClassLoadingLock(name)) {
+ // 1. Check if the class is already loaded
+ Class> c = findLoadedClass(name);
+ if (c != null) {
+ return c;
+ }
+
+ // 2. CRITICAL: Always delegate core Java classes to the parent
+ // Trying to load these yourself will break the JVM.
+ // A real implementation also delegates shared API classes (e.g., "javax.servlet.").
+ if (name.startsWith("java.") || name.startsWith("javax.")) {
+ try {
+ c = super.loadClass(name, false); // Use parent-first logic
+ if (c != null) {
+ return c;
+ }
+ } catch (ClassNotFoundException e) {
+ // Fall through to local search (unlikely for java.*)
+ }
+ }
+
+ // 3. Child-First: Try to find the class in our local URLs *first*
+ try {
+ c = findClass(name);
+ return c; // Found it locally
+ } catch (ClassNotFoundException e) {
+ // Not found locally. Swallow this exception.
+ }
+
+ // 4. Parent-Last: If not found locally, *now* delegate to the parent.
+ // We call the superclass's loadClass, which implements parent-first.
+ return super.loadClass(name, false);
+ }
+ }
+
+
+ @Override
+ protected Class> findClass(String name) throws ClassNotFoundException {
+ log.debug("findClass({})", name);
+ URI uri = URI.create("string:///" + name.replace('.', '/') + ".class");
+ byte[] bytes = data.content.get(uri);
+ if (bytes == null) {
+ if (!delegateToRootClassLoader(name)) {
+ try (java.io.InputStream is = this.getClass().getResourceAsStream(uri.toString().replaceAll("string://", ""))) {
+ if (is != null) {
+ byte[] buffer = copyToByteArray(is);
+ return defineClass(name, buffer, 0, buffer.length);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return super.findClass(name);
+ }
+ return defineClass(name, bytes, 0, bytes.length);
+ }
+
+ private boolean delegateToRootClassLoader(String name) {
+ return name.startsWith("java.") || name.startsWith("javax.")
+ || name.startsWith("org.xml.sax") || name.startsWith("org.w3c.dom");
+ }
+
+ @Override
+ protected URL findResource(String name) {
+ log.debug("findResource({})", name);
+ return super.findResource(name);
+ }
+
+ @Override
+ protected Enumeration findResources(String name) throws IOException {
+ log.debug("findResources({})", name);
+ return super.findResources(name);
+ }
+
+ @Override
+ protected URL findResource(String moduleName, String name) throws IOException {
+ log.debug("findResource({}, {})", moduleName, name);
+ return super.findResource(moduleName, name);
+ }
+
+ @Override
+ public URL getResource(String name) {
+ log.debug("getResource({})", name);
+ URI uri = URI.create("string:///" + name);
+
+ if (overlay.content.containsKey(uri)) {
+ try {
+ return uri.toURL();
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ if (data.content.containsKey(uri)) {
+ try {
+ return uri.toURL();
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return super.getResource(name);
+ }
+
+ @Override
+ public InputStream getResourceAsStream(String name) {
+ log.debug("getResourceAsStream({})", name);
+ return super.getResourceAsStream(name);
+ }
+
+ @Override
+ public Enumeration getResources(String name) throws IOException {
+ log.debug("getResources({})", name);
+ URI uri = URI.create("string:///" + name);
+ if (!data.content.containsKey(uri) && !overlay.content.containsKey(uri))
+ uri = null;
+ URI uri2 = uri;
+
+ Enumeration r = super.getResources(name);
+ return new ConcatenatingEnumeration(uri2 == null ? new URL[0] : new URL[] { uri2.toURL() }, r);
+ }
+
+ public void defineOverlay(OverlayInMemoryFile... files) {
+ overlay = new InMemoryData();
+ for (OverlayInMemoryFile file : files) {
+ if (overlay.content.containsKey(file.toUri()))
+ throw new IllegalArgumentException("Overlaying two resources with the same name has not been implemented yet.");
+ overlay.content.put(file.toUri(), file.getCharContent(true).toString().getBytes(UTF_8));
+ }
+ InMemoryURLStreamHandler.activateOverlay(overlay);
+ }
+}
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryData.java b/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryData.java
new file mode 100644
index 0000000000..a57d1fc3b2
--- /dev/null
+++ b/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryData.java
@@ -0,0 +1,12 @@
+package com.predic8.membrane.annot.util;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A bunch of files (and their content), we hold in memory. Used to simulate the file system.
+ */
+public class InMemoryData {
+ public final Map content = new HashMap<>();
+}
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryFileObject.java b/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryFileObject.java
new file mode 100644
index 0000000000..80f862ede2
--- /dev/null
+++ b/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryFileObject.java
@@ -0,0 +1,86 @@
+package com.predic8.membrane.annot.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.tools.FileObject;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import java.io.*;
+import java.net.URI;
+
+public class InMemoryFileObject implements FileObject {
+ private static final Logger log = LoggerFactory.getLogger(InMemoryFileObject.class);
+
+ private final InMemoryData data;
+ protected final SimpleJavaFileObject inner;
+
+ public InMemoryFileObject(InMemoryData data, String path) {
+ this.data = data;
+ inner = new InnerFileObject(data, URI.create("string:///" + path), JavaFileObject.Kind.OTHER);
+ }
+
+ public InMemoryFileObject(InMemoryData data, String path, JavaFileObject.Kind kind) {
+ this.data = data;
+ inner = new InnerFileObject(data, URI.create("string:///" + path), kind);
+ }
+
+ @Override
+ public URI toUri() {
+ log.debug("toUri() {}", inner.toUri());
+ return inner.toUri();
+ }
+
+ @Override
+ public String getName() {
+ log.debug("getName() {}", inner.toUri());
+ return inner.getName();
+ }
+
+ @Override
+ public InputStream openInputStream() throws IOException {
+ log.debug("openInputStream() {}", inner.toUri());
+ return inner.openInputStream();
+ }
+
+ @Override
+ public OutputStream openOutputStream() throws IOException {
+ log.debug("openOutputStream() {}", inner.toUri());
+ return inner.openOutputStream();
+ }
+
+ @Override
+ public Reader openReader(boolean ignoreEncodingErrors) throws IOException {
+ log.debug("openReader({}) {}", ignoreEncodingErrors, inner.toUri());
+ return inner.openReader(ignoreEncodingErrors);
+ }
+
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+ log.debug("getCharContent({}) {}", ignoreEncodingErrors, inner.toUri());
+ return inner.getCharContent(ignoreEncodingErrors);
+ }
+
+ @Override
+ public Writer openWriter() throws IOException {
+ log.debug("openWriter() {}", inner.toUri());
+ return inner.openWriter();
+ }
+
+ @Override
+ public long getLastModified() {
+ log.debug("getLastModified() {}", inner.toUri());
+ return inner.getLastModified();
+ }
+
+ @Override
+ public boolean delete() {
+ log.debug("delete() {}", inner.toUri());
+ return inner.delete();
+ }
+
+ @Override
+ public String toString() {
+ return "InMemoryFileObject[" + inner.toString() + "]";
+ }
+}
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryJavaFileObject.java b/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryJavaFileObject.java
new file mode 100644
index 0000000000..42da83f51e
--- /dev/null
+++ b/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryJavaFileObject.java
@@ -0,0 +1,40 @@
+package com.predic8.membrane.annot.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.NestingKind;
+import javax.tools.JavaFileObject;
+
+public class InMemoryJavaFileObject extends InMemoryFileObject implements JavaFileObject {
+ private final static Logger log = LoggerFactory.getLogger(InMemoryJavaFileObject.class);
+
+ public InMemoryJavaFileObject(InMemoryData data, String path, Kind kind) {
+ super(data, path, kind);
+ }
+
+ @Override
+ public Kind getKind() {
+ log.debug("getKind() {}", inner.toUri());
+ return inner.getKind();
+ }
+
+ @Override
+ public boolean isNameCompatible(String simpleName, Kind kind) {
+ log.debug("isNameCompatible({}, {}) {}", simpleName, kind, inner.toUri());
+ return inner.isNameCompatible(simpleName, kind);
+ }
+
+ @Override
+ public NestingKind getNestingKind() {
+ log.debug("getNestingKind() {}", inner.toUri());
+ return inner.getNestingKind();
+ }
+
+ @Override
+ public Modifier getAccessLevel() {
+ log.debug("getAccessLevel() {}", inner.toUri());
+ return inner.getAccessLevel();
+ }
+}
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryURLStreamHandler.java b/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryURLStreamHandler.java
new file mode 100644
index 0000000000..3b3365a30b
--- /dev/null
+++ b/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryURLStreamHandler.java
@@ -0,0 +1,58 @@
+package com.predic8.membrane.annot.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.*;
+
+/**
+ * Helper to serve resources from the in-memory file system (and potentially its overlay file system).
+ */
+public class InMemoryURLStreamHandler extends URLStreamHandler {
+ private static final Logger log = LoggerFactory.getLogger(InMemoryURLStreamHandler.class);
+ private static InMemoryData data, overlay;
+
+ static {
+ URL.setURLStreamHandlerFactory(protocol -> "string".equals(protocol) ? new InMemoryURLStreamHandler() : null);
+ }
+
+ public static void activate(InMemoryData data) {
+ InMemoryURLStreamHandler.data = data;
+ }
+
+ public static void activateOverlay(InMemoryData overlay) {
+ InMemoryURLStreamHandler.overlay = overlay;
+ }
+
+ @Override
+ protected URLConnection openConnection(URL u) throws IOException {
+ log.debug("openConnection({})", u);
+ return new URLConnection(u) {
+ @Override
+ public void connect() throws IOException {
+
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ try {
+ URI uri = u.toURI();
+ if (overlay != null && overlay.content.containsKey(uri)) {
+ byte[] buffer = overlay.content.get(uri);
+ if (buffer == null)
+ throw new FileNotFoundException("No in-memory resource for " + uri);
+ return new ByteArrayInputStream(buffer);
+ }
+ return new ByteArrayInputStream(data.content.get(uri));
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ }
+
+}
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/InnerFileObject.java b/annot/src/test/java/com/predic8/membrane/annot/util/InnerFileObject.java
new file mode 100644
index 0000000000..f3244ccb0b
--- /dev/null
+++ b/annot/src/test/java/com/predic8/membrane/annot/util/InnerFileObject.java
@@ -0,0 +1,50 @@
+package com.predic8.membrane.annot.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.tools.SimpleJavaFileObject;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+public class InnerFileObject extends SimpleJavaFileObject {
+ private static final Logger log = LoggerFactory.getLogger(InnerFileObject.class);
+
+ private final InMemoryData data;
+
+ public InnerFileObject(InMemoryData data, URI uri, Kind kind) {
+ super(uri, kind);
+ this.data = data;
+ }
+
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+ byte[] bytes = data.content.get(toUri());
+ if (bytes == null)
+ return "";
+ return new String(bytes, UTF_8);
+ }
+
+ @Override
+ public OutputStream openOutputStream() throws IOException {
+ return new ByteArrayOutputStream() {
+ @Override
+ public void flush() throws IOException {
+ super.flush();
+ data.content.put(toUri(), toByteArray());
+ log.debug("wrote {} : {}", toUri(), new String(toByteArray(), UTF_8));
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ data.content.put(toUri(), toByteArray());
+ log.debug("wrote {} : {}", toUri(), new String(toByteArray(), UTF_8));
+ }
+ };
+ }
+}
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/LoggingFileObject.java b/annot/src/test/java/com/predic8/membrane/annot/util/LoggingFileObject.java
new file mode 100644
index 0000000000..545a885161
--- /dev/null
+++ b/annot/src/test/java/com/predic8/membrane/annot/util/LoggingFileObject.java
@@ -0,0 +1,72 @@
+package com.predic8.membrane.annot.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.tools.FileObject;
+import java.io.*;
+import java.net.URI;
+
+public class LoggingFileObject implements FileObject {
+ private final static Logger log = LoggerFactory.getLogger(LoggingFileObject.class);
+
+ private final FileObject inner;
+
+ public LoggingFileObject(FileObject inner) {
+ this.inner = inner;
+ }
+
+ @Override
+ public URI toUri() {
+ log.debug("toUri() {}", inner.toUri());
+ return inner.toUri();
+ }
+
+ @Override
+ public String getName() {
+ log.debug("getName() {}", inner.toUri());
+ return inner.getName();
+ }
+
+ @Override
+ public InputStream openInputStream() throws IOException {
+ log.debug("openInputStream() {}", inner.toUri());
+ return inner.openInputStream();
+ }
+
+ @Override
+ public OutputStream openOutputStream() throws IOException {
+ log.debug("openOutputStream() {}", inner.toUri());
+ return inner.openOutputStream();
+ }
+
+ @Override
+ public Reader openReader(boolean ignoreEncodingErrors) throws IOException {
+ log.debug("openReader({}) {}", ignoreEncodingErrors, inner.toUri());
+ return inner.openReader(ignoreEncodingErrors);
+ }
+
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+ log.debug("getCharContent({}) {}", ignoreEncodingErrors, inner.toUri());
+ return inner.getCharContent(ignoreEncodingErrors);
+ }
+
+ @Override
+ public Writer openWriter() throws IOException {
+ log.debug("openWriter() {}", inner.toUri());
+ return inner.openWriter();
+ }
+
+ @Override
+ public long getLastModified() {
+ log.debug("getLastModified() {}", inner.toUri());
+ return inner.getLastModified();
+ }
+
+ @Override
+ public boolean delete() {
+ log.debug("delete() {}", inner.toUri());
+ return inner.delete();
+ }
+}
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/LoggingInMemoryJavaFileManager.java b/annot/src/test/java/com/predic8/membrane/annot/util/LoggingInMemoryJavaFileManager.java
new file mode 100644
index 0000000000..5f95709099
--- /dev/null
+++ b/annot/src/test/java/com/predic8/membrane/annot/util/LoggingInMemoryJavaFileManager.java
@@ -0,0 +1,162 @@
+/* Copyright 2025 predic8 GmbH, www.predic8.com
+
+ Licensed 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 com.predic8.membrane.annot.util;
+
+import org.slf4j.Logger;
+
+import javax.tools.*;
+import java.io.*;
+import java.util.*;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * This implementation of JavaFileManager can either proxy the standard JavaFileManager
+ * (USE_IN_MEM=false) logging API calls but modifying the existing file system,
+ * or direct everything into memory (USE_IN_MEM=true).
+ *
+ * Not all possible use case are implemented for USE_IN_MEM=true.
+ */
+public class LoggingInMemoryJavaFileManager implements JavaFileManager {
+ /**
+ * If true, this class directs file system 'write' requests to an in-memory file system. (This has only been tested
+ * for certain use cases.)
+ *
+ * If false, this class logs file system 'write' requests and directs them to the underlying file system.
+ */
+ private static final boolean USE_IN_MEM = true;
+
+ private static final Logger log = getLogger(LoggingInMemoryJavaFileManager.class);
+
+ private final JavaFileManager fm;
+
+ private final InMemoryData data = new InMemoryData();
+
+ public LoggingInMemoryJavaFileManager(JavaFileManager fm) {
+ this.fm = fm;
+
+ InMemoryURLStreamHandler.activate(data);
+ }
+
+ @Override
+ public ClassLoader getClassLoader(Location location) {
+ log.debug("getClassLoader({})", location);
+ if (USE_IN_MEM && location == StandardLocation.CLASS_OUTPUT) {
+ return new InMemoryClassLoader(data);
+ }
+ return fm.getClassLoader(location);
+ }
+
+ @Override
+ public Iterable list(Location location, String packageName, Set kinds, boolean recurse) throws IOException {
+ StringBuilder l = new StringBuilder("list(%s, %s, %s, %s) -> ".formatted(location, packageName, kinds, recurse));
+ Iterable res = fm.list(location, packageName, kinds, recurse);
+ l.append("[");
+ for (JavaFileObject re : res) {
+ l.append(re).append(",");
+ }
+ log.debug("{}]", l);
+ return res;
+ }
+
+ @Override
+ public String inferBinaryName(Location location, JavaFileObject file) {
+ log.debug("inferBinaryName({}, {})", location, file);
+ return fm.inferBinaryName(location, file);
+ }
+
+ @Override
+ public boolean isSameFile(FileObject a, FileObject b) {
+ log.debug("isSameFile({}, {})", a, b);
+ return fm.isSameFile(a, b);
+ }
+
+ @Override
+ public boolean handleOption(String current, Iterator remaining) {
+ log.debug("handleOption({}, {})", current, remaining);
+ return fm.handleOption(current, remaining);
+ }
+
+ @Override
+ public boolean hasLocation(Location location) {
+ log.debug("hasLocation({})", location);
+ return fm.hasLocation(location);
+ }
+
+ @Override
+ public JavaFileObject getJavaFileForInput(Location location, String className, JavaFileObject.Kind kind) throws IOException {
+ String l = "getJavaFileForInput(%s, %s, %s) -> ".formatted(location, className, kind);
+ JavaFileObject res = fm.getJavaFileForInput(location, className, kind);
+ log.debug("{}{}", l, res);
+ return res;
+ }
+
+ @Override
+ public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
+ String l = "getJavaFileForOutput(%s, %s, %s, %s) -> ".formatted(location, className, kind, sibling);
+ JavaFileObject res = USE_IN_MEM ?
+ new InMemoryJavaFileObject(data, className.replace('.', '/') + kind.extension, kind) :
+ new LoggingJavaFileObject(fm.getJavaFileForOutput(location, className, kind, sibling));
+ log.debug("{}{}", l, res);
+ return res;
+ }
+
+ @Override
+ public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
+ String l = "getFileForInput(%s, %s, %s) -> ".formatted(location, packageName, relativeName);
+ FileObject res = fm.getFileForInput(location, packageName, relativeName);
+ log.debug("{}{}", l, res);
+ return res;
+ }
+
+ @Override
+ public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) throws IOException {
+ String l = "getFileForOutput(%s, %s, %s, %s) -> ".formatted(location, packageName, relativeName, sibling);
+ String path = packageName.isEmpty() ? "" : packageName.replace('.', '/') + "/";
+ FileObject res = USE_IN_MEM ? new InMemoryFileObject(data, path + relativeName) :
+ new LoggingFileObject(fm.getFileForOutput(location, packageName, relativeName, sibling));
+ log.debug("{}{}", l, res);
+ return res;
+ }
+
+ @Override
+ public void flush() throws IOException {
+ log.debug("flush()");
+ fm.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ log.debug("close()");
+ fm.close();
+ }
+
+ @Override
+ public int isSupportedOption(String option) {
+ log.debug("isSupportedOption({})", option);
+ return fm.isSupportedOption(option);
+ }
+
+ public Iterable> listLocationsForModules(Location location) throws IOException {
+ log.debug("listLocationsForModules({})", location);
+ return fm.listLocationsForModules(location);
+ }
+
+ @Override
+ public String inferModuleName(Location location) throws IOException {
+ log.debug("inferModuleName({})", location);
+ return fm.inferModuleName(location);
+ }
+
+}
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/LoggingJavaFileObject.java b/annot/src/test/java/com/predic8/membrane/annot/util/LoggingJavaFileObject.java
new file mode 100644
index 0000000000..bf02a19bce
--- /dev/null
+++ b/annot/src/test/java/com/predic8/membrane/annot/util/LoggingJavaFileObject.java
@@ -0,0 +1,44 @@
+package com.predic8.membrane.annot.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.NestingKind;
+import javax.tools.JavaFileObject;
+
+public class LoggingJavaFileObject extends LoggingFileObject implements JavaFileObject {
+ private final static Logger log = LoggerFactory.getLogger(LoggingJavaFileObject.class);
+
+ private final JavaFileObject inner;
+
+ public LoggingJavaFileObject(JavaFileObject inner) {
+ super(inner);
+ this.inner = inner;
+ }
+
+ @Override
+ public Kind getKind() {
+ log.debug("getKind() {}", inner.toUri());
+ return inner.getKind();
+ }
+
+ @Override
+ public boolean isNameCompatible(String simpleName, Kind kind) {
+ log.debug("isNameCompatible({}, {}) {}", simpleName, kind, inner.toUri());
+ return inner.isNameCompatible(simpleName, kind);
+ }
+
+ @Override
+ public NestingKind getNestingKind() {
+ log.debug("getNestingKind() {}", inner.toUri());
+ return inner.getNestingKind();
+ }
+
+ @Override
+ public Modifier getAccessLevel() {
+ log.debug("getAccessLevel() {}", inner.toUri());
+ return inner.getAccessLevel();
+ }
+
+}
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/OverlayInMemoryFile.java b/annot/src/test/java/com/predic8/membrane/annot/util/OverlayInMemoryFile.java
new file mode 100644
index 0000000000..9435646e6a
--- /dev/null
+++ b/annot/src/test/java/com/predic8/membrane/annot/util/OverlayInMemoryFile.java
@@ -0,0 +1,79 @@
+/* Copyright 2025 predic8 GmbH, www.predic8.com
+
+ Licensed 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 com.predic8.membrane.annot.util;
+
+import javax.tools.FileObject;
+import java.io.*;
+import java.net.URI;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * An in-memory file.
+ */
+public class OverlayInMemoryFile implements FileObject {
+ private final String name;
+ private final String content;
+
+ public OverlayInMemoryFile(String name, String content) {
+ super();
+ this.name = name;
+ this.content = content;
+ }
+
+ @Override
+ public URI toUri() {
+ return URI.create("string:" + name);
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public InputStream openInputStream() throws IOException {
+ return new ByteArrayInputStream(content.getBytes(UTF_8));
+ }
+
+ @Override
+ public OutputStream openOutputStream() throws IOException {
+ return null;
+ }
+
+ @Override
+ public Reader openReader(boolean ignoreEncodingErrors) throws IOException {
+ return new StringReader(content);
+ }
+
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) {
+ return content;
+ }
+
+ @Override
+ public Writer openWriter() throws IOException {
+ return null;
+ }
+
+ @Override
+ public long getLastModified() {
+ return 0;
+ }
+
+ @Override
+ public boolean delete() {
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryJavaFile.java b/annot/src/test/java/com/predic8/membrane/annot/util/OverlayInMemoryJavaFile.java
similarity index 88%
rename from annot/src/test/java/com/predic8/membrane/annot/util/InMemoryJavaFile.java
rename to annot/src/test/java/com/predic8/membrane/annot/util/OverlayInMemoryJavaFile.java
index d5e8a1618f..9ec5ee77c4 100644
--- a/annot/src/test/java/com/predic8/membrane/annot/util/InMemoryJavaFile.java
+++ b/annot/src/test/java/com/predic8/membrane/annot/util/OverlayInMemoryJavaFile.java
@@ -16,10 +16,10 @@
import javax.tools.SimpleJavaFileObject;
import java.net.URI;
-public class InMemoryJavaFile extends SimpleJavaFileObject {
+public class OverlayInMemoryJavaFile extends SimpleJavaFileObject {
private final String code;
- public InMemoryJavaFile(String name, String code) {
+ public OverlayInMemoryJavaFile(String name, String code) {
super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension),
Kind.SOURCE);
this.code = code;