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 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 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 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 sources) { + return stream(sources.spliterator(), false) + .filter(i -> i instanceof JavaFileObject) + .map(i -> (JavaFileObject) i) + .toList(); + } + + private static List getResources(Iterable sources) { + return stream(sources.spliterator(), false) + .filter(i -> i instanceof OverlayInMemoryFile) + .map(i -> (OverlayInMemoryFile) i) + .toList(); + } + + private static void copyResourcesToOutput(List 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;