diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c2f955e..a2068da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ slf4j = '2.0.12' antlr = { module = 'org.antlr:antlr4', version.ref = 'antlr' } antlr-runtime = { module = 'org.antlr:antlr4-runtime', version.ref = 'antlr' } asm = 'org.ow2.asm:asm:9.7' +classgraph = 'io.github.classgraph:classgraph:4.8.172' gradle-tooling = 'org.gradle:gradle-tooling-api:8.6' groovy = { module = 'org.apache.groovy:groovy', version.ref = 'groovy' } groovy-all = { module = 'org.apache.groovy:groovy-all', version.ref = 'groovy' } diff --git a/web-views/build.gradle b/web-views/build.gradle index 35059a6..58adced 100644 --- a/web-views/build.gradle +++ b/web-views/build.gradle @@ -35,6 +35,7 @@ dependencies { libs.groovy, libs.groovy.templates, libs.antlr.runtime, + libs.classgraph, project(':view-components'), project(':views') ) diff --git a/web-views/src/main/java/groowt/view/web/DefaultWebViewComponentTemplateCompiler.java b/web-views/src/main/java/groowt/view/web/DefaultWebViewComponentTemplateCompiler.java index 1815624..2d56b1b 100644 --- a/web-views/src/main/java/groowt/view/web/DefaultWebViewComponentTemplateCompiler.java +++ b/web-views/src/main/java/groowt/view/web/DefaultWebViewComponentTemplateCompiler.java @@ -1,10 +1,13 @@ package groowt.view.web; import groovy.lang.GroovyClassLoader; -import groowt.view.component.*; +import groowt.view.component.ComponentTemplate; +import groowt.view.component.ViewComponent; import groowt.view.component.compiler.CachingComponentTemplateCompiler; import groowt.view.component.compiler.ComponentTemplateCompileException; import groowt.view.component.factory.ComponentTemplateSource; +import groowt.view.web.analysis.MismatchedComponentTypeError; +import groowt.view.web.analysis.MismatchedComponentTypeErrorAnalysis; import groowt.view.web.antlr.CompilationUnitParseResult; import groowt.view.web.antlr.ParserUtil; import groowt.view.web.antlr.TokenList; @@ -25,6 +28,7 @@ import java.io.IOException; import java.io.Reader; import java.net.URI; import java.net.URISyntaxException; +import java.util.List; import java.util.Objects; public class DefaultWebViewComponentTemplateCompiler extends CachingComponentTemplateCompiler @@ -92,7 +96,8 @@ public class DefaultWebViewComponentTemplateCompiler extends CachingComponentTem ) { final CompilationUnitParseResult parseResult = ParserUtil.parseCompilationUnit(reader); - // TODO: analysis + final List mismatchedComponentTypeErrors = + MismatchedComponentTypeErrorAnalysis.check(parseResult.getCompilationUnitContext()); final var tokenList = new TokenList(parseResult.getTokenStream()); final var astBuilder = new DefaultAstBuilder(new DefaultNodeFactory(tokenList)); diff --git a/web-views/src/main/java/groowt/view/web/WebViewComponentTemplateCompileException.java b/web-views/src/main/java/groowt/view/web/WebViewComponentTemplateCompileException.java new file mode 100644 index 0000000..8161b20 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/WebViewComponentTemplateCompileException.java @@ -0,0 +1,53 @@ +package groowt.view.web; + +import groowt.view.component.ViewComponent; +import groowt.view.component.compiler.ComponentTemplateCompileException; +import groowt.view.web.ast.node.Node; +import groowt.view.web.util.SourcePosition; + +public class WebViewComponentTemplateCompileException extends ComponentTemplateCompileException { + + private final Node node; + + public WebViewComponentTemplateCompileException( + String message, + Class forClass, + Object templateSource, + Node node + ) { + super(message, forClass, templateSource); + this.node = node; + } + + public WebViewComponentTemplateCompileException( + String message, + Throwable cause, + Class forClass, + Object templateSource, + Node node + ) { + super(message, cause, forClass, templateSource); + this.node = node; + } + + public WebViewComponentTemplateCompileException( + Throwable cause, + Class forClass, + Object templateSource, + Node node + ) { + super(cause, forClass, templateSource); + this.node = node; + } + + public Node getNode() { + return this.node; + } + + @Override + public String getMessage() { + final SourcePosition start = this.node.getTokenRange().getStartPosition(); + return "Line " + start.line() + ", column " + start.column() + ": " + super.getMessage(); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/analysis/AnalysisError.java b/web-views/src/main/java/groowt/view/web/analysis/AnalysisError.java deleted file mode 100644 index 9672ab6..0000000 --- a/web-views/src/main/java/groowt/view/web/analysis/AnalysisError.java +++ /dev/null @@ -1,6 +0,0 @@ -package groowt.view.web.analysis; - -public interface AnalysisError { - T subject(); - String message(); -} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/analysis/Analyzer.java b/web-views/src/main/java/groowt/view/web/analysis/Analyzer.java deleted file mode 100644 index c6710f6..0000000 --- a/web-views/src/main/java/groowt/view/web/analysis/Analyzer.java +++ /dev/null @@ -1,7 +0,0 @@ -package groowt.view.web.analysis; - -import java.util.List; - -public sealed interface Analyzer permits ParseTreeAnalyzer, AstAnalyzer { - List analyze(T t); -} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/analysis/AstAnalyzer.java b/web-views/src/main/java/groowt/view/web/analysis/AstAnalyzer.java deleted file mode 100644 index b4b44fa..0000000 --- a/web-views/src/main/java/groowt/view/web/analysis/AstAnalyzer.java +++ /dev/null @@ -1,6 +0,0 @@ -package groowt.view.web.analysis; - -import groowt.view.web.ast.node.Node; - -@FunctionalInterface -public non-sealed interface AstAnalyzer> extends Analyzer {} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/analysis/AstError.java b/web-views/src/main/java/groowt/view/web/analysis/AstError.java deleted file mode 100644 index 7f0f748..0000000 --- a/web-views/src/main/java/groowt/view/web/analysis/AstError.java +++ /dev/null @@ -1,5 +0,0 @@ -package groowt.view.web.analysis; - -import groowt.view.web.ast.node.Node; - -public interface AstError extends AnalysisError {} diff --git a/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeAnalyzer.java b/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeAnalyzer.java deleted file mode 100644 index 72ad7e3..0000000 --- a/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeAnalyzer.java +++ /dev/null @@ -1,16 +0,0 @@ -package groowt.view.web.analysis; - -import groowt.view.web.antlr.WebViewComponentsParser.ComponentWithChildrenContext; -import org.antlr.v4.runtime.tree.ParseTree; - -import java.util.List; - -public final class MismatchedComponentTypeAnalyzer - implements ParseTreeAnalyzer { - - @Override - public List analyze(ParseTree parseTree) { - return MismatchedComponentTypeErrorAnalyzerKt.check(parseTree); - } - -} diff --git a/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeError.java b/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeError.java deleted file mode 100644 index 4fb2313..0000000 --- a/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeError.java +++ /dev/null @@ -1,6 +0,0 @@ -package groowt.view.web.analysis; - -import groowt.view.web.antlr.WebViewComponentsParser.ComponentWithChildrenContext; - -public record MismatchedComponentTypeError(ComponentWithChildrenContext subject, String message) - implements ParseTreeAnalysisError {} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeErrorAnalyzer.kt b/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeErrorAnalysis.kt similarity index 94% rename from web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeErrorAnalyzer.kt rename to web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeErrorAnalysis.kt index 5984c42..e92f895 100644 --- a/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeErrorAnalyzer.kt +++ b/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeErrorAnalysis.kt @@ -1,3 +1,4 @@ +@file:JvmName("MismatchedComponentTypeErrorAnalysis") package groowt.view.web.analysis import groowt.view.web.antlr.WebViewComponentsParser.ComponentTypeContext @@ -51,6 +52,8 @@ private fun doCheck(tree: ParseTree, destination: MutableList { val result: MutableList = ArrayList() doCheck(tree, result) diff --git a/web-views/src/main/java/groowt/view/web/analysis/ParseTreeAnalysisError.java b/web-views/src/main/java/groowt/view/web/analysis/ParseTreeAnalysisError.java deleted file mode 100644 index b99d1eb..0000000 --- a/web-views/src/main/java/groowt/view/web/analysis/ParseTreeAnalysisError.java +++ /dev/null @@ -1,5 +0,0 @@ -package groowt.view.web.analysis; - -import org.antlr.v4.runtime.tree.ParseTree; - -public interface ParseTreeAnalysisError extends AnalysisError {} diff --git a/web-views/src/main/java/groowt/view/web/analysis/ParseTreeAnalyzer.java b/web-views/src/main/java/groowt/view/web/analysis/ParseTreeAnalyzer.java deleted file mode 100644 index f3518ba..0000000 --- a/web-views/src/main/java/groowt/view/web/analysis/ParseTreeAnalyzer.java +++ /dev/null @@ -1,7 +0,0 @@ -package groowt.view.web.analysis; - -import org.antlr.v4.runtime.tree.ParseTree; - -@FunctionalInterface -public non-sealed interface ParseTreeAnalyzer> - extends Analyzer {} diff --git a/web-views/src/main/java/groowt/view/web/analysis/classes/ClassLoaderClassLocator.java b/web-views/src/main/java/groowt/view/web/analysis/classes/ClassLoaderClassLocator.java deleted file mode 100644 index 33fff03..0000000 --- a/web-views/src/main/java/groowt/view/web/analysis/classes/ClassLoaderClassLocator.java +++ /dev/null @@ -1,129 +0,0 @@ -package groowt.view.web.analysis.classes; - -import groovy.lang.GroovyClassLoader; -import org.codehaus.groovy.control.CompilationFailedException; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.BiPredicate; - -public non-sealed class ClassLoaderClassLocator implements ClassLocator { - - private static final Logger logger = LoggerFactory.getLogger(ClassLoaderClassLocator.class); - - protected sealed interface CachedLocatedClass - permits ClazzCachedLocatedClass, FailedGroovyCachedLocatedClass, CustomCachedLocatedClass {} - - protected record ClazzCachedLocatedClass(Class cached) implements CachedLocatedClass {} - - protected record FailedGroovyCachedLocatedClass( - CompilationFailedException exception) implements CachedLocatedClass {} - - protected non-sealed interface CustomCachedLocatedClass extends CachedLocatedClass { - Class get(); - } - - protected final ClassLoader classLoader; - private final Map cache = new HashMap<>(); - - public ClassLoaderClassLocator(ClassLoader classLoader) { - this.classLoader = classLoader; - } - - public ClassLoaderClassLocator() { - this.classLoader = Thread.currentThread().getContextClassLoader(); - } - - protected final void addToCache(String name, CachedLocatedClass cachedLocatedClass) { - this.cache.put(name, cachedLocatedClass); - } - - protected final boolean cacheHas(String name) { - return this.cache.containsKey(name); - } - - protected final void removeFromCacheIf(BiPredicate predicate) { - final List targets = new ArrayList<>(); - this.cache.forEach((name, cached) -> { - if (predicate.test(name, cached)) { - targets.add(name); - } - }); - targets.forEach(this.cache::remove); - } - - protected final void removeFromCacheIf(Class ofType, BiPredicate predicate) { - this.removeFromCacheIf((name, cached) -> { - if (ofType.isAssignableFrom(cached.getClass())) { - return predicate.test(name, ofType.cast(cached)); - } - return false; - }); - } - - protected final void removeFromCacheByType(Class type) { - this.removeFromCacheIf((name, cached) -> type.isAssignableFrom(cached.getClass())); - } - - protected final Map getFromCacheByType(Class type) { - final Map result = new HashMap<>(); - this.cache.forEach((name, cached) -> { - if (type.isAssignableFrom(cached.getClass())) { - result.put(name, type.cast(cached)); - } - }); - return result; - } - - protected final @Nullable Class loadFromCache(String name) { - final var cachedLocated = this.cache.getOrDefault(name, null); - if (cachedLocated == null) { - return null; - } else { - return switch (cachedLocated) { - case ClazzCachedLocatedClass(var cached) -> cached; - case CustomCachedLocatedClass custom -> custom.get(); - case FailedGroovyCachedLocatedClass(var exception) -> - throw new RuntimeException("Cannot load Groovy class because compilation failed.", exception); - }; - } - } - - protected @Nullable Class searchClassLoader(String name) { - if (classLoader instanceof GroovyClassLoader gcl) { - try { - Class clazz = gcl.loadClass(name, true, true, false); - this.addToCache(name, new ClazzCachedLocatedClass(clazz)); - return clazz; - } catch (ClassNotFoundException ignored) { - // Ignored - } catch (CompilationFailedException cfe) { - logger.warn("Could not compile class: {}", name); - this.addToCache(name, new FailedGroovyCachedLocatedClass(cfe)); - // return null because we don't actually have a class - return null; - } - } else { - try { - Class clazz = classLoader.loadClass(name); - this.addToCache(name, new ClazzCachedLocatedClass(clazz)); - return clazz; - } catch (ClassNotFoundException ignored) {} - } return null; - } - - @Override - public boolean hasClassForFQN(String name) { - return this.cacheHas(name) || this.searchClassLoader(name) != null; - } - - public void clearCache() { - this.cache.clear(); - } - -} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/analysis/classes/ClassLocator.java b/web-views/src/main/java/groowt/view/web/analysis/classes/ClassLocator.java deleted file mode 100644 index c69bfc5..0000000 --- a/web-views/src/main/java/groowt/view/web/analysis/classes/ClassLocator.java +++ /dev/null @@ -1,5 +0,0 @@ -package groowt.view.web.analysis.classes; - -public sealed interface ClassLocator permits ClassLoaderClassLocator, PreambleAwareClassLocator { - boolean hasClassForFQN(String name); -} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/analysis/classes/PreambleAwareClassLocator.java b/web-views/src/main/java/groowt/view/web/analysis/classes/PreambleAwareClassLocator.java deleted file mode 100644 index d818fd7..0000000 --- a/web-views/src/main/java/groowt/view/web/analysis/classes/PreambleAwareClassLocator.java +++ /dev/null @@ -1,90 +0,0 @@ -package groowt.view.web.analysis.classes; - -import groovy.lang.GroovyClassLoader; -import groowt.view.web.antlr.MergedGroovyCodeToken; -import groowt.view.web.antlr.WebViewComponentsParser.PreambleContext; -import org.codehaus.groovy.ast.ASTNode; -import org.codehaus.groovy.ast.ClassNode; -import org.codehaus.groovy.ast.builder.AstStringCompiler; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -public non-sealed class PreambleAwareClassLocator extends ClassLoaderClassLocator implements ClassLocator { - - private final List currentClassNodes = new ArrayList<>(); - private GroovyClassLoader currentGroovyClassLoader; - - public PreambleAwareClassLocator(ClassLoader classLoader) { - super(classLoader); - } - - protected class ClassNodeCachedLocatedClass implements CustomCachedLocatedClass { - private final ClassNode classNode; - private Class lazyLoadedClass; - - public ClassNodeCachedLocatedClass(ClassNode classNode) { - this.classNode = classNode; - } - - @Override - public Class get() { - if (this.lazyLoadedClass == null) { - try { - final File tmp = File.createTempFile("preambleContextAwareClassLocator", "_" + System.currentTimeMillis()); - this.lazyLoadedClass = currentGroovyClassLoader.defineClass(this.classNode, null, tmp.getAbsolutePath()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - return this.lazyLoadedClass; - } - } - - public void setCurrentPreamble(PreambleContext preambleContext) { - this.currentGroovyClassLoader = new GroovyClassLoader(this.classLoader); - this.currentClassNodes.clear(); - final MergedGroovyCodeToken groovyCodeToken = (MergedGroovyCodeToken) preambleContext.GroovyCode().getSymbol(); - final String groovyCode = groovyCodeToken.getText(); - final List astNodes = new AstStringCompiler().compile(groovyCode); - astNodes.forEach(groovyASTNode -> { - if (groovyASTNode instanceof ClassNode classNode) { - this.currentClassNodes.add(classNode); - } - }); - } - - private @Nullable ClassNode searchPreambleSimpleName(String simpleName) { - for (final ClassNode classNode : this.currentClassNodes) { - if (classNode.getNameWithoutPackage().equals(simpleName)) { - this.addToCache(classNode.getName(), new ClassNodeCachedLocatedClass(classNode)); - return classNode; - } - } - return null; - } - - @Override - public boolean hasClassForFQN(String name) { - return super.hasClassForFQN(name) || this.searchPreambleSimpleName(name) != null; - } - - private boolean hasSimpleNameInCache(String simpleName) { - final Collection allCached = this.getFromCacheByType(ClassNodeCachedLocatedClass.class).values(); - for (final ClassNodeCachedLocatedClass cached : allCached) { - if (cached.classNode.getNameWithoutPackage().equals(simpleName)) { - return true; - } - } - return false; - } - - public boolean hasClassForSimpleName(String simpleName) { - return this.hasSimpleNameInCache(simpleName) || this.searchPreambleSimpleName(simpleName) != null; - } - -} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java index c363ef9..dd13031 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java @@ -56,7 +56,7 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { } public void setAppendOrAddStatementFactory(AppendOrAddStatementFactory appendOrAddStatementFactory) { - this.appendOrAddStatementFactory = Objects.requireNonNull(appendOrAddStatementFactory); + this.appendOrAddStatementFactory = appendOrAddStatementFactory; } // ViewComponent c0 diff --git a/web-views/src/main/java/groowt/view/web/transpile/resolve/ClassIdentifier.java b/web-views/src/main/java/groowt/view/web/transpile/resolve/ClassIdentifier.java new file mode 100644 index 0000000..c8debb3 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/resolve/ClassIdentifier.java @@ -0,0 +1,39 @@ +package groowt.view.web.transpile.resolve; + +import org.jetbrains.annotations.NotNull; + +public sealed class ClassIdentifier permits ClassIdentifierWithFqn { + + private final String alias; + + public ClassIdentifier(@NotNull String alias) { + this.alias = alias; + } + + /** + * @return the alias (the name without the package name) + */ + public String getAlias() { + return this.alias; + } + + @Override + public final boolean equals(Object obj) { + if (this == obj) return true; + if (obj instanceof ClassIdentifier other) { + return this.alias.equals(other.alias); + } + return false; + } + + @Override + public final int hashCode() { + return this.alias.hashCode(); + } + + @Override + public String toString() { + return "ClassIdentifier(" + this.alias + ")"; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/resolve/ClassIdentifierWithFqn.java b/web-views/src/main/java/groowt/view/web/transpile/resolve/ClassIdentifierWithFqn.java new file mode 100644 index 0000000..4855165 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/resolve/ClassIdentifierWithFqn.java @@ -0,0 +1,23 @@ +package groowt.view.web.transpile.resolve; + +import org.jetbrains.annotations.NotNull; + +public final class ClassIdentifierWithFqn extends ClassIdentifier { + + private final String fqn; + + public ClassIdentifierWithFqn(@NotNull String alias, @NotNull String fqn) { + super(alias); + this.fqn = fqn; + } + + public String getFqn() { + return this.fqn; + } + + @Override + public String toString() { + return "ClassIdentifierWithFqn(" + this.getAlias() + ", " + this.fqn + ")"; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/resolve/ComponentClassNodeResolver.java b/web-views/src/main/java/groowt/view/web/transpile/resolve/ComponentClassNodeResolver.java new file mode 100644 index 0000000..9bdba71 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/resolve/ComponentClassNodeResolver.java @@ -0,0 +1,14 @@ +package groowt.view.web.transpile.resolve; + +import groowt.view.web.util.Either; +import org.codehaus.groovy.ast.ClassNode; +import org.jetbrains.annotations.Nullable; + +public interface ComponentClassNodeResolver { + + record ClassNodeResolveError(ClassIdentifier identifier, String getMessage, @Nullable Throwable getCause) {} + + Either getClassForFqn(String fqn); + Either getClassForNameWithoutPackage(String nameWithoutPackage); + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/resolve/DefaultComponentClassNodeResolver.java b/web-views/src/main/java/groowt/view/web/transpile/resolve/DefaultComponentClassNodeResolver.java new file mode 100644 index 0000000..4be01e7 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/resolve/DefaultComponentClassNodeResolver.java @@ -0,0 +1,174 @@ +package groowt.view.web.transpile.resolve; + +import groowt.view.web.util.Either; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassInfoList; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ModuleNode; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +public class DefaultComponentClassNodeResolver implements ComponentClassNodeResolver { + + private static final Pattern aliasPattern = + Pattern.compile("^(\\P{Lu}+\\.)*(?\\p{Lu}.+(\\.\\p{Lu}.+)*)$"); + + protected static ClassNode getClassNode(Class clazz) { + return ClassHelper.makeCached(clazz); + } + + protected static String getAlias(String name) { + final var matcher = aliasPattern.matcher(name); + if (matcher.matches()) { + return matcher.group("alias"); + } else { + throw new IllegalArgumentException("Cannot determine alias from " + name); + } + } + + private final ModuleNode moduleNode; + protected final ClassLoader classLoader; + private final ClassInfoList classInfoList; + private final Map cache = new HashMap<>(); + + public DefaultComponentClassNodeResolver( + ModuleNode moduleNode, + ClassLoader classLoader, + ClassInfoList webViewComponentClassInfoList + ) { + this.moduleNode = moduleNode; + this.classLoader = classLoader; + this.classInfoList = webViewComponentClassInfoList; + } + + protected final void addToCache(ClassIdentifier identifier, ClassNode clazz) { + this.cache.put(identifier, clazz); + } + + protected final Either resolveWithClassLoader(ClassIdentifierWithFqn identifier) { + try { + Class clazz = this.classLoader.loadClass(identifier.getFqn()); + final var classNode = getClassNode(clazz); + return Either.right(classNode); + } catch (ClassNotFoundException classNotFoundException) { + return Either.left( + new ClassNodeResolveError( + identifier, + "Could not find class " + identifier.getFqn() + " with classLoader " + + this.classLoader, + classNotFoundException + ) + ); + } + } + + protected final Either resolveWithClassGraph(ClassIdentifier identifier) { + final List potential = this.classInfoList.stream() + .filter(classInfo -> classInfo.getSimpleName().equals(identifier.getAlias())) + .toList(); + if (potential.size() > 1) { + final var error = new ClassNodeResolveError( + identifier, + "There is more than one class on the classpath implementing WebViewComponent that has " + + "the simple name " + identifier.getAlias() + ". Please explicitly import the desired " + + "component class in the preamble, or use the fully qualified name of the component " + + "you wish to use.", + null + ); + return Either.left(error); + } else if (potential.size() == 1) { + final var classInfo = potential.getFirst(); + final ClassNode result = getClassNode(classInfo.loadClass()); + return Either.right(result); + } else { + final var error = new ClassNodeResolveError( + identifier, + "Could not resolve a class implementing WebViewComponent for " + identifier.getAlias(), + null + ); + return Either.left(error); + } + } + + protected final @Nullable ClassNode findInCacheFqn(String fqn) { + for (final var entry : this.cache.entrySet()) { + final var identifier = entry.getKey(); + final var classNode = entry.getValue(); + if (classNode != null + && identifier instanceof ClassIdentifierWithFqn withFqn + && withFqn.getFqn().equals(fqn) + ) { + return classNode; + } + } + return null; + } + + protected final @Nullable ClassNode findInCacheSimpleName(String simpleName) { + for (final var entry : this.cache.entrySet()) { + final var identifier = entry.getKey(); + final var classNode = entry.getValue(); + if (classNode != null && identifier.getAlias().equals(simpleName)) { + return classNode; + } + } + return null; + } + + @Override + public Either getClassForFqn(String fqn) { + // try cache + final var identifier = new ClassIdentifierWithFqn(getAlias(fqn), fqn); + final var fromCache = this.findInCacheFqn(fqn); + if (fromCache != null) { + return Either.right(fromCache); + } + + // do not try preamble, because it is a fully qualified name; i.e., it needs no import + + // try classLoader + final var classLoaderResolved = this.resolveWithClassLoader(identifier); + if (classLoaderResolved.isRight()) { + this.addToCache(identifier, classLoaderResolved.asRight().get()); + } + return classLoaderResolved; + } + + @Override + public Either getClassForNameWithoutPackage(String nameWithoutPackage) { + // try cache + final var identifier = new ClassIdentifier(nameWithoutPackage); + final var fromCache = this.findInCacheSimpleName(nameWithoutPackage); + if (fromCache != null) { + return Either.right(fromCache); + } + + // try imports + final var importedClassNode = this.moduleNode.getImportType(nameWithoutPackage); + if (importedClassNode != null) { + this.addToCache( + new ClassIdentifierWithFqn( + importedClassNode.getName(), + importedClassNode.getNameWithoutPackage() + ), + importedClassNode + ); + return Either.right(importedClassNode); + } + + // try classgraph + final var classGraphResolved = this.resolveWithClassGraph(identifier); + if (classGraphResolved.isRight()) { + final ClassNode resolvedClassNode = classGraphResolved.asRight().get(); + final var withFqn = new ClassIdentifierWithFqn(nameWithoutPackage, resolvedClassNode.getName()); + this.addToCache(withFqn, resolvedClassNode); + } + return classGraphResolved; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/Either.java b/web-views/src/main/java/groowt/view/web/util/Either.java new file mode 100644 index 0000000..2e9719d --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/Either.java @@ -0,0 +1,48 @@ +package groowt.view.web.util; + +public sealed interface Either { + + @SuppressWarnings("unchecked") + static Either left(E error) { + return (Either) new Left<>((Class) error.getClass(), error); + } + + @SuppressWarnings("unchecked") + static Either right(T item) { + return (Either) new Right<>((Class) item.getClass(), item); + } + + record Left(Class errorClass, E error) implements Either { + + public E get() { + return this.errorClass.cast(this.error); + } + } + + record Right(Class itemClass, T item) implements Either { + + public T get() { + return this.itemClass.cast(this.item); + } + + } + + default boolean isLeft() { + return this instanceof Either.Left; + } + + default boolean isRight() { + return this instanceof Either.Right; + } + + @SuppressWarnings("unchecked") + default Left asLeft() { + return (Left) this; + } + + @SuppressWarnings("unchecked") + default Right asRight() { + return (Right) this; + } + +}