From 3a171b8736d14936820fa1df1dad16969dacd62a Mon Sep 17 00:00:00 2001 From: JesseBrault0709 <62299747+JesseBrault0709@users.noreply.github.com> Date: Wed, 8 May 2024 13:51:49 +0200 Subject: [PATCH] Major refactoring of compiler, transpiler, and general api. --- .../view/component/AbstractViewComponent.java | 71 +- .../groowt/view/component/ViewComponent.java | 4 - .../view/component/ViewComponentBugError.java | 31 + .../AbstractComponentTemplateCompileUnit.java | 30 + .../AbstractComponentTemplateCompiler.java | 41 - .../CachingComponentTemplateCompiler.java | 127 +-- .../ComponentTemplateClassFactory.java | 7 + ...omponentTemplateCompileErrorException.java | 61 -- .../ComponentTemplateCompileException.java | 37 + .../ComponentTemplateCompileResult.java | 10 + .../ComponentTemplateCompileUnit.java | 18 + .../compiler/ComponentTemplateCompiler.java | 31 +- ...omponentTemplateCompilerConfiguration.java | 11 + ...omponentTemplateCompilerConfiguration.java | 44 ++ .../SimpleComponentTemplateClassFactory.java | 108 +++ .../SimpleComponentTemplateCompileResult.java | 50 ++ .../source/ComponentTemplateSource.java | 53 ++ .../component/compiler/source/FileSource.java | 50 ++ .../compiler/source/InputStreamSource.java | 40 + .../compiler/source/ReaderSource.java | 38 + .../compiler/source/StringSource.java | 39 + .../component/compiler/source/URISource.java | 50 ++ .../component/compiler/source/URLSource.java | 58 ++ .../component/context/ComponentContext.java | 54 +- .../context/ComponentCreateException.java | 37 - .../context/ComponentResolveException.java | 141 ++++ .../component/context/ComponentScope.java | 62 +- .../context/DefaultComponentContext.java | 130 +--- .../context/DefaultComponentScope.java | 51 +- .../FactoryMissingUnsupportedException.java | 48 ++ .../context/MissingClassTypeException.java | 19 - .../context/MissingComponentException.java | 31 - .../context/MissingFragmentTypeException.java | 16 - .../context/MissingStringTypeException.java | 19 - .../context/NoFactoryMissingException.java | 19 - ...a => AbstractClosureComponentFactory.java} | 43 +- .../ClassTypeClosureComponentFactory.java | 34 + .../component/factory/ComponentFactories.java | 27 + .../component/factory/ComponentFactory.java | 15 +- .../factory/ComponentFactoryBase.java | 9 +- .../factory/ComponentTemplateSource.java | 82 -- .../StringTypeClosureComponentFactory.java | 25 + .../factory/SupplierComponentFactory.java | 20 - .../runtime/ComponentCreateException.java | 91 +++ .../component/runtime/ComponentWriter.java | 32 + .../runtime/DefaultComponentWriter.java | 63 +- .../runtime/DefaultRenderContext.java | 141 ++++ .../view/component/runtime/RenderContext.java | 43 + web-views/build.gradle | 14 +- .../asciidoc/componentTemplateSpec.asciidoc | 130 ++++ .../sketching/memberClassComponentType.wvc | 4 + web-views/sketching/simpleGreeter.wvc | 10 + .../view/web/BaseWebViewComponent.groovy | 46 ++ .../view/web/DefaultWebViewComponent.groovy | 78 -- .../web/DefaultWebViewComponentContext.groovy | 18 +- .../view/web/WebViewComponentFactories.groovy | 32 +- .../view/web/WebViewComponentScope.groovy | 41 + .../groowt/view/web/WebViewScope.groovy | 14 - .../web/lib/DelegatingWebViewComponent.java | 15 +- .../groovy/groowt/view/web/lib/Echo.groovy | 92 +-- .../groowt/view/web/lib/Fragment.groovy | 4 +- .../groowt/view/web/lib/HtmlPage.groovy | 6 + .../groowt/view/web/lib/IntrinsicHtml.groovy | 71 +- .../web/util/ComponentConfigurator.groovy | 27 + .../web/util/ConfigurableComponent.groovy | 22 + .../view/web/util/ContextConfigurator.groovy | 27 + .../groowt/view/web/util/WithHtml.groovy | 30 + .../view/web/AbstractWebViewComponent.java | 43 +- .../web/WebViewChildComponentRenderer.java | 19 - .../view/web/WebViewChildGStringRenderer.java | 23 - .../view/web/WebViewChildJStringRenderer.java | 18 - .../groowt/view/web/WebViewChildRenderer.java | 22 - .../groowt/view/web/WebViewComponent.java | 37 +- .../view/web/WebViewComponentBugError.java | 31 + .../view/web/WebViewComponentChild.java | 64 ++ .../view/web/WebViewComponentContext.java | 13 +- .../antlr/AbstractWebViewComponentsLexer.java | 10 +- .../java/groowt/view/web/antlr/TokenUtil.kt | 10 + .../web/ast/node/ClassComponentTypeNode.java | 1 + .../compiler/AnonymousWebViewComponent.java | 49 ++ ...faultWebViewComponentTemplateCompiler.java | 314 ++------ ...ebViewComponentCompileErrorsException.java | 38 +- ...ViewComponentTemplateCompileException.java | 81 +- .../WebViewComponentTemplateCompileUnit.java | 86 ++ .../WebViewComponentTemplateCompiler.java | 21 +- ...efaultWebViewComponentChildCollection.java | 38 - ...DefaultWebViewComponentChildCollector.java | 43 + .../DefaultWebViewComponentRenderContext.java | 41 + .../WebViewComponentChildCollection.java | 20 - .../WebViewComponentChildCollector.java | 14 + ...WebViewComponentChildCollectorClosure.java | 29 + .../WebViewComponentRenderContext.java | 14 + .../web/runtime/WebViewComponentWriter.java | 14 - .../AppendOrAddStatementFactory.java | 4 + .../web/transpile/ComponentTranspiler.java | 5 +- .../DefaultAppendOrAddStatementFactory.java | 2 +- .../web/transpile/DefaultBodyTranspiler.java | 10 +- .../transpile/DefaultComponentTranspiler.java | 736 ++++++++++-------- .../transpile/DefaultGroovyTranspiler.java | 224 ++++-- .../DefaultTranspilerConfiguration.java | 25 +- .../transpile/DefaultValueNodeTranspiler.java | 16 +- .../view/web/transpile/GroovyTranspiler.java | 15 +- .../view/web/transpile/TranspilerUtil.java | 141 +++- .../CachingComponentClassNodeResolver.java | 60 ++ .../transpile/resolve/ClassIdentifier.java | 39 - .../resolve/ClassIdentifierWithFqn.java | 23 - ...ClassLoaderComponentClassNodeResolver.java | 55 ++ .../resolve/ComponentClassNodeResolver.java | 36 +- .../DefaultComponentClassNodeResolver.java | 187 ----- .../ModuleNodeComponentClassNodeResolver.java | 51 ++ .../web/transpile/resolve/ResolveUtil.java | 31 + .../java/groowt/view/web/util/Either.java | 54 +- .../groowt/view/web/util/LazyProvider.java | 26 + .../java/groowt/view/web/util/Property.java | 5 + .../java/groowt/view/web/util/Provider.java | 17 + .../groowt/view/web/util/SimpleProvider.java | 19 + .../groowt/view/web/sketching/Greeters.groovy | 18 + ...roovy => BaseWebViewComponentTests.groovy} | 34 +- .../groowt/view/web/lib/EchoTests.groovy | 16 +- .../groowt/view/web/lib/FragmentTests.groovy | 24 +- .../DefaultBodyTranspilerTests.java | 15 +- .../DefaultGroovyTranspilerTests.java | 8 +- .../transpiler/resolve/ResolveUtilTests.java | 25 + .../src/{main => test}/resources/log4j2.xml | 2 +- .../lib/AbstractWebViewComponentTests.groovy | 30 +- .../groowt/view/web/lib/WithContext.groovy | 13 +- .../web/transpiler/BodyTranspilerTests.java | 44 +- .../web/transpiler/GroovyTranspilerTests.java | 46 +- web-views/src/tools/binTemplate.gst | 2 +- .../view/web/tools/ConvertToGroovy.groovy | 66 +- .../groowt/view/web/tools/RunTemplate.groovy | 46 +- web-views/src/tools/resources/log4j2.xml | 17 + 132 files changed, 3879 insertions(+), 2343 deletions(-) create mode 100644 view-components/src/main/java/groowt/view/component/ViewComponentBugError.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/AbstractComponentTemplateCompileUnit.java delete mode 100644 view-components/src/main/java/groowt/view/component/compiler/AbstractComponentTemplateCompiler.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateClassFactory.java delete mode 100644 view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileErrorException.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileException.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileResult.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileUnit.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompilerConfiguration.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/DefaultComponentTemplateCompilerConfiguration.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/SimpleComponentTemplateClassFactory.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/SimpleComponentTemplateCompileResult.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/source/ComponentTemplateSource.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/source/FileSource.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/source/InputStreamSource.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/source/ReaderSource.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/source/StringSource.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/source/URISource.java create mode 100644 view-components/src/main/java/groowt/view/component/compiler/source/URLSource.java delete mode 100644 view-components/src/main/java/groowt/view/component/context/ComponentCreateException.java create mode 100644 view-components/src/main/java/groowt/view/component/context/ComponentResolveException.java create mode 100644 view-components/src/main/java/groowt/view/component/context/FactoryMissingUnsupportedException.java delete mode 100644 view-components/src/main/java/groowt/view/component/context/MissingClassTypeException.java delete mode 100644 view-components/src/main/java/groowt/view/component/context/MissingComponentException.java delete mode 100644 view-components/src/main/java/groowt/view/component/context/MissingFragmentTypeException.java delete mode 100644 view-components/src/main/java/groowt/view/component/context/MissingStringTypeException.java delete mode 100644 view-components/src/main/java/groowt/view/component/context/NoFactoryMissingException.java rename view-components/src/main/java/groowt/view/component/factory/{ClosureComponentFactory.java => AbstractClosureComponentFactory.java} (58%) create mode 100644 view-components/src/main/java/groowt/view/component/factory/ClassTypeClosureComponentFactory.java create mode 100644 view-components/src/main/java/groowt/view/component/factory/ComponentFactories.java delete mode 100644 view-components/src/main/java/groowt/view/component/factory/ComponentTemplateSource.java create mode 100644 view-components/src/main/java/groowt/view/component/factory/StringTypeClosureComponentFactory.java delete mode 100644 view-components/src/main/java/groowt/view/component/factory/SupplierComponentFactory.java create mode 100644 view-components/src/main/java/groowt/view/component/runtime/ComponentCreateException.java create mode 100644 view-components/src/main/java/groowt/view/component/runtime/ComponentWriter.java rename web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentWriter.java => view-components/src/main/java/groowt/view/component/runtime/DefaultComponentWriter.java (54%) create mode 100644 view-components/src/main/java/groowt/view/component/runtime/DefaultRenderContext.java create mode 100644 view-components/src/main/java/groowt/view/component/runtime/RenderContext.java create mode 100644 web-views/docs/asciidoc/componentTemplateSpec.asciidoc create mode 100644 web-views/sketching/memberClassComponentType.wvc create mode 100644 web-views/src/main/groovy/groowt/view/web/BaseWebViewComponent.groovy delete mode 100644 web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponent.groovy create mode 100644 web-views/src/main/groovy/groowt/view/web/WebViewComponentScope.groovy delete mode 100644 web-views/src/main/groovy/groowt/view/web/WebViewScope.groovy create mode 100644 web-views/src/main/groovy/groowt/view/web/lib/HtmlPage.groovy create mode 100644 web-views/src/main/groovy/groowt/view/web/util/ComponentConfigurator.groovy create mode 100644 web-views/src/main/groovy/groowt/view/web/util/ConfigurableComponent.groovy create mode 100644 web-views/src/main/groovy/groowt/view/web/util/ContextConfigurator.groovy create mode 100644 web-views/src/main/groovy/groowt/view/web/util/WithHtml.groovy delete mode 100644 web-views/src/main/java/groowt/view/web/WebViewChildComponentRenderer.java delete mode 100644 web-views/src/main/java/groowt/view/web/WebViewChildGStringRenderer.java delete mode 100644 web-views/src/main/java/groowt/view/web/WebViewChildJStringRenderer.java delete mode 100644 web-views/src/main/java/groowt/view/web/WebViewChildRenderer.java create mode 100644 web-views/src/main/java/groowt/view/web/WebViewComponentBugError.java create mode 100644 web-views/src/main/java/groowt/view/web/WebViewComponentChild.java create mode 100644 web-views/src/main/java/groowt/view/web/compiler/AnonymousWebViewComponent.java create mode 100644 web-views/src/main/java/groowt/view/web/compiler/WebViewComponentTemplateCompileUnit.java delete mode 100644 web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentChildCollection.java create mode 100644 web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentChildCollector.java create mode 100644 web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentRenderContext.java delete mode 100644 web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollection.java create mode 100644 web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollector.java create mode 100644 web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollectorClosure.java create mode 100644 web-views/src/main/java/groowt/view/web/runtime/WebViewComponentRenderContext.java delete mode 100644 web-views/src/main/java/groowt/view/web/runtime/WebViewComponentWriter.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/resolve/CachingComponentClassNodeResolver.java delete mode 100644 web-views/src/main/java/groowt/view/web/transpile/resolve/ClassIdentifier.java delete mode 100644 web-views/src/main/java/groowt/view/web/transpile/resolve/ClassIdentifierWithFqn.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/resolve/ClassLoaderComponentClassNodeResolver.java delete mode 100644 web-views/src/main/java/groowt/view/web/transpile/resolve/DefaultComponentClassNodeResolver.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/resolve/ModuleNodeComponentClassNodeResolver.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/resolve/ResolveUtil.java create mode 100644 web-views/src/main/java/groowt/view/web/util/LazyProvider.java create mode 100644 web-views/src/main/java/groowt/view/web/util/Property.java create mode 100644 web-views/src/main/java/groowt/view/web/util/Provider.java create mode 100644 web-views/src/main/java/groowt/view/web/util/SimpleProvider.java create mode 100644 web-views/src/sketching/groovy/groowt/view/web/sketching/Greeters.groovy rename web-views/src/test/groovy/groowt/view/web/{DefaultWebViewComponentTests.groovy => BaseWebViewComponentTests.groovy} (51%) create mode 100644 web-views/src/test/groovy/groowt/view/web/transpiler/resolve/ResolveUtilTests.java rename web-views/src/{main => test}/resources/log4j2.xml (95%) create mode 100644 web-views/src/tools/resources/log4j2.xml diff --git a/view-components/src/main/java/groowt/view/component/AbstractViewComponent.java b/view-components/src/main/java/groowt/view/component/AbstractViewComponent.java index e202d84..e53f4e5 100644 --- a/view-components/src/main/java/groowt/view/component/AbstractViewComponent.java +++ b/view-components/src/main/java/groowt/view/component/AbstractViewComponent.java @@ -1,10 +1,8 @@ package groowt.view.component; import groovy.lang.Closure; -import groowt.view.component.compiler.ComponentTemplateCompileErrorException; -import groowt.view.component.compiler.ComponentTemplateCompiler; +import groowt.view.component.compiler.*; import groowt.view.component.context.ComponentContext; -import groowt.view.component.factory.ComponentTemplateSource; import java.io.IOException; import java.io.Writer; @@ -13,44 +11,45 @@ import java.util.function.Function; public abstract class AbstractViewComponent implements ViewComponent { - private ComponentContext context; - private ComponentTemplate template; + private static final ComponentTemplateClassFactory templateClassFactory = new SimpleComponentTemplateClassFactory(); - public AbstractViewComponent() {} - - public AbstractViewComponent(ComponentTemplate template) { - this.template = Objects.requireNonNull(template); - } - - public AbstractViewComponent(Class templateClass) { + private static ComponentTemplate instantiateTemplate(Class templateClass) { try { - this.template = templateClass.getConstructor().newInstance(); + return templateClass.getConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException(e); } } - protected AbstractViewComponent(ComponentTemplateSource source, ComponentTemplateCompiler compiler) { - try { - this.template = compiler.compileAndGet(this.getSelfClass(), source); - } catch (ComponentTemplateCompileErrorException e) { - throw new RuntimeException(e); - } + private final ComponentTemplate template; + private ComponentContext context; + + public AbstractViewComponent() { + this.template = null; } - protected AbstractViewComponent( - ComponentTemplateSource source, - Function, ? extends ComponentTemplateCompiler> compilerFunction + public AbstractViewComponent(ComponentTemplate template) { + this.template = template; + } + + public AbstractViewComponent(Class templateClass) { + this.template = instantiateTemplate(templateClass); + } + + public AbstractViewComponent( + Function, ComponentTemplateCompileUnit> compileUnitFunction ) { - final var compiler = compilerFunction.apply(this.getSelfClass()); + final ComponentTemplateCompileResult compileResult; try { - this.template = compiler.compileAndGet(this.getSelfClass(), source); - } catch (ComponentTemplateCompileErrorException e) { + compileResult = compileUnitFunction.apply(this.getClass()).compile(); + } catch (ComponentTemplateCompileException e) { throw new RuntimeException(e); } + final var templateClass = templateClassFactory.getTemplateClass(compileResult); + this.template = instantiateTemplate(templateClass); } - protected abstract Class getSelfClass(); + @Override public void setContext(ComponentContext context) { @@ -66,26 +65,20 @@ public abstract class AbstractViewComponent implements ViewComponent { return Objects.requireNonNull(template); } - protected void setTemplate(ComponentTemplate template) { - this.template = Objects.requireNonNull(template); - } + protected void beforeRender() {} - protected void beforeRender() { - this.getContext().beforeComponentRender(this); - } - - protected void afterRender() { - this.getContext().afterComponentRender(this); - } + protected void afterRender() {} /** * @implSpec If overriding, please call - * {@link #beforeRender()}and {@link #afterRender()} before - * and after the actual rendering is done, respectively. + * {@link #beforeRender()} and {@link #afterRender()} before + * and after the actual rendering is done, respectively; + * this way, components can still do their before/after + * logic even if this method is overwritten. */ @Override public void renderTo(Writer out) throws IOException { - final Closure closure = this.template.getRenderer(); + final Closure closure = this.getTemplate().getRenderer(); closure.setDelegate(this); closure.setResolveStrategy(Closure.DELEGATE_FIRST); this.beforeRender(); diff --git a/view-components/src/main/java/groowt/view/component/ViewComponent.java b/view-components/src/main/java/groowt/view/component/ViewComponent.java index bdcda41..713a2fc 100644 --- a/view-components/src/main/java/groowt/view/component/ViewComponent.java +++ b/view-components/src/main/java/groowt/view/component/ViewComponent.java @@ -5,10 +5,6 @@ import groowt.view.component.context.ComponentContext; public interface ViewComponent extends View { - default String getTypeName() { - return this.getClass().getName(); - } - /** * Note: compiled templates are required to automatically * call this method after the component is constructed. One diff --git a/view-components/src/main/java/groowt/view/component/ViewComponentBugError.java b/view-components/src/main/java/groowt/view/component/ViewComponentBugError.java new file mode 100644 index 0000000..61fa95a --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/ViewComponentBugError.java @@ -0,0 +1,31 @@ +package groowt.view.component; + +public class ViewComponentBugError extends RuntimeException { + + public ViewComponentBugError(String message) { + super(message); + } + + public ViewComponentBugError(String message, Throwable cause) { + super(message, cause); + } + + public ViewComponentBugError(Throwable cause) { + super(cause); + } + + public ViewComponentBugError( + String message, + Throwable cause, + boolean enableSuppression, + boolean writableStackTrace + ) { + super(message, cause, enableSuppression, writableStackTrace); + } + + @Override + public String getMessage() { + return "BUG! Please file an issue report at the github repository. " + super.getMessage(); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/AbstractComponentTemplateCompileUnit.java b/view-components/src/main/java/groowt/view/component/compiler/AbstractComponentTemplateCompileUnit.java new file mode 100644 index 0000000..6db1fc4 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/AbstractComponentTemplateCompileUnit.java @@ -0,0 +1,30 @@ +package groowt.view.component.compiler; + +import groowt.view.component.ViewComponent; +import groowt.view.component.compiler.source.ComponentTemplateSource; + +public abstract class AbstractComponentTemplateCompileUnit implements + ComponentTemplateCompileUnit { + + private final Class forClass; + private final ComponentTemplateSource source; + + public AbstractComponentTemplateCompileUnit( + Class forClass, + ComponentTemplateSource source + ) { + this.forClass = forClass; + this.source = source; + } + + @Override + public Class getForClass() { + return this.forClass; + } + + @Override + public ComponentTemplateSource getSource() { + return this.source; + } + +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/AbstractComponentTemplateCompiler.java b/view-components/src/main/java/groowt/view/component/compiler/AbstractComponentTemplateCompiler.java deleted file mode 100644 index 91ab045..0000000 --- a/view-components/src/main/java/groowt/view/component/compiler/AbstractComponentTemplateCompiler.java +++ /dev/null @@ -1,41 +0,0 @@ -package groowt.view.component.compiler; - -import groovy.lang.GroovyClassLoader; -import groowt.view.component.ComponentTemplate; -import groowt.view.component.ViewComponent; -import groowt.view.component.factory.ComponentTemplateSource; - -import java.io.IOException; -import java.io.Reader; - -public abstract class AbstractComponentTemplateCompiler implements ComponentTemplateCompiler { - - private final GroovyClassLoader groovyClassLoader; - - public AbstractComponentTemplateCompiler(GroovyClassLoader groovyClassLoader) { - this.groovyClassLoader = groovyClassLoader; - } - - protected abstract ComponentTemplateCompileResult compile( - ComponentTemplateSource componentTemplateSource, - Class forClass, - Reader actualSource - ) throws ComponentTemplateCompileErrorException; - - @Override - public ComponentTemplateCompileResult compile(Class forClass, ComponentTemplateSource source) - throws ComponentTemplateCompileErrorException { - try (final Reader reader = ComponentTemplateSource.toReader(source)) { - return this.compile(source, forClass, reader); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public ComponentTemplate compileAndGet(Class forClass, ComponentTemplateSource source) - throws ComponentTemplateCompileErrorException { - return this.compileAndGet(this.groovyClassLoader, forClass, source); - } - -} diff --git a/view-components/src/main/java/groowt/view/component/compiler/CachingComponentTemplateCompiler.java b/view-components/src/main/java/groowt/view/component/compiler/CachingComponentTemplateCompiler.java index e4ae34b..b59def4 100644 --- a/view-components/src/main/java/groowt/view/component/compiler/CachingComponentTemplateCompiler.java +++ b/view-components/src/main/java/groowt/view/component/compiler/CachingComponentTemplateCompiler.java @@ -1,104 +1,59 @@ package groowt.view.component.compiler; -import groovy.lang.GroovyClassLoader; -import groowt.view.component.ComponentTemplate; import groowt.view.component.ViewComponent; -import groowt.view.component.factory.ComponentTemplateSource; -import org.codehaus.groovy.tools.GroovyClass; -import org.jetbrains.annotations.Nullable; -import java.io.Reader; import java.util.HashMap; import java.util.Map; -public abstract class CachingComponentTemplateCompiler extends AbstractComponentTemplateCompiler { +public abstract class CachingComponentTemplateCompiler + implements ComponentTemplateCompiler { - private record CachedTemplate( - ComponentTemplateCompileResult compileResult, - @Nullable ComponentTemplate template - ) {} + private final Map, ComponentTemplateCompileResult> cache = new HashMap<>(); - private final Map, CachedTemplate> cache = new HashMap<>(); - - public CachingComponentTemplateCompiler(GroovyClassLoader groovyClassLoader) { - super(groovyClassLoader); - } - - private ComponentTemplate instantiate( - GroovyClassLoader groovyClassLoader, - ComponentTemplateCompileResult compileResult - ) { - for (final var groovyClass : compileResult.otherClasses()) { - // Try to find it. If we can't, we need to load it via the groovy loader - try { - Class.forName(groovyClass.getName(), true, groovyClassLoader); - } catch (ClassNotFoundException ignored) { - groovyClassLoader.defineClass(groovyClass.getName(), groovyClass.getBytes()); - } catch (LinkageError ignored) { - // no-op, because we already have it - } - } - final GroovyClass templateGroovyClass = compileResult.templateClass(); - Class templateClass; - // Try to find it. If we can't, we need to load it via the groovy loader - try { - templateClass = Class.forName(templateGroovyClass.getName(), true, groovyClassLoader); - } catch (ClassNotFoundException ignored) { - templateClass = groovyClassLoader.defineClass( - templateGroovyClass.getName(), - templateGroovyClass.getBytes() - ); - } - try { - return (ComponentTemplate) templateClass.getConstructor().newInstance(); - } catch (Exception e) { - throw new RuntimeException("Unable to instantiate ComponentTemplate " + templateClass.getName(), e); - } - } +// private ComponentTemplate instantiate( +// GroovyClassLoader groovyClassLoader, +// CompileResult compileResult +// ) { +// for (final var groovyClass : compileResult.otherClasses()) { +// // Try to find it. If we can't, we need to load it via the groovy loader +// try { +// Class.forName(groovyClass.getName(), true, groovyClassLoader); +// } catch (ClassNotFoundException ignored) { +// groovyClassLoader.defineClass(groovyClass.getName(), groovyClass.getBytes()); +// } catch (LinkageError ignored) { +// // no-op, because we already have it +// } +// } +// final GroovyClass templateGroovyClass = compileResult.templateClass(); +// Class templateClass; +// // Try to find it. If we can't, we need to load it via the groovy loader +// try { +// templateClass = Class.forName(templateGroovyClass.getName(), true, groovyClassLoader); +// } catch (ClassNotFoundException ignored) { +// templateClass = groovyClassLoader.defineClass( +// templateGroovyClass.getName(), +// templateGroovyClass.getBytes() +// ); +// } +// try { +// return (ComponentTemplate) templateClass.getConstructor().newInstance(); +// } catch (Exception e) { +// throw new RuntimeException("Unable to instantiate ComponentTemplate " + templateClass.getName(), e); +// } +// } @Override - public final ComponentTemplate compileAndGet( - GroovyClassLoader groovyClassLoader, - Class forClass, - ComponentTemplateSource source - ) throws ComponentTemplateCompileErrorException { - if (this.cache.containsKey(forClass)) { - final var cached = this.cache.get(forClass); - if (cached.template() == null) { - final ComponentTemplate template = this.instantiate(groovyClassLoader, cached.compileResult()); - this.cache.put(forClass, new CachedTemplate(cached.compileResult(), template)); - return template; - } else { - return cached.template(); - } + public final ComponentTemplateCompileResult compile(U compileUnit) + throws ComponentTemplateCompileException { + if (this.cache.containsKey(compileUnit.getForClass())) { + return this.cache.get(compileUnit.getForClass()); } else { - final ComponentTemplateCompileResult compileResult = this.compile(forClass, source); - final ComponentTemplate template = this.instantiate(groovyClassLoader, compileResult); - this.cache.put(forClass, new CachedTemplate(compileResult, template)); - return template; - } - } - - @Override - protected final ComponentTemplateCompileResult compile( - ComponentTemplateSource componentTemplateSource, - Class forClass, - Reader actualSource - ) throws ComponentTemplateCompileErrorException { - if (this.cache.containsKey(forClass)) { - return this.cache.get(forClass).compileResult(); - } else { - final ComponentTemplateCompileResult compileResult = - this.doCompile(componentTemplateSource, forClass, actualSource); - this.cache.put(forClass, new CachedTemplate(compileResult, null)); + final ComponentTemplateCompileResult compileResult = this.doCompile(compileUnit); + this.cache.put(compileUnit.getForClass(), compileResult); return compileResult; } } - protected abstract ComponentTemplateCompileResult doCompile( - ComponentTemplateSource source, - Class forClass, - Reader reader - ) throws ComponentTemplateCompileErrorException; + protected abstract ComponentTemplateCompileResult doCompile(U compileUnit) throws ComponentTemplateCompileException; } diff --git a/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateClassFactory.java b/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateClassFactory.java new file mode 100644 index 0000000..ecc9d07 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateClassFactory.java @@ -0,0 +1,7 @@ +package groowt.view.component.compiler; + +import groowt.view.component.ComponentTemplate; + +public interface ComponentTemplateClassFactory { + Class getTemplateClass(ComponentTemplateCompileResult compileResult); +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileErrorException.java b/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileErrorException.java deleted file mode 100644 index 70e5ca0..0000000 --- a/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileErrorException.java +++ /dev/null @@ -1,61 +0,0 @@ -package groowt.view.component.compiler; - -import groowt.view.component.ViewComponent; - -/** - * Represents an exception thrown while attempting to instantiate a ComponentTemplate during compilation. - */ -public class ComponentTemplateCompileErrorException extends Exception { - - private final Class forClass; - private final Object templateSource; - - public ComponentTemplateCompileErrorException( - String message, - Class forClass, - Object templateSource - ) { - super(message); - this.forClass = forClass; - this.templateSource = templateSource; - } - - public ComponentTemplateCompileErrorException( - String message, - Throwable cause, - Class forClass, - Object templateSource - ) { - super(message, cause); - this.forClass = forClass; - this.templateSource = templateSource; - } - - public ComponentTemplateCompileErrorException( - Throwable cause, - Class forClass, - Object templateSource - ) { - super(cause); - this.forClass = forClass; - this.templateSource = templateSource; - } - - public ComponentTemplateCompileErrorException( - Class forClass, - Object templateSource - ) { - super("Compile error in " + templateSource + " for " + forClass.getName()); - this.forClass = forClass; - this.templateSource = templateSource; - } - - public Class getForClass() { - return this.forClass; - } - - public Object getTemplateSource() { - return this.templateSource; - } - -} diff --git a/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileException.java b/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileException.java new file mode 100644 index 0000000..d5e1d4e --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileException.java @@ -0,0 +1,37 @@ +package groowt.view.component.compiler; + +import org.jetbrains.annotations.Nullable; + +public class ComponentTemplateCompileException extends Exception { + + private final ComponentTemplateCompileUnit compileUnit; + + public ComponentTemplateCompileException(ComponentTemplateCompileUnit compileUnit, String message) { + super(message); + this.compileUnit = compileUnit; + } + + public ComponentTemplateCompileException( + ComponentTemplateCompileUnit compileUnit, + String message, + Throwable cause + ) { + super(message, cause); + this.compileUnit = compileUnit; + } + + @Override + public String getMessage() { + final var sb = new StringBuilder("Error in ").append(compileUnit.getSource().getDescription()); + final @Nullable String position = this.getPosition(); + if (position != null) { + sb.append(" at ").append(position); + } + return sb.append(": ").append(super.getMessage()).toString(); + } + + protected @Nullable String getPosition() { + return null; + } + +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileResult.java b/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileResult.java new file mode 100644 index 0000000..da3b003 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileResult.java @@ -0,0 +1,10 @@ +package groowt.view.component.compiler; + +import org.codehaus.groovy.tools.GroovyClass; + +import java.util.Set; + +public interface ComponentTemplateCompileResult { + GroovyClass getTemplateClass(); + Set getOtherClasses(); +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileUnit.java b/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileUnit.java new file mode 100644 index 0000000..4dc8bec --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompileUnit.java @@ -0,0 +1,18 @@ +package groowt.view.component.compiler; + +import groowt.view.component.ViewComponent; +import groowt.view.component.compiler.source.ComponentTemplateSource; + +public interface ComponentTemplateCompileUnit { + + Class getForClass(); + String getDefaultPackageName(); + ComponentTemplateSource getSource(); + ComponentTemplateCompileResult compile(ComponentTemplateCompilerConfiguration configuration) + throws ComponentTemplateCompileException; + + default ComponentTemplateCompileResult compile() throws ComponentTemplateCompileException { + return this.compile(new DefaultComponentTemplateCompilerConfiguration()); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompiler.java b/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompiler.java index 842bc62..d6a5a64 100644 --- a/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompiler.java +++ b/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompiler.java @@ -1,34 +1,7 @@ package groowt.view.component.compiler; -import groovy.lang.GroovyClassLoader; -import groowt.view.component.ComponentTemplate; -import groowt.view.component.ViewComponent; -import groowt.view.component.factory.ComponentTemplateSource; -import org.codehaus.groovy.tools.GroovyClass; +public interface ComponentTemplateCompiler { -import java.io.IOException; -import java.util.List; - -public interface ComponentTemplateCompiler { - - record ComponentTemplateCompileResult(GroovyClass templateClass, List otherClasses) {} - - ComponentTemplateCompileResult compile(Class forClass, ComponentTemplateSource source) - throws ComponentTemplateCompileErrorException; - - ComponentTemplate compileAndGet( - GroovyClassLoader groovyClassLoader, - Class forClass, - ComponentTemplateSource source - ) throws ComponentTemplateCompileErrorException; - - default ComponentTemplate compileAndGet(Class forClass, ComponentTemplateSource source) - throws ComponentTemplateCompileErrorException { - try (final GroovyClassLoader groovyClassLoader = new GroovyClassLoader(this.getClass().getClassLoader())) { - return this.compileAndGet(groovyClassLoader, forClass, source); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } - } + ComponentTemplateCompileResult compile(U compileUnit) throws ComponentTemplateCompileException; } diff --git a/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompilerConfiguration.java b/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompilerConfiguration.java new file mode 100644 index 0000000..0149a9a --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/ComponentTemplateCompilerConfiguration.java @@ -0,0 +1,11 @@ +package groowt.view.component.compiler; + +import groovy.lang.GroovyClassLoader; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.CompilerConfiguration; + +public interface ComponentTemplateCompilerConfiguration { + GroovyClassLoader getGroovyClassLoader(); + CompilerConfiguration getGroovyCompilerConfiguration(); + CompilePhase getToCompilePhase(); +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/DefaultComponentTemplateCompilerConfiguration.java b/view-components/src/main/java/groowt/view/component/compiler/DefaultComponentTemplateCompilerConfiguration.java new file mode 100644 index 0000000..2a02ed9 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/DefaultComponentTemplateCompilerConfiguration.java @@ -0,0 +1,44 @@ +package groowt.view.component.compiler; + +import groovy.lang.GroovyClassLoader; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.CompilerConfiguration; + +public class DefaultComponentTemplateCompilerConfiguration implements ComponentTemplateCompilerConfiguration { + + private GroovyClassLoader groovyClassLoader; + private CompilerConfiguration groovyCompilerConfiguration; + private CompilePhase toCompilePhase; + + @Override + public GroovyClassLoader getGroovyClassLoader() { + return this.groovyClassLoader != null + ? this.groovyClassLoader + : new GroovyClassLoader(this.getClass().getClassLoader()); + } + + public void setGroovyClassLoader(GroovyClassLoader groovyClassLoader) { + this.groovyClassLoader = groovyClassLoader; + } + + @Override + public CompilerConfiguration getGroovyCompilerConfiguration() { + return this.groovyCompilerConfiguration != null + ? this.groovyCompilerConfiguration + : CompilerConfiguration.DEFAULT; + } + + public void setGroovyCompilerConfiguration(CompilerConfiguration groovyCompilerConfiguration) { + this.groovyCompilerConfiguration = groovyCompilerConfiguration; + } + + @Override + public CompilePhase getToCompilePhase() { + return this.toCompilePhase != null ? this.toCompilePhase : CompilePhase.CLASS_GENERATION; + } + + public void setToCompilePhase(CompilePhase toCompilePhase) { + this.toCompilePhase = toCompilePhase; + } + +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/SimpleComponentTemplateClassFactory.java b/view-components/src/main/java/groowt/view/component/compiler/SimpleComponentTemplateClassFactory.java new file mode 100644 index 0000000..d5d22af --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/SimpleComponentTemplateClassFactory.java @@ -0,0 +1,108 @@ +package groowt.view.component.compiler; + +import groowt.view.component.ComponentTemplate; +import org.codehaus.groovy.tools.GroovyClass; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import static java.nio.file.StandardOpenOption.CREATE_NEW; +import static java.nio.file.StandardOpenOption.WRITE; + +public final class SimpleComponentTemplateClassFactory implements ComponentTemplateClassFactory { + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private static String[] classNameToPackageDirParts(String fullClassName) { + final String[] allParts = fullClassName.split("\\."); + if (allParts.length == 0) { + throw new RuntimeException("Did not expect allParts.length to be zero."); + } else if (allParts.length == 1) { + return EMPTY_STRING_ARRAY; + } else { + final var result = new String[allParts.length - 1]; + System.arraycopy(allParts, 0, result, 0, allParts.length - 1); + return result; + } + } + + private static Path resolvePackageDir(Path rootDir, String[] packageDirParts) { + return Path.of(rootDir.toString(), packageDirParts); + } + + private static String isolateClassName(String fullClassName) { + final String[] parts = fullClassName.split("\\."); + if (parts.length == 0) { + throw new RuntimeException("Did not expect parts.length to be zero"); + } + return parts[parts.length - 1]; + } + + private final Map> cache = new HashMap<>(); + private final ClassLoader classLoader; + private final Path tempClassesDir; + + public SimpleComponentTemplateClassFactory() { + try { + this.tempClassesDir = Files.createTempDirectory("view-component-classes-"); + } catch (IOException e) { + throw new RuntimeException(e); + } + try { + this.classLoader = new URLClassLoader( + "SimpleComponentTemplateClassFactoryClassLoader", + new URL[] { this.tempClassesDir.toUri().toURL() }, + this.getClass().getClassLoader() + ); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + private void writeClassToDisk(GroovyClass groovyClass) { + final var className = groovyClass.getName(); + final var packageDirParts = classNameToPackageDirParts(className); + final var packageDir = resolvePackageDir(this.tempClassesDir, packageDirParts); + try { + Files.createDirectories(packageDir); + } catch (IOException e) { + throw new RuntimeException(e); + } + final var classFile = Path.of(packageDir.toString(), isolateClassName(className) + ".class"); + try { + Files.write(classFile, groovyClass.getBytes(), CREATE_NEW, WRITE); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public Class getTemplateClass(ComponentTemplateCompileResult compileResult) { + final String templateClassName = compileResult.getTemplateClass().getName(); + if (this.cache.containsKey(templateClassName)) { + return this.cache.get(templateClassName); + } else { + // write classes to disk + this.writeClassToDisk(compileResult.getTemplateClass()); + compileResult.getOtherClasses().forEach(this::writeClassToDisk); + // load the template class + try { + //noinspection unchecked + final var templateClass = (Class) this.classLoader.loadClass( + templateClassName + ); + this.cache.put(templateClassName, templateClass); + return templateClass; + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/SimpleComponentTemplateCompileResult.java b/view-components/src/main/java/groowt/view/component/compiler/SimpleComponentTemplateCompileResult.java new file mode 100644 index 0000000..ca78f8d --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/SimpleComponentTemplateCompileResult.java @@ -0,0 +1,50 @@ +package groowt.view.component.compiler; + +import org.codehaus.groovy.tools.GroovyClass; + +import java.util.HashSet; +import java.util.Set; + +public class SimpleComponentTemplateCompileResult implements ComponentTemplateCompileResult { + + private final GroovyClass templateClass; + private final Set otherClasses; + + public SimpleComponentTemplateCompileResult( + GroovyClass templateClass, + Set otherClasses + ) { + this.templateClass = templateClass; + this.otherClasses = otherClasses; + } + + @Override + public GroovyClass getTemplateClass() { + return this.templateClass; + } + + @Override + public Set getOtherClasses() { + return new HashSet<>(this.otherClasses); + } + + @Override + public int hashCode() { + return this.templateClass.getName().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj instanceof ComponentTemplateCompileResult other) { + return this.templateClass.getName().equals(other.getTemplateClass().getName()); + } + return false; + } + + @Override + public String toString() { + return "sctCompileResult(" + this.templateClass.getName() + ")"; + } + +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/source/ComponentTemplateSource.java b/view-components/src/main/java/groowt/view/component/compiler/source/ComponentTemplateSource.java new file mode 100644 index 0000000..bdee155 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/source/ComponentTemplateSource.java @@ -0,0 +1,53 @@ +package groowt.view.component.compiler.source; + +import java.io.File; +import java.io.InputStream; +import java.io.Reader; +import java.net.URI; +import java.net.URL; +import java.util.List; + +public interface ComponentTemplateSource { + + static ComponentTemplateSource of(String template) { + return new StringSource(template, null); + } + + static ComponentTemplateSource of(String template, String name) { + return new StringSource(template, name); + } + + static ComponentTemplateSource of(File templateFile) { + return new FileSource(templateFile); + } + + static ComponentTemplateSource of(URI templateURI) { + return new URISource(templateURI); + } + + static ComponentTemplateSource of(URL url) { + return new URLSource(url); + } + + static ComponentTemplateSource of(InputStream templateInputStream) { + return new InputStreamSource(templateInputStream, null); + } + + static ComponentTemplateSource of(InputStream templateInputStream, String description) { + return new InputStreamSource(templateInputStream, description); + } + + static ComponentTemplateSource of(Reader templateReader) { + return new ReaderSource(templateReader, null); + } + + static ComponentTemplateSource of(Reader templateReader, String description) { + return new ReaderSource(templateReader, description); + } + + Reader toReader() throws Exception; + String getDescription(); + boolean canReopen(); + List getLines(); + +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/source/FileSource.java b/view-components/src/main/java/groowt/view/component/compiler/source/FileSource.java new file mode 100644 index 0000000..8e177bb --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/source/FileSource.java @@ -0,0 +1,50 @@ +package groowt.view.component.compiler.source; + +import org.jetbrains.annotations.Nullable; + +import java.io.*; +import java.net.URI; +import java.util.List; + +public class FileSource implements ComponentTemplateSource { + + private final File templateFile; + private List lines; + + public FileSource(File templateFile) { + this.templateFile = templateFile; + } + + @Override + public Reader toReader() throws Exception { + return new FileReader(this.templateFile); + } + + @Override + public String getDescription() { + return this.templateFile.toString(); + } + + @Override + public boolean canReopen() { + return true; + } + + @Override + public List getLines() { + if (this.lines == null) { + try (final var fis = new FileInputStream(this.templateFile)) { + final var allSource = new String(fis.readAllBytes()); + this.lines = allSource.lines().toList(); + } catch (IOException ioException) { + throw new RuntimeException(ioException); + } + } + return this.lines; + } + + public @Nullable URI getURI() { + return templateFile.toURI(); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/source/InputStreamSource.java b/view-components/src/main/java/groowt/view/component/compiler/source/InputStreamSource.java new file mode 100644 index 0000000..6c8dcef --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/source/InputStreamSource.java @@ -0,0 +1,40 @@ +package groowt.view.component.compiler.source; + +import org.jetbrains.annotations.Nullable; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.List; + +public class InputStreamSource implements ComponentTemplateSource { + + private final InputStream templateInputStream; + private final @Nullable String description; + + public InputStreamSource(InputStream templateInputStream, @Nullable String description) { + this.templateInputStream = templateInputStream; + this.description = description; + } + + @Override + public Reader toReader() { + return new InputStreamReader(this.templateInputStream); + } + + @Override + public String getDescription() { + return this.description != null ? this.description : ""; + } + + @Override + public boolean canReopen() { + return false; + } + + @Override + public List getLines() { + throw new UnsupportedOperationException(); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/source/ReaderSource.java b/view-components/src/main/java/groowt/view/component/compiler/source/ReaderSource.java new file mode 100644 index 0000000..18b6adc --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/source/ReaderSource.java @@ -0,0 +1,38 @@ +package groowt.view.component.compiler.source; + +import org.jetbrains.annotations.Nullable; + +import java.io.Reader; +import java.util.List; + +public class ReaderSource implements ComponentTemplateSource { + + private final Reader reader; + private final @Nullable String description; + + public ReaderSource(Reader reader, @Nullable String description) { + this.reader = reader; + this.description = description; + } + + @Override + public Reader toReader() { + return this.reader; + } + + @Override + public String getDescription() { + return this.description != null ? this.description : ""; + } + + @Override + public boolean canReopen() { + return false; + } + + @Override + public List getLines() { + throw new UnsupportedOperationException(); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/source/StringSource.java b/view-components/src/main/java/groowt/view/component/compiler/source/StringSource.java new file mode 100644 index 0000000..0063aa1 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/source/StringSource.java @@ -0,0 +1,39 @@ +package groowt.view.component.compiler.source; + +import org.jetbrains.annotations.Nullable; + +import java.io.Reader; +import java.io.StringReader; +import java.util.List; + +public class StringSource implements ComponentTemplateSource { + + private final String template; + private final @Nullable String name; + + public StringSource(String template, @Nullable String name) { + this.template = template; + this.name = name; + } + + @Override + public Reader toReader() { + return new StringReader(this.template); + } + + @Override + public String getDescription() { + return this.name != null ? this.name : ""; + } + + @Override + public boolean canReopen() { + return true; + } + + @Override + public List getLines() { + return this.template.lines().toList(); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/source/URISource.java b/view-components/src/main/java/groowt/view/component/compiler/source/URISource.java new file mode 100644 index 0000000..ce9bb46 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/source/URISource.java @@ -0,0 +1,50 @@ +package groowt.view.component.compiler.source; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URI; +import java.util.List; + +public class URISource implements ComponentTemplateSource { + + private final URI templateURI; + private List lines; + + public URISource(URI templateURI) { + this.templateURI = templateURI; + } + + @Override + public Reader toReader() throws Exception { + return new InputStreamReader(this.templateURI.toURL().openStream()); + } + + @Override + public String getDescription() { + return this.templateURI.toString(); + } + + @Override + public boolean canReopen() { + return true; + } + + public URI getURI() { + return this.templateURI; + } + + @Override + public List getLines() { + if (this.lines == null) { + try (final var inputStream = this.templateURI.toURL().openStream()) { + final String allSource = new String(inputStream.readAllBytes()); + this.lines = allSource.lines().toList(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return this.lines; + } + +} diff --git a/view-components/src/main/java/groowt/view/component/compiler/source/URLSource.java b/view-components/src/main/java/groowt/view/component/compiler/source/URLSource.java new file mode 100644 index 0000000..6c31fe8 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/compiler/source/URLSource.java @@ -0,0 +1,58 @@ +package groowt.view.component.compiler.source; + +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.List; + +public class URLSource implements ComponentTemplateSource { + + private final URL url; + private List lines; + + public URLSource(URL url) { + this.url = url; + } + + @Override + public Reader toReader() throws Exception { + return new InputStreamReader(this.url.openStream()); + } + + @Override + public String getDescription() { + return this.url.toString(); + } + + @Override + public boolean canReopen() { + return true; + } + + public @Nullable URI getURI() { + try { + return this.url.toURI(); + } catch (URISyntaxException e) { + return null; + } + } + + @Override + public List getLines() { + if (this.lines == null) { + try (final var inputStream = this.url.openStream()) { + final String allSource = new String(inputStream.readAllBytes()); + this.lines = allSource.lines().toList(); + } catch (IOException ioException) { + throw new RuntimeException(ioException); + } + } + return this.lines; + } + +} diff --git a/view-components/src/main/java/groowt/view/component/context/ComponentContext.java b/view-components/src/main/java/groowt/view/component/context/ComponentContext.java index 30c0ff6..172f729 100644 --- a/view-components/src/main/java/groowt/view/component/context/ComponentContext.java +++ b/view-components/src/main/java/groowt/view/component/context/ComponentContext.java @@ -1,62 +1,28 @@ package groowt.view.component.context; import groowt.view.component.ViewComponent; -import groowt.view.component.factory.ComponentFactory; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; -import java.util.Deque; import java.util.List; -import java.util.Objects; import java.util.function.Predicate; public interface ComponentContext { - /** - * For use only by compiled templates. - */ - @ApiStatus.Internal - interface Resolved { - String getTypeName(); - ComponentFactory getComponentFactory(); - } - - /** - * For use only by compiled templates. - */ - @ApiStatus.Internal - Resolved resolve(String component); - - /** - * For use only by compiled templates. - */ - @ApiStatus.Internal - ViewComponent create(Resolved resolved, Object... args); - - /** - * For use only by compiled templates. - */ - @ApiStatus.Internal - void beforeComponentRender(ViewComponent component); - - /** - * For use only by compiled templates. - */ - @ApiStatus.Internal - void afterComponentRender(ViewComponent component); - - Deque getScopeStack(); + List getScopeStack(); void pushScope(ComponentScope scope); void pushDefaultScope(); void popScope(); + ComponentScope getRootScope(); default ComponentScope getCurrentScope() { - return Objects.requireNonNull(this.getScopeStack().peek(), "There is no current scope."); + final List scopeStack = this.getScopeStack(); + if (scopeStack.isEmpty()) { + throw new NullPointerException("There is no current scope."); + } + return scopeStack.getFirst(); } - Deque getComponentStack(); - @Nullable ViewComponent getParent(); @Nullable T getParent(Class parentClass); @@ -66,11 +32,7 @@ public interface ComponentContext { Class ancestorClass, Predicate matching ) { - return ancestorClass.cast(matching.and(ancestorClass::isInstance)); - } - - default @Nullable ViewComponent findNearestAncestorByTypeName(String typeName) { - return this.findNearestAncestor(component -> component.getTypeName().equals(typeName)); + return ancestorClass.cast(this.findNearestAncestor(matching.and(ancestorClass::isInstance))); } List getAllAncestors(); diff --git a/view-components/src/main/java/groowt/view/component/context/ComponentCreateException.java b/view-components/src/main/java/groowt/view/component/context/ComponentCreateException.java deleted file mode 100644 index 41a864c..0000000 --- a/view-components/src/main/java/groowt/view/component/context/ComponentCreateException.java +++ /dev/null @@ -1,37 +0,0 @@ -package groowt.view.component.context; - -import groowt.view.component.ComponentTemplate; - -/** - * An exception which signals that a component of the given type - * could not be created in the given template. - */ -public class ComponentCreateException extends RuntimeException { - - private final Object componentType; - private final ComponentTemplate template; - private final int line; - private final int column; - - public ComponentCreateException( - Object componentType, - ComponentTemplate template, - int line, - int column, - Throwable cause - ) { - super(cause); - this.componentType = componentType; - this.template = template; - this.line = line; - this.column = column; - } - - @Override - public String getMessage() { - return "Exception in " + this.template.getClass().getName() + " while creating " - + this.componentType.getClass().getName() + " at line " + this.line - + ", column " + this.column + "."; - } - -} diff --git a/view-components/src/main/java/groowt/view/component/context/ComponentResolveException.java b/view-components/src/main/java/groowt/view/component/context/ComponentResolveException.java new file mode 100644 index 0000000..a20bbfc --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/context/ComponentResolveException.java @@ -0,0 +1,141 @@ +package groowt.view.component.context; + +import groowt.view.component.ComponentTemplate; +import groowt.view.component.ViewComponent; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public class ComponentResolveException extends Exception { + + private final String typeNameOrAlias; + private @Nullable String message; + private @Nullable Class type; + private @Nullable ComponentTemplate template; + private int line; + private int column; + + public ComponentResolveException(String typeName) { + this.typeNameOrAlias = Objects.requireNonNull(typeName); + } + + public ComponentResolveException(String typeName, @Nullable Class type) { + this.typeNameOrAlias = Objects.requireNonNull(typeName); + this.type = type; + } + + public ComponentResolveException( + @Nullable String message, + String typeName, + @Nullable Class type + ) { + this.typeNameOrAlias = typeName; + this.message = message; + this.type = type; + } + + public ComponentResolveException(String typeName, Throwable cause) { + super(cause); + this.typeNameOrAlias = Objects.requireNonNull(typeName); + } + + public ComponentResolveException(String typeName, @Nullable Class type, Throwable cause) { + super(cause); + this.typeNameOrAlias = Objects.requireNonNull(typeName); + this.type = type; + } + + public ComponentResolveException( + @NotNull ComponentTemplate template, + Throwable cause, + String typeName, + int line, + int column + ) { + super(cause); + this.template = template; + this.typeNameOrAlias = typeName; + this.line = line; + this.column = column; + } + + public ComponentResolveException( + @NotNull ComponentTemplate template, + Throwable cause, + String alias, + @NotNull Class type, + int line, + int column + ) { + super(cause); + this.template = Objects.requireNonNull(template); + this.typeNameOrAlias = alias; + this.type = type; + this.line = line; + this.column = column; + } + + public void setMessage(@Nullable String message) { + this.message = message; + } + + public String getTypeNameOrAlias() { + return this.typeNameOrAlias; + } + + public @Nullable Class getType() { + return this.type; + } + + @ApiStatus.Internal + public void setType(@Nullable Class type) { + this.type = type; + } + + public @NotNull ComponentTemplate getTemplate() { + return Objects.requireNonNull(this.template); + } + + @ApiStatus.Internal + public void setTemplate(ComponentTemplate template) { + this.template = Objects.requireNonNull(template); + } + + public int getLine() { + return this.line; + } + + @ApiStatus.Internal + public void setLine(int line) { + this.line = line; + } + + public int getColumn() { + return this.column; + } + + @ApiStatus.Internal + public void setColumn(int column) { + this.column = column; + } + + @Override + public String getMessage() { + final var sb = new StringBuilder("Exception"); + if (this.template != null) { + sb.append(" in ").append(this.template.getClass().getName()); + } + sb.append(" while resolving component ").append(this.typeNameOrAlias); + if (this.type != null) { + sb.append(" of type ").append(this.type.getName()); + } + sb.append(" at line ").append(this.line).append(", column ").append(this.column).append("."); + if (this.message != null) { + sb.append(" ").append(this.message); + } // else, assume caused by is not null + return sb.toString(); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/context/ComponentScope.java b/view-components/src/main/java/groowt/view/component/context/ComponentScope.java index bcba8ab..6d2ee35 100644 --- a/view-components/src/main/java/groowt/view/component/context/ComponentScope.java +++ b/view-components/src/main/java/groowt/view/component/context/ComponentScope.java @@ -1,48 +1,52 @@ package groowt.view.component.context; -import groovy.lang.Closure; import groowt.view.component.ViewComponent; import groowt.view.component.factory.ComponentFactory; public interface ComponentScope { - void add(String name, ComponentFactory factory); - boolean contains(String name); - void remove(String name); - ComponentFactory get(String name); + record TypeAndFactory(Class type, ComponentFactory factory) {} - default ComponentFactory factoryMissing(String typeName) { - throw new NoFactoryMissingException(this.getClass().getName() + " does not support factoryMissing()"); + //---- string types + + void add(String typeName, Class forClass, ComponentFactory factory); + + boolean contains(String typeName); + + void remove(String typeName); + + TypeAndFactory get(String typeName); + + default TypeAndFactory factoryMissing(String typeName) throws ComponentResolveException { + throw new FactoryMissingUnsupportedException( + this.getClass().getName() + " does not support factoryMissing() for string types.", + typeName + ); } - default void add(Class clazz, ComponentFactory factory) { - this.add(clazz.getName(), factory); - } + //---- class types - default boolean contains(Class clazz) { - return this.contains(clazz.getName()); - } + void add(Class forClass, ComponentFactory factory); - @SuppressWarnings("unchecked") - default ComponentFactory get(Class clazz) { - return (ComponentFactory) this.get(clazz.getName()); - } + void add( + Class publicType, + Class implementingType, + ComponentFactory factory + ); - default void remove(Class clazz) { - this.remove(clazz.getName()); - } + boolean contains(Class type); - @SuppressWarnings("unchecked") - default ComponentFactory factoryMissing(Class clazz) { - return (ComponentFactory) this.factoryMissing(clazz.getName()); - } + TypeAndFactory get(Class type); - default void add(String name, Closure closure) { - this.add(name, ComponentFactory.ofClosure(closure)); - } + void remove(Class type); - default void add(Class type, Closure closure) { - this.add(type, ComponentFactory.ofClosure(closure)); + default TypeAndFactory factoryMissing(String typeName, Class type) + throws ComponentResolveException { + throw new FactoryMissingUnsupportedException( + this.getClass().getName() + " does not support factoryMissing() for class component types.", + typeName, + type + ); } } diff --git a/view-components/src/main/java/groowt/view/component/context/DefaultComponentContext.java b/view-components/src/main/java/groowt/view/component/context/DefaultComponentContext.java index d670dd6..d4c257a 100644 --- a/view-components/src/main/java/groowt/view/component/context/DefaultComponentContext.java +++ b/view-components/src/main/java/groowt/view/component/context/DefaultComponentContext.java @@ -1,98 +1,35 @@ package groowt.view.component.context; import groowt.view.component.ViewComponent; -import groowt.view.component.factory.ComponentFactory; +import groowt.view.component.runtime.RenderContext; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Deque; import java.util.LinkedList; import java.util.List; +import java.util.Objects; import java.util.function.Predicate; public class DefaultComponentContext implements ComponentContext { - protected static class DefaultResolved implements ComponentContext.Resolved { + private final LinkedList scopeStack = new LinkedList<>(); + private RenderContext renderContext; - private final String typeName; - private final ComponentFactory factory; - - public DefaultResolved(String typeName, ComponentFactory factory) { - this.typeName = typeName; - this.factory = factory; - } - - @Override - public String getTypeName() { - return this.typeName; - } - - @Override - public ComponentFactory getComponentFactory() { - return this.factory; - } - - } - - private final Deque scopeStack = new LinkedList<>(); - private final Deque componentStack = new LinkedList<>(); - - @Override - public Resolved resolve(String component) { - if (scopeStack.isEmpty()) { - throw new IllegalStateException("There are no scopes on the scopeStack."); - } - - final var getStack = new LinkedList<>(this.scopeStack); - while (!getStack.isEmpty()) { - final ComponentScope scope = getStack.pop(); - if (scope.contains(component)) { - return new DefaultResolved(component, scope.get(component)); - } - } - - final var missingStack = new LinkedList<>(this.scopeStack); - NoFactoryMissingException first = null; - while (!missingStack.isEmpty()) { - final ComponentScope scope = missingStack.pop(); - try { - return new DefaultResolved(component, scope.factoryMissing(component)); - } catch (NoFactoryMissingException e) { - if (first == null) { - first = e; - } - } - } - - if (first == null) { - throw new IllegalStateException("First FactoryMissingException is still null."); - } - - throw first; - } - - @Override - public ViewComponent create(Resolved resolved, Object... args) { - return resolved.getComponentFactory().create( - resolved.getTypeName(), this, args + @ApiStatus.Internal + public RenderContext getRenderContext() { + return Objects.requireNonNull( + this.renderContext, + "The renderContext is null. Did this method get called from outside of a rendering context?" ); } - @Override - public void beforeComponentRender(ViewComponent component) { - this.componentStack.push(component); + @ApiStatus.Internal + public void setRenderContext(RenderContext renderContext) { + this.renderContext = Objects.requireNonNull(renderContext); } @Override - public void afterComponentRender(ViewComponent component) { - final var popped = this.componentStack.pop(); - if (!popped.equals(component)) { - throw new IllegalStateException("Popped component does not equal arg to afterComponent()"); - } - } - - @Override - public Deque getScopeStack() { + public List getScopeStack() { return new LinkedList<>(this.scopeStack); } @@ -116,18 +53,15 @@ public class DefaultComponentContext implements ComponentContext { } @Override - public Deque getComponentStack() { - return new LinkedList<>(this.componentStack); + public ComponentScope getRootScope() { + return this.scopeStack.getLast(); } @Override public @Nullable ViewComponent getParent() { - if (this.componentStack.size() > 1) { - final var child = this.componentStack.pop(); - final var parent = this.componentStack.pop(); - this.componentStack.push(parent); - this.componentStack.push(child); - return parent; + final List componentStack = this.getRenderContext().getComponentStack(); + if (componentStack.size() > 1) { + return componentStack.get(1); } return null; } @@ -139,35 +73,21 @@ public class DefaultComponentContext implements ComponentContext { @Override public @Nullable ViewComponent findNearestAncestor(Predicate matching) { - if (this.componentStack.size() > 1) { - final Deque tmp = new LinkedList<>(); - tmp.push(this.componentStack.pop()); // child - ViewComponent result = null; - while (result == null && !this.componentStack.isEmpty()) { - final var ancestor = this.componentStack.pop(); - tmp.push(ancestor); + final List componentStack = this.getRenderContext().getComponentStack(); + if (componentStack.size() > 1) { + for (final var ancestor : componentStack.subList(1, componentStack.size() -1)) { if (matching.test(ancestor)) { - result = ancestor; + return ancestor; } } - while (!tmp.isEmpty()) { - this.componentStack.push(tmp.pop()); - } - return result; } return null; } @Override public List getAllAncestors() { - if (this.componentStack.size() > 1) { - final var child = this.componentStack.pop(); - final List result = new ArrayList<>(this.componentStack); - this.componentStack.push(child); - return result; - } else { - return List.of(); - } + final List componentStack = this.getRenderContext().getComponentStack(); + return componentStack.subList(1, componentStack.size()); } } diff --git a/view-components/src/main/java/groowt/view/component/context/DefaultComponentScope.java b/view-components/src/main/java/groowt/view/component/context/DefaultComponentScope.java index c92347c..22e2174 100644 --- a/view-components/src/main/java/groowt/view/component/context/DefaultComponentScope.java +++ b/view-components/src/main/java/groowt/view/component/context/DefaultComponentScope.java @@ -1,32 +1,65 @@ package groowt.view.component.context; +import groowt.view.component.ViewComponent; import groowt.view.component.factory.ComponentFactory; import java.util.HashMap; import java.util.Map; +import java.util.Objects; public class DefaultComponentScope implements ComponentScope { - private final Map> factories = new HashMap<>(); + private final Map> stringFactories = new HashMap<>(); + private final Map, TypeAndFactory> classFactories = new HashMap<>(); @Override - public void add(String name, ComponentFactory factory) { - this.factories.put(name, factory); + public void add(String typeName, Class forClass, ComponentFactory factory) { + this.stringFactories.put(typeName, new TypeAndFactory<>(forClass, factory)); } @Override - public boolean contains(String name) { - return this.factories.containsKey(name); + public boolean contains(String typeName) { + return this.stringFactories.containsKey(typeName); } @Override - public void remove(String name) { - this.factories.remove(name); + public TypeAndFactory get(String typeName) { + return Objects.requireNonNull(this.stringFactories.get(typeName)); } @Override - public ComponentFactory get(String name) { - return this.factories.get(name); + public void remove(String typeName) { + this.stringFactories.remove(typeName); + } + + @Override + public void add(Class forClass, ComponentFactory factory) { + this.classFactories.put(forClass, new TypeAndFactory<>(forClass, factory)); + } + + @Override + public void add( + Class publicType, + Class implementingType, + ComponentFactory factory + ) { + this.classFactories.put(publicType, new TypeAndFactory(implementingType, factory)); + } + + @Override + public boolean contains(Class type) { + return this.classFactories.containsKey(type); + } + + @SuppressWarnings("unchecked") + @Override + public TypeAndFactory get(Class type) { + return (TypeAndFactory) Objects.requireNonNull(this.classFactories.get(type)); + } + + @Override + public void remove(Class type) { + this.classFactories.remove(type); } } diff --git a/view-components/src/main/java/groowt/view/component/context/FactoryMissingUnsupportedException.java b/view-components/src/main/java/groowt/view/component/context/FactoryMissingUnsupportedException.java new file mode 100644 index 0000000..ef7ff2b --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/context/FactoryMissingUnsupportedException.java @@ -0,0 +1,48 @@ +package groowt.view.component.context; + +import groowt.view.component.ViewComponent; +import org.jetbrains.annotations.Nullable; + +public class FactoryMissingUnsupportedException extends ComponentResolveException { + + private final String message; + + public FactoryMissingUnsupportedException(@Nullable String message, String typeName) { + super(typeName); + this.message = message; + } + + public FactoryMissingUnsupportedException( + @Nullable String message, + String typeName, + @Nullable Class type + ) { + super(typeName, type); + this.message = message; + } + + public FactoryMissingUnsupportedException(@Nullable String message, String typeName, Throwable cause) { + super(typeName, cause); + this.message = message; + } + + public FactoryMissingUnsupportedException( + @Nullable String message, + String typeName, + @Nullable Class type, + Throwable cause + ) { + super(typeName, type, cause); + this.message = message; + } + + @Override + public String getMessage() { + if (this.message != null) { + return super.getMessage() + " " + this.message; + } else { + return super.getMessage(); + } + } + +} diff --git a/view-components/src/main/java/groowt/view/component/context/MissingClassTypeException.java b/view-components/src/main/java/groowt/view/component/context/MissingClassTypeException.java deleted file mode 100644 index 58b128f..0000000 --- a/view-components/src/main/java/groowt/view/component/context/MissingClassTypeException.java +++ /dev/null @@ -1,19 +0,0 @@ -package groowt.view.component.context; - -import groowt.view.component.ComponentTemplate; - -public class MissingClassTypeException extends MissingComponentException { - - private final String typeName; - - public MissingClassTypeException(ComponentTemplate template, String typeName, int line, int col, Throwable cause) { - super(template, cause, line, col); - this.typeName = typeName; - } - - @Override - protected String getMissingKeyName() { - return "class component " + this.typeName; - } - -} diff --git a/view-components/src/main/java/groowt/view/component/context/MissingComponentException.java b/view-components/src/main/java/groowt/view/component/context/MissingComponentException.java deleted file mode 100644 index 21f9c74..0000000 --- a/view-components/src/main/java/groowt/view/component/context/MissingComponentException.java +++ /dev/null @@ -1,31 +0,0 @@ -package groowt.view.component.context; - -import groowt.view.component.ComponentTemplate; -import groowt.view.component.context.ComponentContext; - -/** - * An exception which represents that a component type could not be - * found by the {@link ComponentContext}. - */ -public abstract class MissingComponentException extends RuntimeException { - - private final ComponentTemplate template; - private final int line; - private final int col; - - public MissingComponentException(ComponentTemplate template, Throwable cause, int line, int col) { - super(cause); - this.template = template; - this.line = line; - this.col = col; - } - - protected abstract String getMissingKeyName(); - - @Override - public String getMessage() { - return "In " + this.template + " missing " + this.getMissingKeyName() - + " on line " + this.line + ", column " + this.col + "."; - } - -} diff --git a/view-components/src/main/java/groowt/view/component/context/MissingFragmentTypeException.java b/view-components/src/main/java/groowt/view/component/context/MissingFragmentTypeException.java deleted file mode 100644 index d6c2718..0000000 --- a/view-components/src/main/java/groowt/view/component/context/MissingFragmentTypeException.java +++ /dev/null @@ -1,16 +0,0 @@ -package groowt.view.component.context; - -import groowt.view.component.ComponentTemplate; - -public class MissingFragmentTypeException extends MissingComponentException { - - public MissingFragmentTypeException(ComponentTemplate template, int line, int col, Throwable cause) { - super(template, cause, line, col); - } - - @Override - protected String getMissingKeyName() { - return "fragment type"; - } - -} diff --git a/view-components/src/main/java/groowt/view/component/context/MissingStringTypeException.java b/view-components/src/main/java/groowt/view/component/context/MissingStringTypeException.java deleted file mode 100644 index d213bd1..0000000 --- a/view-components/src/main/java/groowt/view/component/context/MissingStringTypeException.java +++ /dev/null @@ -1,19 +0,0 @@ -package groowt.view.component.context; - -import groowt.view.component.ComponentTemplate; - -public abstract class MissingStringTypeException extends MissingComponentException { - - private final String keyName; - - public MissingStringTypeException(ComponentTemplate template, String keyName, int line, int col, Throwable cause) { - super(template, cause, line, col); - this.keyName = keyName; - } - - @Override - protected String getMissingKeyName() { - return "string-typed component " + this.keyName; - } - -} diff --git a/view-components/src/main/java/groowt/view/component/context/NoFactoryMissingException.java b/view-components/src/main/java/groowt/view/component/context/NoFactoryMissingException.java deleted file mode 100644 index 8b1fa01..0000000 --- a/view-components/src/main/java/groowt/view/component/context/NoFactoryMissingException.java +++ /dev/null @@ -1,19 +0,0 @@ -package groowt.view.component.context; - -public class NoFactoryMissingException extends UnsupportedOperationException { - - public NoFactoryMissingException() {} - - public NoFactoryMissingException(String message) { - super(message); - } - - public NoFactoryMissingException(String message, Throwable cause) { - super(message, cause); - } - - public NoFactoryMissingException(Throwable cause) { - super(cause); - } - -} diff --git a/view-components/src/main/java/groowt/view/component/factory/ClosureComponentFactory.java b/view-components/src/main/java/groowt/view/component/factory/AbstractClosureComponentFactory.java similarity index 58% rename from view-components/src/main/java/groowt/view/component/factory/ClosureComponentFactory.java rename to view-components/src/main/java/groowt/view/component/factory/AbstractClosureComponentFactory.java index fcc2ac4..ec606f3 100644 --- a/view-components/src/main/java/groowt/view/component/factory/ClosureComponentFactory.java +++ b/view-components/src/main/java/groowt/view/component/factory/AbstractClosureComponentFactory.java @@ -1,12 +1,13 @@ package groowt.view.component.factory; import groovy.lang.Closure; -import groowt.view.component.context.ComponentContext; import groowt.view.component.ViewComponent; +import groowt.view.component.context.ComponentContext; import static groowt.view.component.factory.ComponentFactoryUtil.flatten; -final class ClosureComponentFactory implements ComponentFactory { +public abstract class AbstractClosureComponentFactory extends Closure implements + ComponentFactory { private enum Type { ALL, @@ -15,18 +16,18 @@ final class ClosureComponentFactory implements Componen NONE } - private final Closure closure; + private final Closure closure; private final Type type; - @SuppressWarnings("unchecked") - public ClosureComponentFactory(Closure closure) { - this.closure = (Closure) closure; + public AbstractClosureComponentFactory(Closure closure) { + super(closure.getOwner(), closure.getThisObject()); + this.closure = closure; final var paramTypes = this.closure.getParameterTypes(); if (paramTypes.length == 0) { this.type = Type.NONE; } else if (paramTypes.length == 1) { final var paramType = paramTypes[0]; - if (paramType == String.class || paramType == Class.class) { + if (paramType == String.class) { this.type = Type.NAME_ONLY; } else if (ComponentContext.class.isAssignableFrom(paramType)) { this.type = Type.CONTEXT_ONLY; @@ -36,7 +37,7 @@ final class ClosureComponentFactory implements Componen } else { final var firstParamType = paramTypes[0]; final var secondParamType = paramTypes[1]; - if (firstParamType == String.class || firstParamType == Class.class) { + if (firstParamType == String.class) { if (ComponentContext.class.isAssignableFrom(secondParamType)) { if (paramTypes.length > 2) { this.type = Type.ALL; @@ -54,31 +55,17 @@ final class ClosureComponentFactory implements Componen } } - private T flatCall(Object... args) { - return this.closure.call(flatten(args)); - } - - private T objTypeCreate(Object type, ComponentContext componentContext, Object... args) { + protected T doCall(String typeNameOrAlias, ComponentContext componentContext, Object... args) { return switch (this.type) { - case ALL -> this.flatCall(type, componentContext, args); - case NAME_AND_CONTEXT -> this.closure.call(type, componentContext); - case NAME_AND_ARGS -> this.flatCall(type, args); - case CONTEXT_AND_ARGS -> this.flatCall(componentContext, args); - case NAME_ONLY -> this.closure.call(type); + case ALL -> this.closure.call(flatten(typeNameOrAlias, componentContext, args)); + case NAME_AND_CONTEXT -> this.closure.call(typeNameOrAlias, componentContext); + case NAME_AND_ARGS -> this.closure.call(flatten(typeNameOrAlias, args)); + case CONTEXT_AND_ARGS -> this.closure.call(flatten(componentContext, args)); + case NAME_ONLY -> this.closure.call(typeNameOrAlias); case CONTEXT_ONLY -> this.closure.call(componentContext); case ARGS_ONLY -> this.closure.call(args); case NONE -> this.closure.call(); }; } - @Override - public T create(String type, ComponentContext componentContext, Object... args) { - return this.objTypeCreate(type, componentContext, args); - } - - @Override - public T create(Class type, ComponentContext componentContext, Object... args) { - return this.objTypeCreate(type, componentContext, args); - } - } diff --git a/view-components/src/main/java/groowt/view/component/factory/ClassTypeClosureComponentFactory.java b/view-components/src/main/java/groowt/view/component/factory/ClassTypeClosureComponentFactory.java new file mode 100644 index 0000000..6863d65 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/factory/ClassTypeClosureComponentFactory.java @@ -0,0 +1,34 @@ +package groowt.view.component.factory; + +import groovy.lang.Closure; +import groowt.view.component.ViewComponent; +import groowt.view.component.context.ComponentContext; + +final class ClassTypeClosureComponentFactory extends AbstractClosureComponentFactory { + + private final Class forClass; + + public ClassTypeClosureComponentFactory(Class forClass, Closure closure) { + super(closure); + this.forClass = forClass; + } + + @Override + public T create(String typeName, ComponentContext componentContext, Object... args) { + throw new UnsupportedOperationException( + "ClassTypeClosureComponentFactory cannot handle string component types." + ); + } + + @Override + public T create(String alias, Class type, ComponentContext componentContext, Object... args) { + if (!this.forClass.isAssignableFrom(type)) { + throw new IllegalArgumentException( + "This ClassTypeClosureComponentFactory cannot handle type " + type.getName() + + "; can only handle " + this.forClass.getName() + "." + ); + } + return this.doCall(alias, componentContext, args); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/factory/ComponentFactories.java b/view-components/src/main/java/groowt/view/component/factory/ComponentFactories.java new file mode 100644 index 0000000..3e1d8ed --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/factory/ComponentFactories.java @@ -0,0 +1,27 @@ +package groowt.view.component.factory; + +import groovy.lang.Closure; +import groowt.view.component.ViewComponent; + +import java.util.function.Supplier; + +public final class ComponentFactories { + + public static ComponentFactory ofClosureStringType(Closure closure) { + return new StringTypeClosureComponentFactory<>(closure); + } + + public static ComponentFactory ofClosureClassType( + Class forClass, + Closure closure + ) { + return new ClassTypeClosureComponentFactory<>(forClass, closure); + } + + public static ComponentFactory ofSupplier(Supplier supplier) { + return (typeName, componentContext, args) -> supplier.get(); + } + + private ComponentFactories() {} + +} diff --git a/view-components/src/main/java/groowt/view/component/factory/ComponentFactory.java b/view-components/src/main/java/groowt/view/component/factory/ComponentFactory.java index 2c0009b..5c9a3b5 100644 --- a/view-components/src/main/java/groowt/view/component/factory/ComponentFactory.java +++ b/view-components/src/main/java/groowt/view/component/factory/ComponentFactory.java @@ -1,25 +1,14 @@ package groowt.view.component.factory; -import groovy.lang.Closure; -import groowt.view.component.context.ComponentContext; import groowt.view.component.ViewComponent; - -import java.util.function.Supplier; +import groowt.view.component.context.ComponentContext; @FunctionalInterface public interface ComponentFactory { - static ComponentFactory ofClosure(Closure closure) { - return new ClosureComponentFactory<>(closure); - } - - static ComponentFactory ofSupplier(Supplier supplier) { - return new SupplierComponentFactory<>(supplier); - } - T create(String typeName, ComponentContext componentContext, Object... args); - default T create(Class type, ComponentContext componentContext, Object... args) { + default T create(String alias, Class type, ComponentContext componentContext, Object... args) { return this.create(type.getName(), componentContext, args); } diff --git a/view-components/src/main/java/groowt/view/component/factory/ComponentFactoryBase.java b/view-components/src/main/java/groowt/view/component/factory/ComponentFactoryBase.java index 88ec06e..c3ad2ea 100644 --- a/view-components/src/main/java/groowt/view/component/factory/ComponentFactoryBase.java +++ b/view-components/src/main/java/groowt/view/component/factory/ComponentFactoryBase.java @@ -94,13 +94,14 @@ public abstract class ComponentFactoryBase extends Groo } @Override - public T create(String type, ComponentContext componentContext, Object... args) { - return this.findAndDoCreate(type, componentContext, args); + public T create(String typeName, ComponentContext componentContext, Object... args) { + throw new UnsupportedOperationException(); } + // TODO: this needs to be updated. @Override - public T create(Class type, ComponentContext componentContext, Object... args) { - return this.findAndDoCreate(type, componentContext, args); + public T create(String alias, Class type, ComponentContext componentContext, Object... args) { + throw new UnsupportedOperationException(); } } diff --git a/view-components/src/main/java/groowt/view/component/factory/ComponentTemplateSource.java b/view-components/src/main/java/groowt/view/component/factory/ComponentTemplateSource.java deleted file mode 100644 index 19ff547..0000000 --- a/view-components/src/main/java/groowt/view/component/factory/ComponentTemplateSource.java +++ /dev/null @@ -1,82 +0,0 @@ -package groowt.view.component.factory; - -import java.io.*; -import java.net.URI; -import java.net.URL; - -public sealed interface ComponentTemplateSource { - - static ComponentTemplateSource of(String template) { - return new StringSource(template); - } - - static ComponentTemplateSource of(File templateFile) { - return new FileSource(templateFile); - } - - static ComponentTemplateSource of(URI templateURI) { - return new URISource(templateURI); - } - - static ComponentTemplateSource of(URL url) { - return new URLSource(url); - } - - static ComponentTemplateSource of(InputStream templateInputStream) { - return new InputStreamSource(templateInputStream); - } - - static ComponentTemplateSource of(Reader templateReader) { - return new ReaderSource(templateReader); - } - - /** - * @param resourceName An absolute path resource name. - * @return A template source - */ - static ComponentTemplateSource fromResource(String resourceName) { - return of(ComponentTemplateSource.class.getClassLoader().getResource(resourceName)); - } - - static Reader toReader(ComponentTemplateSource source) { - return switch (source) { - case FileSource(File file) -> { - try { - yield new FileReader(file); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - } - case StringSource(String rawSource) -> new StringReader(rawSource); - case InputStreamSource(InputStream inputStream) -> new InputStreamReader(inputStream); - case URISource(URI uri) -> { - try { - yield new InputStreamReader(uri.toURL().openStream()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - case URLSource(URL url) -> { - try { - yield new InputStreamReader(url.openStream()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - case ReaderSource(Reader reader) -> reader; - }; - } - - record StringSource(String template) implements ComponentTemplateSource {} - - record FileSource(File templateFile) implements ComponentTemplateSource {} - - record URISource(URI templateURI) implements ComponentTemplateSource {} - - record URLSource(URL templateURL) implements ComponentTemplateSource {} - - record InputStreamSource(InputStream templateInputStream) implements ComponentTemplateSource {} - - record ReaderSource(Reader templateReader) implements ComponentTemplateSource {} - -} diff --git a/view-components/src/main/java/groowt/view/component/factory/StringTypeClosureComponentFactory.java b/view-components/src/main/java/groowt/view/component/factory/StringTypeClosureComponentFactory.java new file mode 100644 index 0000000..dd3b1cc --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/factory/StringTypeClosureComponentFactory.java @@ -0,0 +1,25 @@ +package groowt.view.component.factory; + +import groovy.lang.Closure; +import groowt.view.component.ViewComponent; +import groowt.view.component.context.ComponentContext; + +final class StringTypeClosureComponentFactory extends AbstractClosureComponentFactory { + + public StringTypeClosureComponentFactory(Closure closure) { + super(closure); + } + + @Override + public T create(String typeName, ComponentContext componentContext, Object... args) { + return this.doCall(typeName, componentContext, args); + } + + @Override + public T create(String alias, Class type, ComponentContext componentContext, Object... args) { + throw new UnsupportedOperationException( + "StringTypeClosureComponentFactory cannot handle class component types." + ); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/factory/SupplierComponentFactory.java b/view-components/src/main/java/groowt/view/component/factory/SupplierComponentFactory.java deleted file mode 100644 index c52dd66..0000000 --- a/view-components/src/main/java/groowt/view/component/factory/SupplierComponentFactory.java +++ /dev/null @@ -1,20 +0,0 @@ -package groowt.view.component.factory; - -import groowt.view.component.context.ComponentContext; -import groowt.view.component.ViewComponent; - -import java.util.function.Supplier; - -final class SupplierComponentFactory extends ComponentFactoryBase { - - private final Supplier tSupplier; - - public SupplierComponentFactory(Supplier tSupplier) { - this.tSupplier = tSupplier; - } - - public T doCreate(Object type, ComponentContext componentContext, Object... args) { - return this.tSupplier.get(); - } - -} diff --git a/view-components/src/main/java/groowt/view/component/runtime/ComponentCreateException.java b/view-components/src/main/java/groowt/view/component/runtime/ComponentCreateException.java new file mode 100644 index 0000000..b825d97 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/runtime/ComponentCreateException.java @@ -0,0 +1,91 @@ +package groowt.view.component.runtime; + +import groowt.view.component.ComponentTemplate; +import groowt.view.component.runtime.RenderContext.ResolvedStringType; +import org.jetbrains.annotations.ApiStatus; + +import java.util.Objects; + +/** + * An exception which signals that a component of the given type + * could not be created in the given template. + */ +public class ComponentCreateException extends RuntimeException { + + private final RenderContext.Resolved resolved; + + private ComponentTemplate template; + private int line; + private int column; + + public ComponentCreateException( + RenderContext.Resolved resolved, + Throwable cause + ) { + super(cause); + this.resolved = resolved; + } + + public RenderContext.Resolved getResolved() { + return this.resolved; + } + + public ComponentTemplate getTemplate() { + return this.template; + } + + @ApiStatus.Internal + public void setTemplate(ComponentTemplate template) { + this.template = Objects.requireNonNull(template); + } + + public int getLine() { + return this.line; + } + + @ApiStatus.Internal + public void setLine(int line) { + this.line = line; + } + + public int getColumn() { + return this.column; + } + + @ApiStatus.Internal + public void setColumn(int column) { + this.column = column; + } + + @Override + public String getMessage() { + final var sb = new StringBuilder("Exception in ") + .append(this.template.getClass().getName()); + if (this.resolved instanceof ResolvedStringType resolvedStringType) { + sb.append(" while creating string-typed component ") + .append(resolvedStringType.typeName()) + .append(" (using ") + .append(resolvedStringType.resolvedType().getName()) + .append(") "); + } else if (this.resolved instanceof RenderContext.ResolvedClassType resolvedClassType) { + sb.append(" while creating class-typed component ") + .append(resolvedClassType.alias()) + .append(" of public type ") + .append(resolvedClassType.requestedType()) + .append(" (using ") + .append(resolvedClassType.resolvedType()) + .append(") "); + } else { + sb.append(" while creating unknown-typed component (using ") + .append(resolved.resolvedType().getName()) + .append(") "); + } + return sb.append(" at line ") + .append(this.line) + .append(", column ") + .append(this.column) + .append(".") + .toString(); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/runtime/ComponentWriter.java b/view-components/src/main/java/groowt/view/component/runtime/ComponentWriter.java new file mode 100644 index 0000000..adc86c5 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/runtime/ComponentWriter.java @@ -0,0 +1,32 @@ +package groowt.view.component.runtime; + +import groovy.lang.GString; +import groowt.view.component.ViewComponent; +import groowt.view.component.context.ComponentContext; +import org.jetbrains.annotations.ApiStatus; + +public interface ComponentWriter { + + @ApiStatus.Internal + void setRenderContext(RenderContext renderContext); + + @ApiStatus.Internal + void setComponentContext(ComponentContext componentContext); + + void append(String string); + void append(GString gString); + void append(GString gString, int line, int column); + void append(ViewComponent viewComponent); + void append(ViewComponent viewComponent, int line, int column); + void append(Object object); + + default void leftShift(Object object) { + switch (object) { + case String s -> this.append(s); + case GString gs -> this.append(gs); + case ViewComponent viewComponent -> this.append(viewComponent); + default -> this.append(object); + } + } + +} diff --git a/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentWriter.java b/view-components/src/main/java/groowt/view/component/runtime/DefaultComponentWriter.java similarity index 54% rename from web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentWriter.java rename to view-components/src/main/java/groowt/view/component/runtime/DefaultComponentWriter.java index 3ef07a8..4e6982b 100644 --- a/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentWriter.java +++ b/view-components/src/main/java/groowt/view/component/runtime/DefaultComponentWriter.java @@ -1,20 +1,42 @@ -package groowt.view.web.runtime; +package groowt.view.component.runtime; import groovy.lang.GString; import groowt.view.component.ComponentRenderException; import groowt.view.component.ViewComponent; +import groowt.view.component.context.ComponentContext; import java.io.IOException; import java.io.Writer; +import java.util.Objects; -public class DefaultWebViewComponentWriter implements WebViewComponentWriter { +public class DefaultComponentWriter implements ComponentWriter { private final Writer delegate; + private RenderContext renderContext; + private ComponentContext componentContext; - public DefaultWebViewComponentWriter(Writer delegate) { + public DefaultComponentWriter(Writer delegate) { this.delegate = delegate; } + protected RenderContext getRenderContext() { + return Objects.requireNonNull(this.renderContext); + } + + @Override + public void setRenderContext(RenderContext renderContext) { + this.renderContext = Objects.requireNonNull(renderContext); + } + + protected ComponentContext getComponentContext() { + return this.componentContext; + } + + @Override + public void setComponentContext(ComponentContext componentContext) { + this.componentContext = Objects.requireNonNull(componentContext); + } + @Override public void append(String string) { try { @@ -50,12 +72,22 @@ public class DefaultWebViewComponentWriter implements WebViewComponentWriter { } } + private void doComponentRender(ViewComponent viewComponent) throws IOException { + this.getRenderContext().pushComponent(viewComponent); + this.getComponentContext().pushDefaultScope(); + viewComponent.renderTo(this.delegate); + this.getComponentContext().popScope(); + this.getRenderContext().popComponent(viewComponent); + } + @Override public void append(ViewComponent viewComponent) { try { - viewComponent.renderTo(this.delegate); + this.doComponentRender(viewComponent); } catch (IOException ioException) { throw new RuntimeException(ioException); + } catch (ComponentRenderException componentRenderException) { + throw componentRenderException; } catch (Exception exception) { throw new ComponentRenderException(viewComponent, exception); } @@ -64,9 +96,11 @@ public class DefaultWebViewComponentWriter implements WebViewComponentWriter { @Override public void append(ViewComponent viewComponent, int line, int column) { try { - viewComponent.renderTo(this.delegate); + this.doComponentRender(viewComponent); } catch (IOException ioException) { throw new RuntimeException(ioException); + } catch (ComponentRenderException componentRenderException) { + throw componentRenderException; } catch (Exception exception) { throw new ComponentRenderException(viewComponent, line, column, exception); } @@ -74,20 +108,17 @@ public class DefaultWebViewComponentWriter implements WebViewComponentWriter { @Override public void append(Object object) { - try { - this.delegate.append(object.toString()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void leftShift(Object object) { switch (object) { case String s -> this.append(s); - case GString gs -> this.append(gs); + case GString gString -> this.append(gString); case ViewComponent viewComponent -> this.append(viewComponent); - default -> this.append(object); + default -> { + try { + this.delegate.append(object.toString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } } diff --git a/view-components/src/main/java/groowt/view/component/runtime/DefaultRenderContext.java b/view-components/src/main/java/groowt/view/component/runtime/DefaultRenderContext.java new file mode 100644 index 0000000..6b3a716 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/runtime/DefaultRenderContext.java @@ -0,0 +1,141 @@ +package groowt.view.component.runtime; + +import groowt.view.component.ViewComponent; +import groowt.view.component.ViewComponentBugError; +import groowt.view.component.context.ComponentContext; +import groowt.view.component.context.ComponentResolveException; +import groowt.view.component.context.ComponentScope.TypeAndFactory; + +import java.util.LinkedList; +import java.util.List; + +public class DefaultRenderContext implements RenderContext { + + private final ComponentContext componentContext; + private final ComponentWriter writer; + private final LinkedList componentStack = new LinkedList<>(); + + public DefaultRenderContext(ComponentContext componentContext, ComponentWriter writer) { + this.componentContext = componentContext; + this.writer = writer; + } + + public ComponentContext getComponentContext() { + return this.componentContext; + } + + @Override + public Resolved resolve(String typeName) throws ComponentResolveException { + for (final var scope : this.getComponentContext().getScopeStack()) { + if (scope.contains(typeName)) { + try { + final var typeAndFactory = scope.get(typeName); + return new ResolvedStringType<>(typeName, typeAndFactory.type(), typeAndFactory.factory()); + } catch (Exception e) { + throw new ComponentResolveException(typeName, e); + } + } + } + Exception firstException = null; + for (final var scope : this.getComponentContext().getScopeStack()) { + try { + final var typeAndFactory = (TypeAndFactory) scope.factoryMissing(typeName); + return new ResolvedStringType<>(typeName, typeAndFactory.type(), typeAndFactory.factory()); + } catch (Exception e) { + if (firstException == null) { + firstException = e; + } + } + } + if (firstException != null) { + throw new ComponentResolveException(typeName, firstException); + } else { + throw new ViewComponentBugError( + "Could not resolve factory for " + typeName + " and firstException was null." + ); + } + } + + @Override + public Resolved resolve(String alias, Class type) throws ComponentResolveException { + for (final var scope : this.getComponentContext().getScopeStack()) { + if (scope.contains(type)) { + try { + final var typeAndFactory = (TypeAndFactory) scope.get(type); + return new ResolvedClassType<>( + alias, + type, + typeAndFactory.type(), + typeAndFactory.factory() + ); + } catch (Exception e) { + throw new ComponentResolveException(alias, type, e); + } + } + } + throw new ComponentResolveException( + "Could not find a factory for " + alias + " of type " + type.getName() + " in scope.", + alias, + type + ); + } + + @Override + public ViewComponent create(Resolved resolved, Object... args) { + final ViewComponent created; + if (resolved instanceof ResolvedStringType resolvedStringType) { + try { + created = resolvedStringType.componentFactory().create( + resolvedStringType.typeName(), + this.getComponentContext(), + args + ); + } catch (Exception createException) { + throw new ComponentCreateException(resolved, createException); + } + } else if (resolved instanceof ResolvedClassType resolvedClassType) { + try { + created = resolvedClassType.componentFactory().create( + resolvedClassType.alias(), + resolvedClassType.resolvedType(), + this.getComponentContext(), + args + ); + } catch (Exception createException) { + throw new ComponentCreateException(resolved, createException); + } + } else { + throw new UnsupportedOperationException( + this.getClass().getName() + " cannot handle Resolved of sub-type " + resolved.getClass().getName() + ); + } + created.setContext(this.getComponentContext()); + return created; + } + + @Override + public void pushComponent(ViewComponent component) { + this.componentStack.push(component); + } + + @Override + public void popComponent(ViewComponent component) { + final var popped = this.componentStack.pop(); + if (!popped.equals(component)) { + throw new ViewComponentBugError( + "Popped component != expected component; popped: " + popped + ", expected: " + component + "." + ); + } + } + + @Override + public List getComponentStack() { + return new LinkedList<>(this.componentStack); + } + + @Override + public ComponentWriter getWriter() { + return this.writer; + } + +} diff --git a/view-components/src/main/java/groowt/view/component/runtime/RenderContext.java b/view-components/src/main/java/groowt/view/component/runtime/RenderContext.java new file mode 100644 index 0000000..6c8fbd7 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/runtime/RenderContext.java @@ -0,0 +1,43 @@ +package groowt.view.component.runtime; + +import groowt.view.component.ViewComponent; +import groowt.view.component.context.ComponentResolveException; +import groowt.view.component.factory.ComponentFactory; +import org.jetbrains.annotations.ApiStatus; + +import java.util.List; + +@ApiStatus.Internal +public interface RenderContext { + + interface Resolved { + Class resolvedType(); + ComponentFactory componentFactory(); + } + + record ResolvedStringType( + String typeName, + Class resolvedType, + ComponentFactory componentFactory + ) implements Resolved {} + + record ResolvedClassType( + String alias, + Class requestedType, + Class resolvedType, + ComponentFactory componentFactory + ) implements Resolved {} + + Resolved resolve(String typeName) throws ComponentResolveException; + Resolved resolve(String alias, Class type) throws ComponentResolveException; + + ViewComponent create(Resolved resolved, Object... args); + + void pushComponent(ViewComponent component); + void popComponent(ViewComponent component); + + List getComponentStack(); + + ComponentWriter getWriter(); + +} diff --git a/web-views/build.gradle b/web-views/build.gradle index 58adced..351d66c 100644 --- a/web-views/build.gradle +++ b/web-views/build.gradle @@ -4,6 +4,7 @@ import groowt.gradle.antlr.GroowtAntlrExecTask plugins { id 'groowt-conventions' id 'groowt-antlr-plugin' + id 'groowt-logging' id 'java-library' id 'groovy' id 'org.jetbrains.kotlin.jvm' @@ -19,13 +20,23 @@ configurations { toolsImplementation { extendsFrom(apiElements, runtimeElements) } + sketchingImplementation { + extendsFrom(apiElements, runtimeElements) + } } sourceSets { + sketching { + java { + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output + } + } tools { java { compileClasspath += sourceSets.main.output runtimeClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.sketching.output } } } @@ -35,7 +46,6 @@ dependencies { libs.groovy, libs.groovy.templates, libs.antlr.runtime, - libs.classgraph, project(':view-components'), project(':views') ) @@ -64,6 +74,8 @@ dependencies { groovyConsole libs.groovy.console toolsApi libs.picocli toolsImplementation libs.groovy.console + + sketchingApi libs.groovy } java { diff --git a/web-views/docs/asciidoc/componentTemplateSpec.asciidoc b/web-views/docs/asciidoc/componentTemplateSpec.asciidoc new file mode 100644 index 0000000..e056018 --- /dev/null +++ b/web-views/docs/asciidoc/componentTemplateSpec.asciidoc @@ -0,0 +1,130 @@ += Component Template Specification + +== Compiled Component Template Code + +The following code represents a typical (transpiled) component template: + +[source, groovy] +---- +package com.jessebrault.website + +import groowt.view.component.ComponentTemplate +import groowt.view.component.context.ComponentContext +import groowt.view.component.runtime.* +import groowt.view.web.WebViewComponent +import groowt.view.web.lib.* +import groowt.view.web.runtime.* + +class MyComponentTemplate implements ComponentTemplate { + + Closure getRenderer() { + return { ComponentContext componentContext, ComponentWriter out -> + // <1> + final RenderContext renderContext = new DefaultWebViewComponentRenderContext(componentContext, out) + componentContext.setRenderContext(renderContext) + out.setRenderContext(renderContext) + out.setComponentContext(renderContext) + + out.append 'Hello from simple text!' // <2> + + out.append "Hello from GString!" // <3> + + // <4> + ComponentContext.Resolved c0Resolved // <5> + try { + c0Resolved = renderContext.resolve('MySubComponent', MySubComponent) // <6> + } catch (ComponentResolveException c0ResolveException) { // <7> + c0ResolveException.template = this + c0ResolveException.line = 1 + c0ResolveException.column = 1 + throw c0ResolveException + } + + WebViewComponent c0 // <8> + try { + c0 = renderContext.create( // <9> + c0Resolved, + [greeting: 'Hello, World!'], + "Some constructor arg", + WebViewComponentChildCollectorClosure.get(this) { c0cc -> + c0cc.add 'JString child.' // <10> + c0cc.add "GString child" + + ComponentContext.Resolved c1Resolved + try { + c1Resolved = renderContext.resolve('h1') // <11> + } catch (ComponentResolveException c1ResolveException) { + c1ResolveException.type = IntrinsicHtml // <12> + c1ResolveException.template = this + c1ResolveException.line = 1 + c1ResolveException.column = 10 + throw c1ResolveException + } + + WebViewComponent c1 + try { + c1 = renderContext.create( + c1_resolved, + WebViewComponentChildCollectorClosure.get(this) { c1cc -> + c1cc.add "$greeting" + } + ) + } catch (ComponentCreateException c1CreateException) { + c1CreateException.template = this + c1CreateException.line = 1 + c1CreateException.column = 1 + } + + c0cc.add c1 + } + ) + } catch (ComponentCreateException c0CreateException) { + c0CreateException.template = this + c0CreateException.line = 1 + c0CreateException.column = 1 + throw c0CreateException + } + + // append it + out.append c0 + } + } +} +---- +<1> Initialize the contexts. +<2> Appending a plain old java string (jstring) to `out`. +<3> Appending a GString to `out`. +<4> Now begins a component 'block', where a component is resolved, created, and either rendered or appended + to a child collector (see below). +<5> First, we define the `resolved` variable, of the type `ComponentContext.Resolved`. +<6> Resolve it from the context. +<7> If the context cannot resolve the component, it should throw a `ComponentResolveException`. We catch it + here in order to set the template (`this`), line, and column information for debugging purposes. +<8> Now we can start to create the component by first defining a variable for it. +<9> The create function takes a few things: +. The relevant instance of `ComponentContext.Resolved`. +. Any attributes for the component, passed as a `Map`. +. Any arguments from the component constructor. +. A `WebViewComponentChildCollectorClosure`, which is a 'marker' subclass of `Closure` +(so that constructor args ending with a closure don't get confused), which simply collects +the children of the component. +<10> For children, we add the value of the child, rather than appending it directly to `out`. +The collector itself will be given the `out` writer from the `context`, which will then +supply the parent with the ability to render to out. This way the parent doesn't ever have to worry about `out` itself +(though if the parent wants access to `out` (for example, it is using a non-wvc template) it can access the out writer +from the context). +<11> Here, a string type is passed, since all lowercase type names are treated as string types in web view components. +<12> Because this is an intrinsic html element, we set the type here. +<13> Finally, our child component is added as a `Closure` which accepts the component back again and appends +it to out. + +== Items requiring Groovy ASTNode position adjustment + +The following items all need to have their transpiled Groovy ASTNodes' positions adjusted to match the original source +file, in case there is a Groovy compilation error involved. + +* JStrings +* GStrings +* Component types (Class and String expressions) +* Attribute keys and values +* Component constructor args diff --git a/web-views/sketching/memberClassComponentType.wvc b/web-views/sketching/memberClassComponentType.wvc new file mode 100644 index 0000000..a63dd90 --- /dev/null +++ b/web-views/sketching/memberClassComponentType.wvc @@ -0,0 +1,4 @@ +--- +package groowt.view.web.sketching +--- + diff --git a/web-views/sketching/simpleGreeter.wvc b/web-views/sketching/simpleGreeter.wvc index fa47c6d..bcc823d 100644 --- a/web-views/sketching/simpleGreeter.wvc +++ b/web-views/sketching/simpleGreeter.wvc @@ -1 +1,11 @@ +--- +class Greeter extends BaseWebViewComponent { + String target + + Greeter(Map attr) { + super('Hello, $target!') + this.target = attr.target + } +} +--- diff --git a/web-views/src/main/groovy/groowt/view/web/BaseWebViewComponent.groovy b/web-views/src/main/groovy/groowt/view/web/BaseWebViewComponent.groovy new file mode 100644 index 0000000..be84900 --- /dev/null +++ b/web-views/src/main/groovy/groowt/view/web/BaseWebViewComponent.groovy @@ -0,0 +1,46 @@ +package groowt.view.web + +import groowt.view.component.AbstractViewComponent +import groowt.view.component.ComponentTemplate +import groowt.view.component.compiler.ComponentTemplateCompileUnit +import groowt.view.component.compiler.source.ComponentTemplateSource + +import java.util.function.Function + +abstract class BaseWebViewComponent extends AbstractWebViewComponent { + + BaseWebViewComponent() {} + + BaseWebViewComponent(ComponentTemplate template) { + super(template) + } + + BaseWebViewComponent(Class templateClass) { + super(templateClass) + } + + BaseWebViewComponent( + Function, ComponentTemplateCompileUnit> compileUnitFunction + ) { + super(compileUnitFunction) + } + + BaseWebViewComponent(ComponentTemplateSource source) { + super(source) + } + + /** + * A convenience constructor which creates a {@link ComponentTemplateSource} + * from the given {@code source} parameter and passes it to super. See + * {@link ComponentTemplateSource} for possible types. + * + * @param source the object passed to {@link ComponentTemplateSource#of} + * + * @see ComponentTemplateSource + */ + @SuppressWarnings('GroovyAssignabilityCheck') + BaseWebViewComponent(Object source) { + super(ComponentTemplateSource.of(source)) + } + +} diff --git a/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponent.groovy b/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponent.groovy deleted file mode 100644 index 7f571b2..0000000 --- a/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponent.groovy +++ /dev/null @@ -1,78 +0,0 @@ -package groowt.view.web - -import groowt.view.component.AbstractViewComponent -import groowt.view.component.ComponentTemplate -import groowt.view.component.compiler.ComponentTemplateCompiler -import groowt.view.component.factory.ComponentTemplateSource -import groowt.view.web.compiler.DefaultWebViewComponentTemplateCompiler -import groowt.view.web.compiler.WebViewComponentTemplateCompiler -import org.codehaus.groovy.control.CompilerConfiguration - -import java.util.function.Function - -abstract class DefaultWebViewComponent extends AbstractWebViewComponent { - - private static final GroovyClassLoader groovyClassLoader = - new GroovyClassLoader(DefaultWebViewComponent.classLoader) - - private static final Function compilerFunction = { Class givenSelfClass -> - new DefaultWebViewComponentTemplateCompiler( - groovyClassLoader, - CompilerConfiguration.DEFAULT, - givenSelfClass.packageName - ) - } - - protected DefaultWebViewComponent() {} - - protected DefaultWebViewComponent(ComponentTemplate template) { - super(template) - } - - protected DefaultWebViewComponent(Class templateType) { - super(templateType) - } - - protected DefaultWebViewComponent(ComponentTemplateSource source) { - super(source, compilerFunction) - } - - protected DefaultWebViewComponent(ComponentTemplateSource source, WebViewComponentTemplateCompiler compiler) { - super(source, compiler) - } - - /** - * A convenience constructor which creates a {@link ComponentTemplateSource} - * from the given {@code source} parameter and passes it to super. See - * {@link ComponentTemplateSource} for possible types. - * - * @param source the object passed to {@link ComponentTemplateSource#of} - * @param compiler the compiler to use - * - * @see ComponentTemplateSource - */ - @SuppressWarnings('GroovyAssignabilityCheck') - protected DefaultWebViewComponent(Object source, WebViewComponentTemplateCompiler compiler) { - super(ComponentTemplateSource.of(source), compiler) - } - - /** - * A convenience constructor which creates a {@link ComponentTemplateSource} - * from the given {@code source} parameter and passes it to super. See - * {@link ComponentTemplateSource} for possible types. - * - * @param source the object passed to {@link ComponentTemplateSource#of} - * - * @see ComponentTemplateSource - */ - @SuppressWarnings('GroovyAssignabilityCheck') - protected DefaultWebViewComponent(Object source) { - super(ComponentTemplateSource.of(source), compilerFunction) - } - - @Override - protected final Class getSelfClass() { - this.class - } - -} diff --git a/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponentContext.groovy b/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponentContext.groovy index a3b83db..68ae298 100644 --- a/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponentContext.groovy +++ b/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponentContext.groovy @@ -2,26 +2,16 @@ package groowt.view.web import groowt.view.component.context.ComponentScope import groowt.view.component.context.DefaultComponentContext -import groowt.view.component.ViewComponent -import groowt.view.web.lib.Fragment -import groowt.view.web.runtime.DefaultWebViewComponentChildCollection -import org.jetbrains.annotations.ApiStatus class DefaultWebViewComponentContext extends DefaultComponentContext implements WebViewComponentContext { - @Override - protected ComponentScope getNewDefaultScope() { - new WebViewScope() + DefaultWebViewComponentContext() { + this.pushScope(WebViewComponentScope.getDefaultRootScope()) } @Override - @ApiStatus.Internal - ViewComponent createFragment(Closure childCollector) { - def childCollection = new DefaultWebViewComponentChildCollection() - childCollector.call(childCollection) - def fragment = new Fragment() - fragment.childRenderers = childCollection.children - fragment + protected ComponentScope getNewDefaultScope() { + new WebViewComponentScope() } } diff --git a/web-views/src/main/groovy/groowt/view/web/WebViewComponentFactories.groovy b/web-views/src/main/groovy/groowt/view/web/WebViewComponentFactories.groovy index 0b649c8..efb6576 100644 --- a/web-views/src/main/groovy/groowt/view/web/WebViewComponentFactories.groovy +++ b/web-views/src/main/groovy/groowt/view/web/WebViewComponentFactories.groovy @@ -3,20 +3,42 @@ package groowt.view.web import groovy.transform.stc.ClosureParams import groovy.transform.stc.FromString import groowt.view.component.factory.ComponentFactory +import groowt.view.web.runtime.DefaultWebViewComponentChildCollector +import groowt.view.web.runtime.WebViewComponentChildCollector -import java.util.function.Function +import static groowt.view.component.factory.ComponentFactories.ofClosureClassType final class WebViewComponentFactories { static ComponentFactory withAttr( + Class forClass, @ClosureParams(value = FromString, options = 'java.util.Map') - Closure closure + Closure closure ) { - ComponentFactory.ofClosure { Map attr -> closure(attr) } + ofClosureClassType(forClass) { Map attr -> closure(attr) } } - static ComponentFactory withAttr(Function, T> tFunction) { - ComponentFactory.ofClosure { Map attr -> tFunction.apply(attr) } + static ComponentFactory withChildren( + Class forClass, + @ClosureParams(value = FromString, options = 'java.util.List') + Closure closure + ) { + ofClosureClassType(forClass) { WebViewComponentChildCollector childCollector -> + closure(childCollector.children) + } + } + + static ComponentFactory withAttrAndChildren( + Class forClass, + @ClosureParams( + value = FromString, + options = 'java.util.Map, java.util.List' + ) + Closure closure + ) { + ofClosureClassType(forClass) { Map attr, WebViewComponentChildCollector childCollector -> + closure(attr, childCollector.children) + } } private WebViewComponentFactories() {} diff --git a/web-views/src/main/groovy/groowt/view/web/WebViewComponentScope.groovy b/web-views/src/main/groovy/groowt/view/web/WebViewComponentScope.groovy new file mode 100644 index 0000000..2e78ac9 --- /dev/null +++ b/web-views/src/main/groovy/groowt/view/web/WebViewComponentScope.groovy @@ -0,0 +1,41 @@ +package groowt.view.web + +import groowt.view.component.context.DefaultComponentScope +import groowt.view.web.lib.Echo +import groowt.view.web.lib.IntrinsicHtml +import org.codehaus.groovy.runtime.InvokerHelper + +import static groowt.view.web.WebViewComponentFactories.* + +class WebViewComponentScope extends DefaultComponentScope { + + static WebViewComponentScope getDefaultRootScope() { + new WebViewComponentScope().tap { + add(Echo, Echo.FACTORY) + } + } + + void addWithAttr(Class componentClass) { + add(componentClass, withAttr(componentClass) { attr -> + InvokerHelper.invokeConstructorOf(componentClass, attr) as T + }) + } + + void addWithChildren(Class componentClass) { + add(componentClass, withChildren(componentClass) { children -> + InvokerHelper.invokeConstructorOf(componentClass, children) as T + }) + } + + void addWithAttrAndChildren(Class componentClass) { + add(componentClass, withAttrAndChildren(componentClass) { attr, children -> + InvokerHelper.invokeConstructorOf(componentClass, [attr, children] as Object[]) as T + }) + } + + @Override + TypeAndFactory factoryMissing(String typeName) { + IntrinsicHtml.TYPE_AND_FACTORY + } + +} diff --git a/web-views/src/main/groovy/groowt/view/web/WebViewScope.groovy b/web-views/src/main/groovy/groowt/view/web/WebViewScope.groovy deleted file mode 100644 index ec68379..0000000 --- a/web-views/src/main/groovy/groowt/view/web/WebViewScope.groovy +++ /dev/null @@ -1,14 +0,0 @@ -package groowt.view.web - -import groowt.view.component.factory.ComponentFactory -import groowt.view.component.context.DefaultComponentScope -import groowt.view.web.lib.Echo - -class WebViewScope extends DefaultComponentScope { - - @Override - ComponentFactory factoryMissing(String typeName) { - Echo.FACTORY - } - -} diff --git a/web-views/src/main/groovy/groowt/view/web/lib/DelegatingWebViewComponent.java b/web-views/src/main/groovy/groowt/view/web/lib/DelegatingWebViewComponent.java index c48f94b..3b65e31 100644 --- a/web-views/src/main/groovy/groowt/view/web/lib/DelegatingWebViewComponent.java +++ b/web-views/src/main/groovy/groowt/view/web/lib/DelegatingWebViewComponent.java @@ -1,23 +1,12 @@ package groowt.view.web.lib; import groowt.view.View; -import groowt.view.web.DefaultWebViewComponent; +import groowt.view.web.BaseWebViewComponent; import java.io.IOException; import java.io.Writer; -import java.util.Map; -public abstract class DelegatingWebViewComponent extends DefaultWebViewComponent { - - private final Map attr; - - public DelegatingWebViewComponent(Map attr) { - this.attr = attr; - } - - protected Map getAttr() { - return this.attr; - } +public abstract class DelegatingWebViewComponent extends BaseWebViewComponent { protected abstract View getDelegate(); diff --git a/web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy b/web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy index b3d1428..f732081 100644 --- a/web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy +++ b/web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy @@ -3,8 +3,7 @@ package groowt.view.web.lib import groowt.view.View import groowt.view.component.context.ComponentContext import groowt.view.component.factory.ComponentFactory -import groowt.view.component.ComponentRenderException -import groowt.view.web.WebViewChildComponentRenderer +import groowt.view.web.runtime.WebViewComponentChildCollector class Echo extends DelegatingWebViewComponent { @@ -12,95 +11,50 @@ class Echo extends DelegatingWebViewComponent { protected static class EchoFactory implements ComponentFactory { - Echo doCreate(String typeName) { - doCreate(typeName, [:], true) + protected Echo doCreate() { + new Echo([:], []) } - Echo doCreate(String typeName, boolean selfClose) { - doCreate(typeName, [:], selfClose) + protected Echo doCreate(Map attr) { + new Echo(attr, []) } - Echo doCreate(String typeName, Map attr) { - doCreate(typeName, attr, true) + protected Echo doCreate(WebViewComponentChildCollector childCollector) { + new Echo([:], childCollector.children) } - Echo doCreate(String typeName, Map attr, boolean selfClose) { - new Echo(attr, typeName, selfClose) - } - - Echo doCreate( - String typeName, - Map attr, - List children - ) { - def echo = new Echo(attr, typeName, false) - echo.childRenderers = children - echo + protected Echo doCreate(Map attr, WebViewComponentChildCollector childCollector) { + new Echo(attr, childCollector.children) } @Override - Echo create(String type, ComponentContext componentContext, Object... args) { - this.doCreate(type, *args) as Echo + Echo create(String typeName, ComponentContext componentContext, Object... args) { + throw new UnsupportedOperationException('Cannot create Echo for string type components') } @Override - Echo create(Class type, ComponentContext componentContext, Object... args) { - throw new UnsupportedOperationException(' can only be used with String types.') + Echo create(String alias, Class type, ComponentContext componentContext, Object... args) { + this.doCreate(*args) } } - String name - boolean selfClose + Map attr - Echo(Map attr, String name, boolean selfClose) { - super(attr) - this.name = name - this.selfClose = selfClose + Echo(Map attr, List children) { + this.attr = attr + this.children = children + } + + Object propertyMissing(String propertyName) { + attr[propertyName] } @Override protected View getDelegate() { - if (this.selfClose && this.hasChildren()) { - throw new ComponentRenderException('Cannot have selfClose set to true and have children.') - } return { - it << '<' - it << this.name - if (!this.attr.isEmpty()) { - it << ' ' - formatAttr(it) - } - if (this.selfClose) { - it << ' /' - } - it << '>' - if (this.hasChildren()) { - this.renderChildren() // TODO: fix this - } - if (this.hasChildren() || !this.selfClose) { - it << '' - } - } - } - - protected void formatAttr(Writer writer) { - def iter = this.attr.iterator() - while (iter.hasNext()) { - def entry = iter.next() - writer << entry.key - def value = entry.value - if (value instanceof Boolean) { - // no-op, because we already wrote the key - } else { - writer << '="' - writer << value - writer << '"' - } - if (iter.hasNext()) { - writer << ' ' + this.children.each { + it.render(this) } } } diff --git a/web-views/src/main/groovy/groowt/view/web/lib/Fragment.groovy b/web-views/src/main/groovy/groowt/view/web/lib/Fragment.groovy index b0d5d41..c4c4334 100644 --- a/web-views/src/main/groovy/groowt/view/web/lib/Fragment.groovy +++ b/web-views/src/main/groovy/groowt/view/web/lib/Fragment.groovy @@ -1,8 +1,8 @@ package groowt.view.web.lib -import groowt.view.web.DefaultWebViewComponent +import groowt.view.web.BaseWebViewComponent -final class Fragment extends DefaultWebViewComponent { +final class Fragment extends BaseWebViewComponent { @Override void renderTo(Writer out) throws IOException { diff --git a/web-views/src/main/groovy/groowt/view/web/lib/HtmlPage.groovy b/web-views/src/main/groovy/groowt/view/web/lib/HtmlPage.groovy new file mode 100644 index 0000000..24fff19 --- /dev/null +++ b/web-views/src/main/groovy/groowt/view/web/lib/HtmlPage.groovy @@ -0,0 +1,6 @@ +package groowt.view.web.lib + +import groowt.view.web.BaseWebViewComponent +import groowt.view.web.util.ConfigurableComponent + +class HtmlPage extends BaseWebViewComponent implements ConfigurableComponent {} diff --git a/web-views/src/main/groovy/groowt/view/web/lib/IntrinsicHtml.groovy b/web-views/src/main/groovy/groowt/view/web/lib/IntrinsicHtml.groovy index 0412382..788c05c 100644 --- a/web-views/src/main/groovy/groowt/view/web/lib/IntrinsicHtml.groovy +++ b/web-views/src/main/groovy/groowt/view/web/lib/IntrinsicHtml.groovy @@ -1,16 +1,29 @@ package groowt.view.web.lib +import groowt.view.View +import groowt.view.component.ComponentRenderException import groowt.view.component.context.ComponentContext +import groowt.view.component.context.ComponentScope.TypeAndFactory import groowt.view.component.factory.ComponentFactory -import groowt.view.web.WebViewChildComponentRenderer +import groowt.view.web.WebViewComponentChild +import groowt.view.web.util.WithHtml -class IntrinsicHtml extends Echo { +class IntrinsicHtml extends DelegatingWebViewComponent implements WithHtml { + + static final ComponentFactory FACTORY = new IntrinsicHtmlFactory() + static final TypeAndFactory TYPE_AND_FACTORY = new TypeAndFactory<>(IntrinsicHtml, FACTORY) + + private static final Set voidElements = Set.of( + 'area', 'base', 'br', 'col', + 'embed', 'hr', 'img', 'input', + 'link', 'meta', 'param', 'source', + 'track', 'wbr' + ) - // TODO: check type name for HTML 5 validity protected static class IntrinsicHtmlFactory implements ComponentFactory { IntrinsicHtml doCreate(String typeName) { - new IntrinsicHtml([:], typeName, false) + new IntrinsicHtml([:], typeName, typeName in voidElements) } IntrinsicHtml doCreate(String typeName, boolean selfClose) { @@ -18,16 +31,16 @@ class IntrinsicHtml extends Echo { } IntrinsicHtml doCreate(String typeName, Map attr) { - new IntrinsicHtml(attr, typeName, false) + new IntrinsicHtml(attr, typeName, typeName in voidElements) } IntrinsicHtml doCreate(String typeName, Map attr, boolean selfClose) { new IntrinsicHtml(attr, typeName, selfClose) } - IntrinsicHtml doCreate(String typeName, Map attr, List children) { - def intrinsicHtml = new IntrinsicHtml(attr, typeName, false) - intrinsicHtml.childRenderers = children + IntrinsicHtml doCreate(String typeName, Map attr, List children) { + def intrinsicHtml = new IntrinsicHtml(attr, typeName, typeName in voidElements) + intrinsicHtml.children = children intrinsicHtml } @@ -37,14 +50,50 @@ class IntrinsicHtml extends Echo { } @Override - IntrinsicHtml create(Class type, ComponentContext componentContext, Object... args) { + IntrinsicHtml create(String alias, Class type, ComponentContext componentContext, Object... args) { throw new UnsupportedOperationException('Cannot create an IntrinsicHtml component with a class type.') } } - IntrinsicHtml(Map attr, String elementName, boolean selfClose) { - super(attr, elementName, selfClose) + Map attr + String name + boolean selfClose + + IntrinsicHtml(Map attr, String elementName, boolean selfClose) { + this.attr = attr + this.name = elementName + this.selfClose = selfClose + } + + @Override + protected View getDelegate() { + if (this.selfClose && this.hasChildren()) { + throw new ComponentRenderException('Cannot have selfClose set to true and have children.') + } + return { + it << '<' + it << this.name + if (!this.attr.isEmpty()) { + it << ' ' + this.formatAttr(it) + } + if (this.selfClose) { + it << ' /' + } + it << '>' + if (this.hasChildren()) { + this.children.each { + def renderer = it.getRenderer(this) + renderer.call(it.child) + } + } + if (this.hasChildren() || !this.selfClose) { + it << '' + } + } } } diff --git a/web-views/src/main/groovy/groowt/view/web/util/ComponentConfigurator.groovy b/web-views/src/main/groovy/groowt/view/web/util/ComponentConfigurator.groovy new file mode 100644 index 0000000..9c94983 --- /dev/null +++ b/web-views/src/main/groovy/groowt/view/web/util/ComponentConfigurator.groovy @@ -0,0 +1,27 @@ +package groowt.view.web.util + +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType +import groowt.view.web.WebViewComponent +import groowt.view.web.WebViewComponentContext + +class ComponentConfigurator { + + private final WebViewComponent self + + ComponentConfigurator(WebViewComponent self) { + this.self = self + } + + void context( + @DelegatesTo(ContextConfigurator) + @ClosureParams(value = SimpleType, options = 'groowt.view.web.WebViewComponentContext') + Closure configureContext + ) { + //noinspection GroovyAssignabilityCheck + WebViewComponentContext context = self.context + configureContext.delegate = new ContextConfigurator(context) + configureContext(context) + } + +} diff --git a/web-views/src/main/groovy/groowt/view/web/util/ConfigurableComponent.groovy b/web-views/src/main/groovy/groowt/view/web/util/ConfigurableComponent.groovy new file mode 100644 index 0000000..73a0823 --- /dev/null +++ b/web-views/src/main/groovy/groowt/view/web/util/ConfigurableComponent.groovy @@ -0,0 +1,22 @@ +package groowt.view.web.util + +import groovy.transform.SelfType +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.FromString +import groowt.view.web.WebViewComponent + +@SelfType(WebViewComponent) +trait ConfigurableComponent { + + @SuppressWarnings('GroovyAssignabilityCheck') + T configure( + @DelegatesTo(ComponentConfigurator) + @ClosureParams(value = FromString, options = 'T') + Closure configure + ) { + configure.delegate = new ComponentConfigurator(this) + configure(this) + this + } + +} diff --git a/web-views/src/main/groovy/groowt/view/web/util/ContextConfigurator.groovy b/web-views/src/main/groovy/groowt/view/web/util/ContextConfigurator.groovy new file mode 100644 index 0000000..8ada9da --- /dev/null +++ b/web-views/src/main/groovy/groowt/view/web/util/ContextConfigurator.groovy @@ -0,0 +1,27 @@ +package groowt.view.web.util + +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType +import groowt.view.web.WebViewComponentContext +import groowt.view.web.WebViewComponentScope + +class ContextConfigurator { + + private final WebViewComponentContext context + + ContextConfigurator(WebViewComponentContext context) { + this.context = context + } + + void rootScope( + @DelegatesTo(WebViewComponentScope) + @ClosureParams(value = SimpleType, options = 'groowt.view.web.WebViewComponentScope') + Closure configureRootScope + ) { + //noinspection GroovyAssignabilityCheck + WebViewComponentScope rootScope = context.rootScope + configureRootScope.delegate = rootScope + configureRootScope(rootScope) + } + +} diff --git a/web-views/src/main/groovy/groowt/view/web/util/WithHtml.groovy b/web-views/src/main/groovy/groowt/view/web/util/WithHtml.groovy new file mode 100644 index 0000000..b09c534 --- /dev/null +++ b/web-views/src/main/groovy/groowt/view/web/util/WithHtml.groovy @@ -0,0 +1,30 @@ +package groowt.view.web.util + +trait WithHtml { + + abstract Map getAttr() + + /** + * @param writer A {@link java.io.Writer}, a {@link groowt.view.component.runtime.ComponentWriter}, + * or anything which has {@code leftShift(String | Object)} as a method. + */ + void formatAttr(writer) { + def iter = attr.iterator() + while (iter.hasNext()) { + def entry = iter.next() + writer << entry.key + def value = entry.value + if (value instanceof Boolean) { + // no-op, because we already wrote the key + } else { + writer << '="' + writer << value + writer << '"' + } + if (iter.hasNext()) { + writer << ' ' + } + } + } + +} diff --git a/web-views/src/main/java/groowt/view/web/AbstractWebViewComponent.java b/web-views/src/main/java/groowt/view/web/AbstractWebViewComponent.java index 7b1d43f..873e853 100644 --- a/web-views/src/main/java/groowt/view/web/AbstractWebViewComponent.java +++ b/web-views/src/main/java/groowt/view/web/AbstractWebViewComponent.java @@ -3,11 +3,11 @@ package groowt.view.web; import groovy.lang.Closure; import groowt.view.component.AbstractViewComponent; import groowt.view.component.ComponentTemplate; -import groowt.view.component.compiler.ComponentTemplateCompiler; -import groowt.view.component.factory.ComponentTemplateSource; -import groowt.view.web.compiler.WebViewComponentTemplateCompiler; -import groowt.view.web.runtime.DefaultWebViewComponentWriter; -import groowt.view.web.runtime.WebViewComponentWriter; +import groowt.view.component.compiler.ComponentTemplateCompileUnit; +import groowt.view.component.compiler.source.ComponentTemplateSource; +import groowt.view.component.runtime.ComponentWriter; +import groowt.view.component.runtime.DefaultComponentWriter; +import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit; import java.io.IOException; import java.io.Writer; @@ -17,7 +17,7 @@ import java.util.function.Function; public abstract class AbstractWebViewComponent extends AbstractViewComponent implements WebViewComponent { - private List childRenderers; + private List childRenderers; public AbstractWebViewComponent() {} @@ -29,20 +29,18 @@ public abstract class AbstractWebViewComponent extends AbstractViewComponent imp super(templateClass); } - protected AbstractWebViewComponent(ComponentTemplateSource source, WebViewComponentTemplateCompiler compiler) { - super(source, compiler); + public AbstractWebViewComponent( + Function, ComponentTemplateCompileUnit> compileUnitFunction + ) { + super(compileUnitFunction); } - @SuppressWarnings("unchecked") - protected AbstractWebViewComponent( - ComponentTemplateSource source, - Function, ? extends ComponentTemplateCompiler> compilerFunction - ) { - super(source, selfClass -> compilerFunction.apply((Class) selfClass)); + public AbstractWebViewComponent(ComponentTemplateSource source) { + this(selfClass -> new WebViewComponentTemplateCompileUnit(selfClass, source, selfClass.getPackageName())); } @Override - public List getChildRenderers() { + public List getChildren() { if (this.childRenderers == null) { this.childRenderers = new ArrayList<>(); } @@ -51,35 +49,28 @@ public abstract class AbstractWebViewComponent extends AbstractViewComponent imp @Override public boolean hasChildren() { - return !this.getChildRenderers().isEmpty(); + return !this.getChildren().isEmpty(); } @Override - public void setChildRenderers(List children) { + public void setChildren(List children) { this.childRenderers = children; } @Override public void renderChildren() { - for (final var childRenderer : this.getChildRenderers()) { + for (final var childRenderer : this.getChildren()) { try { - if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) { - this.getContext().beforeComponentRender(childComponentRenderer.getComponent()); - } childRenderer.render(this); } catch (Exception e) { throw new ChildRenderException(e); - } finally { - if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) { - this.getContext().afterComponentRender(childComponentRenderer.getComponent()); - } } } } @Override public void renderTo(Writer out) throws IOException { - final WebViewComponentWriter webWriter = new DefaultWebViewComponentWriter(out); + final ComponentWriter webWriter = new DefaultComponentWriter(out); final Closure renderer = this.getTemplate().getRenderer(); renderer.setDelegate(this); renderer.setResolveStrategy(Closure.DELEGATE_FIRST); diff --git a/web-views/src/main/java/groowt/view/web/WebViewChildComponentRenderer.java b/web-views/src/main/java/groowt/view/web/WebViewChildComponentRenderer.java deleted file mode 100644 index 12fb166..0000000 --- a/web-views/src/main/java/groowt/view/web/WebViewChildComponentRenderer.java +++ /dev/null @@ -1,19 +0,0 @@ -package groowt.view.web; - -import groovy.lang.Closure; -import groowt.view.component.ViewComponent; - -public non-sealed class WebViewChildComponentRenderer extends WebViewChildRenderer { - - private final ViewComponent component; - - public WebViewChildComponentRenderer(ViewComponent component, Closure renderer) { - super(renderer); - this.component = component; - } - - public ViewComponent getComponent() { - return this.component; - } - -} diff --git a/web-views/src/main/java/groowt/view/web/WebViewChildGStringRenderer.java b/web-views/src/main/java/groowt/view/web/WebViewChildGStringRenderer.java deleted file mode 100644 index cb67db7..0000000 --- a/web-views/src/main/java/groowt/view/web/WebViewChildGStringRenderer.java +++ /dev/null @@ -1,23 +0,0 @@ -package groowt.view.web; - -import groovy.lang.Closure; -import groovy.lang.GString; - -public non-sealed class WebViewChildGStringRenderer extends WebViewChildRenderer { - - private final GString gString; - - public WebViewChildGStringRenderer(GString gString, Closure renderer) { - super(renderer); - this.gString = gString; - } - - public GString getGString() { - return this.gString; - } - - public String getContent() { - return this.gString.toString(); - } - -} diff --git a/web-views/src/main/java/groowt/view/web/WebViewChildJStringRenderer.java b/web-views/src/main/java/groowt/view/web/WebViewChildJStringRenderer.java deleted file mode 100644 index 1e11427..0000000 --- a/web-views/src/main/java/groowt/view/web/WebViewChildJStringRenderer.java +++ /dev/null @@ -1,18 +0,0 @@ -package groowt.view.web; - -import groovy.lang.Closure; - -public non-sealed class WebViewChildJStringRenderer extends WebViewChildRenderer { - - private final String content; - - public WebViewChildJStringRenderer(String content, Closure renderer) { - super(renderer); - this.content = content; - } - - public String getContent() { - return this.content; - } - -} diff --git a/web-views/src/main/java/groowt/view/web/WebViewChildRenderer.java b/web-views/src/main/java/groowt/view/web/WebViewChildRenderer.java deleted file mode 100644 index c7bbfec..0000000 --- a/web-views/src/main/java/groowt/view/web/WebViewChildRenderer.java +++ /dev/null @@ -1,22 +0,0 @@ -package groowt.view.web; - -import groovy.lang.Closure; -import groowt.view.component.ViewComponent; - -public sealed abstract class WebViewChildRenderer permits WebViewChildComponentRenderer, - WebViewChildGStringRenderer, - WebViewChildJStringRenderer { - - private final Closure renderer; - - public WebViewChildRenderer(Closure renderer) { - this.renderer = renderer; - } - - public void render(ViewComponent parent) { - this.renderer.setDelegate(parent); - this.renderer.setResolveStrategy(Closure.DELEGATE_FIRST); - this.renderer.call(); - } - -} diff --git a/web-views/src/main/java/groowt/view/web/WebViewComponent.java b/web-views/src/main/java/groowt/view/web/WebViewComponent.java index dcc98d4..654fa34 100644 --- a/web-views/src/main/java/groowt/view/web/WebViewComponent.java +++ b/web-views/src/main/java/groowt/view/web/WebViewComponent.java @@ -1,24 +1,45 @@ package groowt.view.web; +import groovy.lang.GString; import groowt.view.component.ViewComponent; import java.util.List; public interface WebViewComponent extends ViewComponent { - List getChildRenderers(); + List getChildren(); boolean hasChildren(); - void setChildRenderers(List children); + void setChildren(List children); void renderChildren(); - default List getChildren() { - return this.getChildRenderers().stream() - .map(childRenderer -> switch (childRenderer) { - case WebViewChildComponentRenderer componentRenderer -> componentRenderer.getComponent(); - case WebViewChildGStringRenderer gStringRenderer -> gStringRenderer.getGString(); - case WebViewChildJStringRenderer jStringRenderer -> jStringRenderer.getContent(); + default List getChildStrings() { + return this.getChildren().stream() + .map(WebViewComponentChild::getChild) + .filter(obj -> obj instanceof String || obj instanceof GString) + .map(obj -> { + if (obj instanceof String s) { + return s; + } else { + return ((GString) obj).toString(); + } }) .toList(); } + default List getChildGStrings() { + return this.getChildren().stream() + .map(WebViewComponentChild::getChild) + .filter(GString.class::isInstance) + .map(GString.class::cast) + .toList(); + } + + default List getChildComponents() { + return this.getChildren().stream() + .map(WebViewComponentChild::getChild) + .filter(WebViewComponent.class::isInstance) + .map(WebViewComponent.class::cast) + .toList(); + } + } diff --git a/web-views/src/main/java/groowt/view/web/WebViewComponentBugError.java b/web-views/src/main/java/groowt/view/web/WebViewComponentBugError.java new file mode 100644 index 0000000..7c3122e --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/WebViewComponentBugError.java @@ -0,0 +1,31 @@ +package groowt.view.web; + +public class WebViewComponentBugError extends RuntimeException { + + public WebViewComponentBugError(String message) { + super(message); + } + + public WebViewComponentBugError(String message, Throwable cause) { + super(message, cause); + } + + public WebViewComponentBugError(Throwable cause) { + super(cause); + } + + public WebViewComponentBugError( + String message, + Throwable cause, + boolean enableSuppression, + boolean writableStackTrace + ) { + super(message, cause, enableSuppression, writableStackTrace); + } + + @Override + public String getMessage() { + return "BUG! Please file an issue at the github repository. " + super.getMessage(); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/WebViewComponentChild.java b/web-views/src/main/java/groowt/view/web/WebViewComponentChild.java new file mode 100644 index 0000000..0d415dd --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/WebViewComponentChild.java @@ -0,0 +1,64 @@ +package groowt.view.web; + +import groovy.lang.Closure; +import groowt.view.component.ComponentTemplate; +import groowt.view.component.ViewComponent; +import groowt.view.component.runtime.ComponentWriter; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public class WebViewComponentChild { + + private static final class ChildRenderClosure extends Closure { + + private final ViewComponent parent; + private final @Nullable Object child; + + public ChildRenderClosure(ComponentTemplate template, ViewComponent parent, @Nullable Object child) { + super(template, template); + this.parent = parent; + this.child = child; + this.setDelegate(this.parent); + this.setResolveStrategy(Closure.DELEGATE_FIRST); + } + + public ViewComponent getParent() { + return this.parent; + } + + public void doCall(ComponentWriter writer) { + writer.append(Objects.requireNonNull(this.child)); + } + + public void doCall(ComponentWriter writer, Object givenChild) { + writer.append(givenChild); + } + + } + + private final ComponentTemplate template; + private final ComponentWriter out; + private final Object child; + + public WebViewComponentChild(ComponentTemplate template, ComponentWriter out, Object child) { + this.template = template; + this.out = out; + this.child = child; + } + + public Object getChild() { + return this.child; + } + + public void render(ViewComponent parent) { + final var cl = this.getRenderer(parent); + cl.call(this.child); + } + + public Closure getRenderer(ViewComponent parent) { + final var cl = new ChildRenderClosure(this.template, parent, null); + return cl.curry(this.out); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/WebViewComponentContext.java b/web-views/src/main/java/groowt/view/web/WebViewComponentContext.java index bdcfdc8..422e6f4 100644 --- a/web-views/src/main/java/groowt/view/web/WebViewComponentContext.java +++ b/web-views/src/main/java/groowt/view/web/WebViewComponentContext.java @@ -1,16 +1,5 @@ package groowt.view.web; -import groovy.lang.Closure; import groowt.view.component.context.ComponentContext; -import groowt.view.component.ViewComponent; -import org.jetbrains.annotations.ApiStatus; -public interface WebViewComponentContext extends ComponentContext { - - /** - * For use only by compiled web view component templates. - */ - @ApiStatus.Internal - ViewComponent createFragment(Closure childCollector); - -} +public interface WebViewComponentContext extends ComponentContext {} diff --git a/web-views/src/main/java/groowt/view/web/antlr/AbstractWebViewComponentsLexer.java b/web-views/src/main/java/groowt/view/web/antlr/AbstractWebViewComponentsLexer.java index ef7a012..5e3ef10 100644 --- a/web-views/src/main/java/groowt/view/web/antlr/AbstractWebViewComponentsLexer.java +++ b/web-views/src/main/java/groowt/view/web/antlr/AbstractWebViewComponentsLexer.java @@ -75,11 +75,11 @@ public abstract class AbstractWebViewComponentsLexer extends Lexer { @Override public void pushMode(int m) { - if (logger.isDebugEnabled()) { + if (logger.isTraceEnabled()) { final var old = this._mode; super.pushMode(m); final var delta = this._mode; - logger.debug("pushMode: target {} to {}", this.getModeName(old), this.getModeName(delta)); + logger.trace("pushMode: target {} to {}", this.getModeName(old), this.getModeName(delta)); } else { super.pushMode(m); } @@ -87,10 +87,10 @@ public abstract class AbstractWebViewComponentsLexer extends Lexer { @Override public int popMode() { - if (logger.isDebugEnabled()) { + if (logger.isTraceEnabled()) { final var popped = this._mode; final var delta = super.popMode(); - logger.debug("popMode: to {} target {}", this.getModeName(popped), this.getModeName(delta)); + logger.trace("popMode: to {} target {}", this.getModeName(popped), this.getModeName(delta)); return popped; } else { return super.popMode(); @@ -273,4 +273,4 @@ public abstract class AbstractWebViewComponentsLexer extends Lexer { return this.debugHookReturning(name, msgFunction, true); } -} \ No newline at end of file +} diff --git a/web-views/src/main/java/groowt/view/web/antlr/TokenUtil.kt b/web-views/src/main/java/groowt/view/web/antlr/TokenUtil.kt index 538d3bf..470a456 100644 --- a/web-views/src/main/java/groowt/view/web/antlr/TokenUtil.kt +++ b/web-views/src/main/java/groowt/view/web/antlr/TokenUtil.kt @@ -32,6 +32,16 @@ fun shortFormatToken(token: Token): String = fun formatTokenText(text: String): String = excerptTokenParts(escapeTokenPartsToList(text)) +fun formatTokenPosition(token: Token): String { + return "line ${token.line}, column ${token.charPositionInLine + 1}" +} + +fun excerptToken(token: Token) = excerptToken(token, 30, 7, "...") + +fun excerptToken(token: Token, startLength: Int = 30, endLength: Int = 7, separator: String = "..."): String { + return excerptTokenParts(escapeTokenPartsToList(token.text), startLength, endLength, separator) +} + fun excerptTokenParts( parts: List, startLength: Int = 30, diff --git a/web-views/src/main/java/groowt/view/web/ast/node/ClassComponentTypeNode.java b/web-views/src/main/java/groowt/view/web/ast/node/ClassComponentTypeNode.java index 01c8578..da8403f 100644 --- a/web-views/src/main/java/groowt/view/web/ast/node/ClassComponentTypeNode.java +++ b/web-views/src/main/java/groowt/view/web/ast/node/ClassComponentTypeNode.java @@ -25,6 +25,7 @@ public non-sealed class ClassComponentTypeNode extends ComponentTypeNode { this.fqn = Objects.requireNonNull(fullyQualifiedName); } + @Deprecated public String getFullyQualifiedName() { return Objects.requireNonNullElse(this.fqn, this.getIdentifier()); } diff --git a/web-views/src/main/java/groowt/view/web/compiler/AnonymousWebViewComponent.java b/web-views/src/main/java/groowt/view/web/compiler/AnonymousWebViewComponent.java new file mode 100644 index 0000000..0dfa48a --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/compiler/AnonymousWebViewComponent.java @@ -0,0 +1,49 @@ +package groowt.view.web.compiler; + +import groowt.view.component.context.ComponentContext; +import groowt.view.web.WebViewComponentChild; +import groowt.view.web.WebViewComponent; +import org.jetbrains.annotations.ApiStatus; + +import java.io.Writer; +import java.util.List; + +@ApiStatus.Internal +public final class AnonymousWebViewComponent implements WebViewComponent { + + @Override + public void setContext(ComponentContext context) { + throw new UnsupportedOperationException(); + } + + @Override + public ComponentContext getContext() { + throw new UnsupportedOperationException(); + } + + @Override + public void renderTo(Writer writer) { + throw new UnsupportedOperationException(); + } + + @Override + public List getChildren() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasChildren() { + throw new UnsupportedOperationException(); + } + + @Override + public void setChildren(List children) { + throw new UnsupportedOperationException(); + } + + @Override + public void renderChildren() { + throw new UnsupportedOperationException(); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/compiler/DefaultWebViewComponentTemplateCompiler.java b/web-views/src/main/java/groowt/view/web/compiler/DefaultWebViewComponentTemplateCompiler.java index 15d5a1a..ca66c36 100644 --- a/web-views/src/main/java/groowt/view/web/compiler/DefaultWebViewComponentTemplateCompiler.java +++ b/web-views/src/main/java/groowt/view/web/compiler/DefaultWebViewComponentTemplateCompiler.java @@ -1,201 +1,114 @@ package groowt.view.web.compiler; -import groovy.lang.GroovyClassLoader; -import groowt.view.component.AbstractViewComponent; -import groowt.view.component.ComponentTemplate; -import groowt.view.component.ViewComponent; -import groowt.view.component.compiler.CachingComponentTemplateCompiler; -import groowt.view.component.compiler.ComponentTemplateCompileErrorException; -import groowt.view.component.context.ComponentContext; -import groowt.view.component.factory.ComponentTemplateSource; +import groowt.view.component.compiler.*; +import groowt.view.web.WebViewComponentBugError; import groowt.view.web.analysis.MismatchedComponentTypeAnalysis; import groowt.view.web.analysis.MismatchedComponentTypeError; -import groowt.view.web.antlr.AntlrUtil; -import groowt.view.web.antlr.CompilationUnitParseResult; -import groowt.view.web.antlr.ParserUtil; -import groowt.view.web.antlr.TokenList; +import groowt.view.web.antlr.*; import groowt.view.web.ast.DefaultAstBuilder; import groowt.view.web.ast.DefaultNodeFactory; import groowt.view.web.ast.node.CompilationUnitNode; import groowt.view.web.transpile.DefaultGroovyTranspiler; -import groowt.view.web.transpile.DefaultTranspilerConfiguration; -import groowt.view.web.util.SourcePosition; import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.tree.TerminalNode; import org.antlr.v4.runtime.tree.Tree; import org.codehaus.groovy.control.CompilationFailedException; -import org.codehaus.groovy.control.CompilationUnit; -import org.codehaus.groovy.control.CompilerConfiguration; -import org.codehaus.groovy.control.Phases; -import org.codehaus.groovy.control.io.AbstractReaderSource; +import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.tools.GroovyClass; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; -import java.io.IOException; import java.io.Reader; -import java.io.Writer; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; -public class DefaultWebViewComponentTemplateCompiler extends CachingComponentTemplateCompiler +public class DefaultWebViewComponentTemplateCompiler + extends CachingComponentTemplateCompiler implements WebViewComponentTemplateCompiler { - protected static final class AnonymousWebViewComponent extends AbstractViewComponent { + private final ComponentTemplateCompilerConfiguration configuration; - // DO NOT INSTANTIATE, this is merely a marker class - private AnonymousWebViewComponent() { - throw new UnsupportedOperationException(); - } - - @Override - public void setContext(ComponentContext context) { - throw new UnsupportedOperationException(); - } - - @Override - public ComponentContext getContext() { - throw new UnsupportedOperationException(); - } - - @Override - protected ComponentTemplate getTemplate() { - throw new UnsupportedOperationException(); - } - - @Override - protected void setTemplate(ComponentTemplate template) { - throw new UnsupportedOperationException(); - } - - @Override - protected void beforeRender() { - throw new UnsupportedOperationException(); - } - - @Override - protected void afterRender() { - throw new UnsupportedOperationException(); - } - - @Override - public void renderTo(Writer out) { - throw new UnsupportedOperationException(); - } - - @Override - protected Class getSelfClass() { - throw new UnsupportedOperationException(); - } - - } - - private final CompilerConfiguration configuration; - private final String defaultPackageName; - private final int phase; - - public DefaultWebViewComponentTemplateCompiler( - GroovyClassLoader groovyClassLoader, - CompilerConfiguration configuration, - String defaultPackageName - ) { - this(groovyClassLoader, configuration, defaultPackageName, Phases.CLASS_GENERATION); - } - - @ApiStatus.Internal - public DefaultWebViewComponentTemplateCompiler( - GroovyClassLoader groovyClassLoader, - CompilerConfiguration configuration, - String defaultPackageName, - int phase - ) { - super(groovyClassLoader); + public DefaultWebViewComponentTemplateCompiler(ComponentTemplateCompilerConfiguration configuration) { this.configuration = configuration; - this.defaultPackageName = defaultPackageName; - this.phase = phase; } protected WebViewComponentTemplateCompileException getException( - TerminalNode terminalNode, - Class forClass, - Reader reader + WebViewComponentTemplateCompileUnit compileUnit, + TerminalNode terminalNode ) { final Token offending = terminalNode.getSymbol(); - return new WebViewComponentTemplateCompileException( - "Compile error on token at " + SourcePosition.fromStartOfToken(offending).toStringLong() + ".", - forClass, - reader, - offending + final var exception = new WebViewComponentTemplateCompileException( + compileUnit, "Invalid token '" + TokenUtil.excerptToken(offending) + "'." ); + exception.setTerminalNode(terminalNode); + return exception; } protected WebViewComponentTemplateCompileException getException( - ParserRuleContext parserRuleContext, - Class forClass, - Reader reader + WebViewComponentTemplateCompileUnit compileUnit, + ParserRuleContext parserRuleContext ) { - return new WebViewComponentTemplateCompileException( - "Compile error at " + SourcePosition.fromStartOfToken(parserRuleContext.getStart()).toStringLong() - + ".", - forClass, - reader, - parserRuleContext + final var exception = new WebViewComponentTemplateCompileException( + compileUnit, + "Parser error: " + parserRuleContext.exception.getMessage(), + parserRuleContext.exception ); + exception.setParserRuleContext(parserRuleContext); + return exception; } - protected WebViewComponentTemplateCompileException mapToErrorException( - Tree tree, - Class forClass, - Reader reader + protected WebViewComponentTemplateCompileException getException( + WebViewComponentTemplateCompileUnit compileUnit, + Tree tree ) { - if (tree instanceof TerminalNode terminalNode) { - return getException(terminalNode, forClass, reader); - } else if (tree instanceof ParserRuleContext parserRuleContext) { - return getException(parserRuleContext, forClass, reader); + if (tree instanceof ParserRuleContext parserRuleContext) { + return getException(compileUnit, parserRuleContext); + } else if (tree instanceof TerminalNode terminalNode) { + return getException(compileUnit, terminalNode); } else { return new WebViewComponentTemplateCompileException( - "Compile error with " + tree + ".", - forClass, - reader, - tree + compileUnit, + "Error at parser/lexer node " + tree.toString() ); } } - protected WebViewComponentTemplateCompileException mapToErrorException( - MismatchedComponentTypeError error, - Class forClass, - Reader reader + protected WebViewComponentTemplateCompileException getException( + WebViewComponentTemplateCompileUnit compileUnit, + MismatchedComponentTypeError error ) { - return new WebViewComponentTemplateCompileException( - error.getMessage(), - forClass, - reader, - error.getComponent() + final var exception = new WebViewComponentTemplateCompileException( + compileUnit, + error.getMessage() ); + exception.setParserRuleContext(error.getComponent()); + return exception; } - protected ComponentTemplateCompileResult doCompile( - Class forClass, - Reader reader, - @Nullable URI uri - ) throws ComponentTemplateCompileErrorException { - final CompilationUnitParseResult parseResult = ParserUtil.parseCompilationUnit(reader); + @Override + protected ComponentTemplateCompileResult doCompile(WebViewComponentTemplateCompileUnit compileUnit) + throws ComponentTemplateCompileException { + + final Reader sourceReader; + try { + sourceReader = compileUnit.getSource().toReader(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + final CompilationUnitParseResult parseResult = ParserUtil.parseCompilationUnit(sourceReader); // check for parser/lexer errors final var parseErrors = AntlrUtil.findErrorNodes(parseResult.getCompilationUnitContext()); if (!parseErrors.isEmpty()) { if (parseErrors.getErrorCount() == 1) { final var errorNode = parseErrors.getAll().getFirst(); - throw mapToErrorException(errorNode, forClass, reader); + throw getException(compileUnit, errorNode); } else { final var errorExceptions = parseErrors.getAll().stream() - .map(errorNode -> mapToErrorException(errorNode, forClass, reader)) + .map(errorNode -> getException(compileUnit, errorNode)) .toList(); - throw new MultipleWebViewComponentCompileErrorsException(errorExceptions, forClass, reader); + throw new MultipleWebViewComponentCompileErrorsException(compileUnit, errorExceptions); } } @@ -205,16 +118,12 @@ public class DefaultWebViewComponentTemplateCompiler extends CachingComponentTem if (!mismatchedComponentTypeErrors.isEmpty()) { if (mismatchedComponentTypeErrors.size() == 1) { - throw mapToErrorException(mismatchedComponentTypeErrors.getFirst(), forClass, reader); + throw getException(compileUnit, mismatchedComponentTypeErrors.getFirst()); } else { final var errorExceptions = mismatchedComponentTypeErrors.stream() - .map(error -> mapToErrorException(error, forClass, reader)) + .map(error -> getException(compileUnit, error)) .toList(); - throw new MultipleWebViewComponentCompileErrorsException( - errorExceptions, - forClass, - reader - ); + throw new MultipleWebViewComponentCompileErrorsException(compileUnit, errorExceptions); } } @@ -224,63 +133,39 @@ public class DefaultWebViewComponentTemplateCompiler extends CachingComponentTem final var cuNode = (CompilationUnitNode) astBuilder.build(parseResult.getCompilationUnitContext()); // transpile to Groovy - final var groovyCompilationUnit = new CompilationUnit(this.configuration); - final var transpiler = new DefaultGroovyTranspiler( - groovyCompilationUnit, - this.defaultPackageName, - DefaultTranspilerConfiguration::new + final var transpiler = new DefaultGroovyTranspiler(); + + final var ownerComponentName = compileUnit.getForClass() != AnonymousWebViewComponent.class + ? compileUnit.getForClass().getSimpleName() + : "AnonymousWebViewComponent" + System.nanoTime(); + final var templateClassSimpleName = ownerComponentName + "Template"; + + final SourceUnit sourceUnit = transpiler.transpile( + this.configuration, + compileUnit, + cuNode, + templateClassSimpleName ); + compileUnit.getGroovyCompilationUnit().addSource(sourceUnit); - final var ownerComponentName = forClass != null ? forClass.getSimpleName() : "AnonymousComponent" + System.nanoTime(); - final var templateClassName = ownerComponentName + "Template"; - final var fqn = this.defaultPackageName + "." + templateClassName; - - final var readerSource = new AbstractReaderSource(this.configuration) { - - @Override - public Reader getReader() throws IOException { - reader.reset(); - return reader; - } - - @Override - public @Nullable URI getURI() { - return uri; - } - - @Override - public void cleanup() { - super.cleanup(); - try { - reader.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - }; - - transpiler.transpile(cuNode, tokenList, ownerComponentName, readerSource); - + // compile groovy try { - groovyCompilationUnit.compile(this.phase); + compileUnit.getGroovyCompilationUnit().compile(this.configuration.getToCompilePhase().getPhaseNumber()); } catch (CompilationFailedException compilationFailedException) { throw new WebViewComponentTemplateCompileException( - "Error while compiling Groovy in " + templateClassName + " for component class " + - forClass.getName() + ".", - compilationFailedException, - forClass, - forClass, - reader + compileUnit, + "Error while compiling Groovy.", + compilationFailedException ); } // get the classes - final var allClasses = groovyCompilationUnit.getClasses(); + final var allClasses = compileUnit.getGroovyCompilationUnit().getClasses(); GroovyClass templateGroovyClass = null; - final List otherClasses = new ArrayList<>(); + final Set otherClasses = new HashSet<>(); + final String templateClassFqn = sourceUnit.getAST().getPackageName() + "." + templateClassSimpleName; for (final GroovyClass groovyClass : allClasses) { - if (groovyClass.getName().equals(fqn)) { + if (groovyClass.getName().equals(templateClassFqn)) { if (templateGroovyClass != null) { throw new IllegalStateException("Already found a templateGroovyClass."); } @@ -291,40 +176,13 @@ public class DefaultWebViewComponentTemplateCompiler extends CachingComponentTem } if (templateGroovyClass == null) { - throw new IllegalStateException("Did not find templateClass"); + throw new WebViewComponentBugError(new IllegalStateException("Did not find templateClass")); } - return new ComponentTemplateCompileResult(templateGroovyClass, otherClasses); - } - - @Override - protected ComponentTemplateCompileResult doCompile( - @Nullable ComponentTemplateSource source, - Class forClass, - Reader sourceReader - ) throws ComponentTemplateCompileErrorException { - if (source instanceof ComponentTemplateSource.URISource uriSource) { - return this.doCompile(forClass, sourceReader, uriSource.templateURI()); - } else if (source instanceof ComponentTemplateSource.URLSource urlSource) { - try { - return this.doCompile(forClass, sourceReader, urlSource.templateURL().toURI()); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - } else { - return this.doCompile(forClass, sourceReader, null); - } - } - - @Override - public ComponentTemplateCompileResult compileAnonymous(ComponentTemplateSource source) - throws ComponentTemplateCompileErrorException { - return this.compile(AnonymousWebViewComponent.class, source); - } - - @Override - public ComponentTemplate compileAndGetAnonymous(ComponentTemplateSource source) throws ComponentTemplateCompileErrorException { - return this.compileAndGet(AnonymousWebViewComponent.class, source); + return new SimpleComponentTemplateCompileResult( + templateGroovyClass, + otherClasses + ); } } diff --git a/web-views/src/main/java/groowt/view/web/compiler/MultipleWebViewComponentCompileErrorsException.java b/web-views/src/main/java/groowt/view/web/compiler/MultipleWebViewComponentCompileErrorsException.java index 186ffdd..16d0487 100644 --- a/web-views/src/main/java/groowt/view/web/compiler/MultipleWebViewComponentCompileErrorsException.java +++ b/web-views/src/main/java/groowt/view/web/compiler/MultipleWebViewComponentCompileErrorsException.java @@ -1,31 +1,22 @@ package groowt.view.web.compiler; -import groowt.view.component.ViewComponent; -import groowt.view.component.compiler.ComponentTemplateCompileErrorException; +import groowt.view.component.compiler.ComponentTemplateCompileException; +import groowt.view.component.compiler.ComponentTemplateCompileUnit; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.ArrayList; import java.util.List; -public class MultipleWebViewComponentCompileErrorsException extends ComponentTemplateCompileErrorException { +public class MultipleWebViewComponentCompileErrorsException extends ComponentTemplateCompileException { private final List errors = new ArrayList<>(); public MultipleWebViewComponentCompileErrorsException( - String message, - List errors, - Class forClass, - Object templateSource + ComponentTemplateCompileUnit compileUnit, + List errors ) { - super(message, forClass, templateSource); - this.errors.addAll(errors); - } - - public MultipleWebViewComponentCompileErrorsException( - List errors, - Class forClass, - Object templateSource - ) { - super(forClass, templateSource); + super(compileUnit, "There were multiple errors during compilation."); this.errors.addAll(errors); } @@ -33,4 +24,17 @@ public class MultipleWebViewComponentCompileErrorsException extends ComponentTem return this.errors; } + @Override + public String getMessage() { + final var sw = new StringWriter(); + sw.append(super.getMessage()).append("\n\n"); + for (int i = 0; i < this.errors.size(); i++) { + final var error = this.errors.get(i); + sw.append(String.format("Error no. %d:\n", i + 1)); + error.printStackTrace(new PrintWriter(sw)); + sw.append("\n"); + } + return sw.toString(); + } + } diff --git a/web-views/src/main/java/groowt/view/web/compiler/WebViewComponentTemplateCompileException.java b/web-views/src/main/java/groowt/view/web/compiler/WebViewComponentTemplateCompileException.java index 02ad2d2..f7b158b 100644 --- a/web-views/src/main/java/groowt/view/web/compiler/WebViewComponentTemplateCompileException.java +++ b/web-views/src/main/java/groowt/view/web/compiler/WebViewComponentTemplateCompileException.java @@ -1,56 +1,75 @@ package groowt.view.web.compiler; -import groowt.view.component.ViewComponent; -import groowt.view.component.compiler.ComponentTemplateCompileErrorException; +import groowt.view.component.compiler.ComponentTemplateCompileException; +import groowt.view.component.compiler.ComponentTemplateCompileUnit; +import groowt.view.web.antlr.TokenUtil; import groowt.view.web.ast.node.Node; -import groowt.view.web.util.SourcePosition; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.jetbrains.annotations.Nullable; -public class WebViewComponentTemplateCompileException extends ComponentTemplateCompileErrorException { +public class WebViewComponentTemplateCompileException extends ComponentTemplateCompileException { - private final Object node; + private @Nullable TerminalNode terminalNode; + private @Nullable ParserRuleContext parserRuleContext; + private @Nullable Node node; public WebViewComponentTemplateCompileException( + ComponentTemplateCompileUnit compileUnit, + String message + ) { + super(compileUnit, message); + } + + public WebViewComponentTemplateCompileException( + ComponentTemplateCompileUnit compileUnit, String message, - Class forClass, - Object templateSource, - Object node + Throwable cause ) { - super(message, forClass, templateSource); - this.node = node; + super(compileUnit, message, cause); } public WebViewComponentTemplateCompileException( - String message, - Throwable cause, - Class forClass, - Object templateSource, - Object node + ComponentTemplateCompileUnit compileUnit, + Throwable cause ) { - super(message, cause, forClass, templateSource); - this.node = node; + super(compileUnit, "There was an error during compilation.", cause); } - public WebViewComponentTemplateCompileException( - Throwable cause, - Class forClass, - Object templateSource, - Object node - ) { - super(cause, forClass, templateSource); - this.node = node; + public @Nullable TerminalNode getTerminalNode() { + return this.terminalNode; } - public Object getNode() { + public void setTerminalNode(@Nullable TerminalNode terminalNode) { + this.terminalNode = terminalNode; + } + + public @Nullable ParserRuleContext getParserRuleContext() { + return this.parserRuleContext; + } + + public void setParserRuleContext(@Nullable ParserRuleContext parserRuleContext) { + this.parserRuleContext = parserRuleContext; + } + + public @Nullable Node getNode() { return this.node; } + public void setNode(@Nullable Node node) { + this.node = node; + } + @Override - public String getMessage() { - if (this.node instanceof Node asNode) { - final SourcePosition start = asNode.getTokenRange().getStartPosition(); - return "At " + start.toStringLong() + ": " + super.getMessage(); + protected @Nullable String getPosition() { + if (this.node != null) { + return this.node.getTokenRange().getStartPosition().toStringLong(); + } else if (this.parserRuleContext != null) { + return TokenUtil.formatTokenPosition(this.parserRuleContext.start); + } else if (this.terminalNode != null) { + return TokenUtil.formatTokenPosition(terminalNode.getSymbol()); } else { - return super.getMessage(); + return null; } } diff --git a/web-views/src/main/java/groowt/view/web/compiler/WebViewComponentTemplateCompileUnit.java b/web-views/src/main/java/groowt/view/web/compiler/WebViewComponentTemplateCompileUnit.java new file mode 100644 index 0000000..b9ebcd9 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/compiler/WebViewComponentTemplateCompileUnit.java @@ -0,0 +1,86 @@ +package groowt.view.web.compiler; + +import groowt.view.component.ViewComponent; +import groowt.view.component.compiler.AbstractComponentTemplateCompileUnit; +import groowt.view.component.compiler.ComponentTemplateCompileException; +import groowt.view.component.compiler.ComponentTemplateCompileResult; +import groowt.view.component.compiler.ComponentTemplateCompilerConfiguration; +import groowt.view.component.compiler.source.ComponentTemplateSource; +import groowt.view.component.compiler.source.FileSource; +import groowt.view.component.compiler.source.URISource; +import groowt.view.component.compiler.source.URLSource; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.Janitor; +import org.codehaus.groovy.control.io.ReaderSource; +import org.jetbrains.annotations.Nullable; + +import java.io.Reader; +import java.net.URI; + +public class WebViewComponentTemplateCompileUnit extends AbstractComponentTemplateCompileUnit implements ReaderSource { + + private final String defaultPackageName; + private final CompilationUnit groovyCompilationUnit = new CompilationUnit(); + + public WebViewComponentTemplateCompileUnit( + Class forClass, + ComponentTemplateSource source, + String defaultPackageName + ) { + super(forClass, source); + this.defaultPackageName = defaultPackageName; + } + + @Override + public String getDefaultPackageName() { + return this.defaultPackageName; + } + + public CompilationUnit getGroovyCompilationUnit() { + return this.groovyCompilationUnit; + } + + @Override + public ComponentTemplateCompileResult compile(ComponentTemplateCompilerConfiguration configuration) + throws ComponentTemplateCompileException { + final var compiler = new DefaultWebViewComponentTemplateCompiler(configuration); + return compiler.compile(this); + } + + @Override + public Reader getReader() { + try { + return this.getSource().toReader(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean canReopenSource() { + return this.getSource().canReopen(); + } + + @Override + public @Nullable String getLine(int lineNumber, Janitor janitor) { + if (this.getSource().canReopen()) { + return this.getSource().getLines().get(lineNumber); + } else { + return null; + } + } + + @Override + public void cleanup() {} + + @Override + public @Nullable URI getURI() { + return switch (this.getSource()) { + case FileSource fileSource -> fileSource.getURI(); + case URISource uriSource -> uriSource.getURI(); + case URLSource urlSource -> urlSource.getURI(); + default -> null; + }; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/compiler/WebViewComponentTemplateCompiler.java b/web-views/src/main/java/groowt/view/web/compiler/WebViewComponentTemplateCompiler.java index 3f713cf..6ca2cef 100644 --- a/web-views/src/main/java/groowt/view/web/compiler/WebViewComponentTemplateCompiler.java +++ b/web-views/src/main/java/groowt/view/web/compiler/WebViewComponentTemplateCompiler.java @@ -1,11 +1,20 @@ package groowt.view.web.compiler; -import groowt.view.component.ComponentTemplate; -import groowt.view.component.compiler.ComponentTemplateCompileErrorException; +import groowt.view.component.compiler.ComponentTemplateCompileException; +import groowt.view.component.compiler.ComponentTemplateCompileResult; import groowt.view.component.compiler.ComponentTemplateCompiler; -import groowt.view.component.factory.ComponentTemplateSource; +import groowt.view.component.compiler.source.ComponentTemplateSource; + +public interface WebViewComponentTemplateCompiler + extends ComponentTemplateCompiler { + + default ComponentTemplateCompileResult compileAnonymous(ComponentTemplateSource source, String packageName) + throws ComponentTemplateCompileException { + return this.compile(new WebViewComponentTemplateCompileUnit( + AnonymousWebViewComponent.class, + source, + packageName + )); + } -public interface WebViewComponentTemplateCompiler extends ComponentTemplateCompiler { - ComponentTemplateCompileResult compileAnonymous(ComponentTemplateSource source) throws ComponentTemplateCompileErrorException; - ComponentTemplate compileAndGetAnonymous(ComponentTemplateSource source) throws ComponentTemplateCompileErrorException; } diff --git a/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentChildCollection.java b/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentChildCollection.java deleted file mode 100644 index eaafb9d..0000000 --- a/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentChildCollection.java +++ /dev/null @@ -1,38 +0,0 @@ -package groowt.view.web.runtime; - -import groovy.lang.Closure; -import groovy.lang.GString; -import groowt.view.component.ViewComponent; -import groowt.view.web.WebViewChildComponentRenderer; -import groowt.view.web.WebViewChildGStringRenderer; -import groowt.view.web.WebViewChildJStringRenderer; -import groowt.view.web.WebViewChildRenderer; - -import java.util.ArrayList; -import java.util.List; - -public class DefaultWebViewComponentChildCollection implements WebViewComponentChildCollection { - - private final List children = new ArrayList<>(); - - @Override - public void add(String jString, Closure renderer) { - this.children.add(new WebViewChildJStringRenderer(jString, renderer)); - } - - @Override - public void add(GString gString, Closure renderer) { - this.children.add(new WebViewChildGStringRenderer(gString, renderer)); - } - - @Override - public void add(ViewComponent component, Closure renderer) { - this.children.add(new WebViewChildComponentRenderer(component, renderer)); - } - - @Override - public List getChildren() { - return this.children; - } - -} diff --git a/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentChildCollector.java b/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentChildCollector.java new file mode 100644 index 0000000..63e634b --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentChildCollector.java @@ -0,0 +1,43 @@ +package groowt.view.web.runtime; + +import groovy.lang.GString; +import groowt.view.component.ComponentTemplate; +import groowt.view.component.ViewComponent; +import groowt.view.component.runtime.ComponentWriter; +import groowt.view.web.WebViewComponentChild; + +import java.util.ArrayList; +import java.util.List; + +public class DefaultWebViewComponentChildCollector implements WebViewComponentChildCollector { + + private final ComponentTemplate template; + private final ComponentWriter out; + private final List children = new ArrayList<>(); + + public DefaultWebViewComponentChildCollector(ComponentTemplate template, ComponentWriter out) { + this.template = template; + this.out = out; + } + + @Override + public void add(String jString) { + this.children.add(new WebViewComponentChild(this.template, this.out, jString)); + } + + @Override + public void add(GString gString) { + this.children.add(new WebViewComponentChild(this.template, this.out, gString)); + } + + @Override + public void add(ViewComponent component) { + this.children.add(new WebViewComponentChild(this.template, this.out, component)); + } + + @Override + public List getChildren() { + return this.children; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentRenderContext.java b/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentRenderContext.java new file mode 100644 index 0000000..47839c2 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentRenderContext.java @@ -0,0 +1,41 @@ +package groowt.view.web.runtime; + +import groowt.view.component.ViewComponent; +import groowt.view.component.context.ComponentContext; +import groowt.view.component.runtime.ComponentWriter; +import groowt.view.component.runtime.DefaultRenderContext; +import groowt.view.web.WebViewComponent; +import org.jetbrains.annotations.ApiStatus; + +public class DefaultWebViewComponentRenderContext extends DefaultRenderContext + implements WebViewComponentRenderContext { + + DefaultWebViewComponentRenderContext(ComponentContext componentContext, ComponentWriter writer) { + super(componentContext, writer); + } + + @Override + public ViewComponent create(Resolved resolved, Object... args) { + if (args != null && args.length > 0) { + final Object last = args[args.length - 1]; + if (last instanceof WebViewComponentChildCollectorClosure cl) { + final var childCollector = new DefaultWebViewComponentChildCollector( + cl.getTemplate(), + this.getWriter() + ); + args[args.length - 1] = childCollector; + cl.call(childCollector); + } + } + return super.create(resolved, args); + } + + @ApiStatus.Internal + public ViewComponent createFragment(WebViewComponent fragment, WebViewComponentChildCollectorClosure cl) { + final var childCollection = new DefaultWebViewComponentChildCollector(cl.getTemplate(), this.getWriter()); + cl.call(childCollection); + fragment.setChildren(childCollection.getChildren()); + return fragment; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollection.java b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollection.java deleted file mode 100644 index 84a5e05..0000000 --- a/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollection.java +++ /dev/null @@ -1,20 +0,0 @@ -package groowt.view.web.runtime; - -import groovy.lang.Closure; -import groovy.lang.GString; -import groowt.view.component.ViewComponent; -import groowt.view.web.WebViewChildRenderer; - -import java.util.List; - -public interface WebViewComponentChildCollection { - - void add(String jString, Closure renderer); - - void add(GString gString, Closure renderer); - - void add(ViewComponent component, Closure renderer); - - List getChildren(); - -} diff --git a/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollector.java b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollector.java new file mode 100644 index 0000000..ec197eb --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollector.java @@ -0,0 +1,14 @@ +package groowt.view.web.runtime; + +import groovy.lang.GString; +import groowt.view.component.ViewComponent; +import groowt.view.web.WebViewComponentChild; + +import java.util.List; + +public interface WebViewComponentChildCollector { + void add(String jString); + void add(GString gString); + void add(ViewComponent component); + List getChildren(); +} diff --git a/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollectorClosure.java b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollectorClosure.java new file mode 100644 index 0000000..9c9de82 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollectorClosure.java @@ -0,0 +1,29 @@ +package groowt.view.web.runtime; + +import groovy.lang.Closure; +import groowt.view.component.ComponentTemplate; + +public class WebViewComponentChildCollectorClosure extends Closure { + + public static Closure get(ComponentTemplate template, Closure collectorClosure) { + return new WebViewComponentChildCollectorClosure(template, collectorClosure); + } + + private final ComponentTemplate template; + private final Closure collectorClosure; + + private WebViewComponentChildCollectorClosure(ComponentTemplate template, Closure collectorClosure) { + super(template, template); + this.template = template; + this.collectorClosure = collectorClosure; + } + + public ComponentTemplate getTemplate() { + return this.template; + } + + public Object doCall(WebViewComponentChildCollector childCollector) { + return this.collectorClosure.call(childCollector); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentRenderContext.java b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentRenderContext.java new file mode 100644 index 0000000..4fc34eb --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentRenderContext.java @@ -0,0 +1,14 @@ +package groowt.view.web.runtime; + +import groowt.view.component.ViewComponent; +import groowt.view.component.runtime.RenderContext; +import groowt.view.web.WebViewComponent; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +public interface WebViewComponentRenderContext extends RenderContext { + + @ApiStatus.Internal + ViewComponent createFragment(WebViewComponent fragment, WebViewComponentChildCollectorClosure cl); + +} diff --git a/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentWriter.java b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentWriter.java deleted file mode 100644 index 28a2d75..0000000 --- a/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentWriter.java +++ /dev/null @@ -1,14 +0,0 @@ -package groowt.view.web.runtime; - -import groovy.lang.GString; -import groowt.view.component.ViewComponent; - -public interface WebViewComponentWriter { - void append(String string); - void append(GString gString); - void append(GString gString, int line, int column); - void append(ViewComponent viewComponent); - void append(ViewComponent viewComponent, int line, int column); - void append(Object object); - void leftShift(Object object); -} diff --git a/web-views/src/main/java/groowt/view/web/transpile/AppendOrAddStatementFactory.java b/web-views/src/main/java/groowt/view/web/transpile/AppendOrAddStatementFactory.java index c80fcde..8c56e8e 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/AppendOrAddStatementFactory.java +++ b/web-views/src/main/java/groowt/view/web/transpile/AppendOrAddStatementFactory.java @@ -17,4 +17,8 @@ public interface AppendOrAddStatementFactory { Statement appendOnly(BodyChildNode sourceNode, TranspilerState state, Expression rightSide); Statement addOrAppend(BodyChildNode sourceNode, TranspilerState state, Function getRightSide); + default Statement addOrAppend(BodyChildNode sourceNode, TranspilerState state, Expression rightSide) { + return this.addOrAppend(sourceNode, state, ignored -> rightSide); + } + } diff --git a/web-views/src/main/java/groowt/view/web/transpile/ComponentTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/ComponentTranspiler.java index 667aeab..e641c5f 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/ComponentTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/ComponentTranspiler.java @@ -3,9 +3,12 @@ package groowt.view.web.transpile; import groowt.view.web.ast.node.ComponentNode; import groowt.view.web.transpile.TranspilerUtil.TranspilerState; import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.Statement; + +import java.util.List; public interface ComponentTranspiler { - BlockStatement createComponentStatements( + List createComponentStatements( ComponentNode componentNode, TranspilerState state ); diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultAppendOrAddStatementFactory.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultAppendOrAddStatementFactory.java index b2d97f9..0308c90 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultAppendOrAddStatementFactory.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultAppendOrAddStatementFactory.java @@ -63,7 +63,7 @@ public class DefaultAppendOrAddStatementFactory implements AppendOrAddStatementF return this.doCreate( bodyChildNode, rightSide, - new VariableExpression(state.out()), + new VariableExpression(state.getWriter()), TranspilerUtil.APPEND, true ); diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultBodyTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultBodyTranspiler.java index f371331..e7f6a74 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultBodyTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultBodyTranspiler.java @@ -1,5 +1,6 @@ package groowt.view.web.transpile; +import groowt.view.web.WebViewComponentBugError; import groowt.view.web.ast.node.*; import groowt.view.web.transpile.TranspilerUtil.TranspilerState; import jakarta.inject.Inject; @@ -30,7 +31,7 @@ public class DefaultBodyTranspiler implements BodyTranspiler { TranspilerState state ) { final BlockStatement block = new BlockStatement(); - block.setVariableScope(state.currentScope()); + block.setVariableScope(state.pushScope()); for (final Node child : bodyNode.getChildren()) { switch (child) { case GStringBodyTextNode gStringBodyTextNode -> { @@ -49,16 +50,17 @@ public class DefaultBodyTranspiler implements BodyTranspiler { } case ComponentNode componentNode -> { // DO NOT add/append this, because the component transpiler does it already - block.addStatement(this.componentTranspiler.createComponentStatements(componentNode, state)); + block.addStatements(this.componentTranspiler.createComponentStatements(componentNode, state)); } case PlainScriptletNode plainScriptletNode -> { throw new UnsupportedOperationException("TODO"); } - default -> throw new UnsupportedOperationException( + default -> throw new WebViewComponentBugError(new UnsupportedOperationException( "BodyNode child of type " + child.getClass().getSimpleName() + " is not supported." - ); + )); } } + state.popScope(); return block; } 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 b65b890..751d6bd 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 @@ -1,106 +1,283 @@ package groowt.view.web.transpile; -import groovy.lang.Tuple2; -import groowt.view.component.*; -import groowt.view.component.context.*; +import groowt.view.component.context.ComponentResolveException; +import groowt.view.component.runtime.ComponentCreateException; +import groowt.view.component.runtime.RenderContext; +import groowt.view.web.WebViewComponentBugError; import groowt.view.web.ast.node.*; -import groowt.view.web.runtime.WebViewComponentChildCollection; +import groowt.view.web.runtime.WebViewComponentChildCollector; +import groowt.view.web.runtime.WebViewComponentChildCollectorClosure; import groowt.view.web.transpile.resolve.ComponentClassNodeResolver; import groowt.view.web.transpile.util.GroovyUtil; import groowt.view.web.transpile.util.GroovyUtil.ConvertResult; +import groowt.view.web.util.Provider; +import groowt.view.web.util.SourcePosition; import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.stmt.*; -import org.codehaus.groovy.syntax.Token; -import org.codehaus.groovy.syntax.Types; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; -import java.util.Objects; +import java.util.regex.Pattern; import static groowt.view.web.transpile.TranspilerUtil.*; public class DefaultComponentTranspiler implements ComponentTranspiler { - private static final ClassNode VIEW_COMPONENT = ClassHelper.make(ViewComponent.class); - private static final ClassNode CHILD_COLLECTION = ClassHelper.make(WebViewComponentChildCollection.class); + private static final ClassNode CHILD_COLLECTOR_TYPE = ClassHelper.make(WebViewComponentChildCollector.class); + private static final ClassNode FRAGMENT_TYPE = ClassHelper.make(GROOWT_VIEW_WEB + ".lib.Fragment"); + private static final ClassNode RESOLVED_TYPE = ClassHelper.make(RenderContext.Resolved.class); + private static final ClassNode CHILD_COLLECTOR_CLOSURE_TYPE = + ClassHelper.make(WebViewComponentChildCollectorClosure.class); + private static final ClassNode COMPONENT_RESOLVE_EXCEPTION_TYPE = ClassHelper.make(ComponentResolveException.class); + private static final ClassNode COMPONENT_CREATE_EXCEPTION_TYPE = ClassHelper.make(ComponentCreateException.class); - private static final ClassNode EXCEPTION = ClassHelper.make(Exception.class); - private static final ClassNode COMPONENT_CREATE = ClassHelper.make(ComponentCreateException.class); + private static final Pattern isFqn = Pattern.compile("^(\\p{Ll}.+\\.)+\\p{Lu}.+$"); + private static final Pattern isWithPackage = Pattern.compile("^\\p{Ll}.+\\."); - private static final ClassNode NO_FACTORY_MISSING_EXCEPTION = ClassHelper.make(NoFactoryMissingException.class); + private final Provider appendOrAddStatementFactoryProvider; + private final Provider componentClassNodeResolverProvider; + private final Provider valueNodeTranspilerProvider; + private final Provider bodyTranspilerProvider; - private static final ClassNode MISSING_COMPONENT_EXCEPTION = ClassHelper.make(MissingComponentException.class); - private static final ClassNode MISSING_CLASS_TYPE_EXCEPTION = ClassHelper.make(MissingClassTypeException.class); - private static final ClassNode MISSING_STRING_TYPE_EXCEPTION = ClassHelper.make(MissingStringTypeException.class); - private static final ClassNode MISSING_FRAGMENT_TYPE_EXCEPTION = - ClassHelper.make(MissingFragmentTypeException.class); - - private static final String CREATE = "create"; - private static final String CREATE_FRAGMENT = "createFragment"; - private static final String RESOLVE = "resolve"; - private static final String ADD = "add"; - private static final String APPEND = "append"; - private static final String FRAGMENT_FQN = GROOWT_VIEW_WEB + ".lib.Fragment"; - - private ValueNodeTranspiler valueNodeTranspiler; - private BodyTranspiler bodyTranspiler; - private AppendOrAddStatementFactory appendOrAddStatementFactory; - private ComponentClassNodeResolver componentClassNodeResolver; - - public void setValueNodeTranspiler(ValueNodeTranspiler valueNodeTranspiler) { - this.valueNodeTranspiler = valueNodeTranspiler; + public DefaultComponentTranspiler( + Provider appendOrAddStatementFactoryProvider, + Provider componentClassNodeResolverProvider, + Provider valueNodeTranspilerProvider, + Provider bodyTranspilerProvider + ) { + this.appendOrAddStatementFactoryProvider = appendOrAddStatementFactoryProvider; + this.componentClassNodeResolverProvider = componentClassNodeResolverProvider; + this.valueNodeTranspilerProvider = valueNodeTranspilerProvider; + this.bodyTranspilerProvider = bodyTranspilerProvider; } - public void setBodyTranspiler(BodyTranspiler bodyTranspiler) { - this.bodyTranspiler = bodyTranspiler; + protected ValueNodeTranspiler getValueNodeTranspiler() { + return this.valueNodeTranspilerProvider.get(); } - public void setAppendOrAddStatementFactory(AppendOrAddStatementFactory appendOrAddStatementFactory) { - this.appendOrAddStatementFactory = appendOrAddStatementFactory; + protected BodyTranspiler getBodyTranspiler() { + return this.bodyTranspilerProvider.get(); } - public void setComponentClassNodeResolver(ComponentClassNodeResolver componentClassNodeResolver) { - this.componentClassNodeResolver = componentClassNodeResolver; + protected AppendOrAddStatementFactory getAppendOrAddStatementFactory() { + return this.appendOrAddStatementFactoryProvider.get(); } - // ViewComponent c0 - protected ExpressionStatement getComponentDeclaration(Variable component) { - final var componentDeclaration = new DeclarationExpression( - new VariableExpression(component), - new Token(Types.ASSIGN, "=", -1, -1), + protected ComponentClassNodeResolver getComponentClassNodeResolver() { + return this.componentClassNodeResolverProvider.get(); + } + + /* UTIL */ + + private void addLineAndColumn(Node sourceNode, ArgumentListExpression args) { + final var lineAndColumn = lineAndColumn(sourceNode.getTokenRange().getStartPosition()); + args.addExpression(lineAndColumn.getV1()); + args.addExpression(lineAndColumn.getV2()); + } + + protected String getComponentName(int componentNumber) { + return "c" + componentNumber; + } + + /* RESOLVED DECLARATION */ + + // RenderContext.Resolved c0Resolved + protected Statement getResolvedDeclaration(TranspilerState state) { + final ClassNode resolvedType = RESOLVED_TYPE.getPlainNodeReference(); + resolvedType.setGenericsTypes( + new GenericsType[] { new GenericsType(WEB_VIEW_COMPONENT_TYPE) } + ); + final var resolvedVariable = new VariableExpression( + this.getComponentName(state.newComponentNumber()) + "Resolved", + resolvedType + ); + state.pushResolved(resolvedVariable); + final var declarationExpr = new DeclarationExpression( + resolvedVariable, + getAssignToken(), EmptyExpression.INSTANCE ); - return new ExpressionStatement(componentDeclaration); + return new ExpressionStatement(declarationExpr); } - // 'ComponentName' - protected ConstantExpression getComponentTypeNameExpression(ComponentNode componentNode) { - final String componentTypeName = switch (componentNode) { - case TypedComponentNode typedComponentNode -> switch (typedComponentNode.getArgs().getType()) { - case ClassComponentTypeNode classComponentTypeNode -> classComponentTypeNode.getFullyQualifiedName(); - case StringComponentTypeNode stringComponentTypeNode -> stringComponentTypeNode.getIdentifier(); - }; - case FragmentComponentNode ignored -> FRAGMENT_FQN; + /* RESOLVE */ + + protected List getArgsAsList( + TypedComponentNode componentNode, + TranspilerState state + ) { + return switch (componentNode.getArgs().getType()) { + case ClassComponentTypeNode classComponentTypeNode -> { + final String identifier = classComponentTypeNode.getIdentifier(); + final ConstantExpression alias = getStringLiteral(identifier); + final var matcher = isFqn.matcher(identifier); + if (matcher.matches()) { + final ClassNode classNode = ClassHelper.make(identifier); + final ClassExpression classExpression = new ClassExpression(classNode); + yield List.of(alias, classExpression); + } else { + // we need to resolve it + final var isWithPackageMatcher = isWithPackage.matcher(identifier); + if (isWithPackageMatcher.matches()) { + final var resolveResult = this.getComponentClassNodeResolver().getClassForFqn(identifier); + if (resolveResult.isLeft()) { + final var error = resolveResult.getLeft(); + error.setNode(componentNode.getArgs().getType()); + state.addError(error); + yield List.of(); + } else { + final ClassNode classNode = resolveResult.getRight(); + final ClassExpression classExpression = new ClassExpression(classNode); // TODO: pos + yield List.of(alias, classExpression); + } + } else { + final var resolveResult = + this.getComponentClassNodeResolver().getClassForNameWithoutPackage(identifier); + if (resolveResult.isLeft()) { + final var error = resolveResult.getLeft(); + error.setNode(componentNode.getArgs().getType()); + state.addError(error); + yield List.of(); + } else { + final ClassNode classNode = resolveResult.getRight(); + final ClassExpression classExpression = new ClassExpression(classNode); // TODO: pos + yield List.of(alias, classExpression); + } + } + } + } + case StringComponentTypeNode stringComponentTypeNode -> { + final String identifier = stringComponentTypeNode.getIdentifier(); + final ConstantExpression typeName = getStringLiteral(identifier); + yield List.of(typeName); + } }; - return makeStringLiteral(componentTypeName); } - // context.resolve('ComponentName') - protected MethodCallExpression getContextResolveExpr(ComponentNode componentNode, Variable componentContext) { - final var args = new ArgumentListExpression(); - args.addExpression(this.getComponentTypeNameExpression(componentNode)); - return new MethodCallExpression(new VariableExpression(componentContext), RESOLVE, args); + // 'h1' | 'MyComponent', MyComponent(.class) + protected ArgumentListExpression getResolveArgs(TypedComponentNode componentNode, TranspilerState state) { + final List args = this.getArgsAsList(componentNode, state); + final ArgumentListExpression argsListExpr = new ArgumentListExpression(); + args.forEach(argsListExpr::addExpression); + return argsListExpr; } + // context.resolve('h1' | 'MyComponent', MyComponent.class) + protected MethodCallExpression getContextResolveExpr( + TypedComponentNode componentNode, + TranspilerState state + ) { + return new MethodCallExpression( + new VariableExpression(state.getRenderContext()), + "resolve", + this.getResolveArgs(componentNode, state) + ); + } + + // context.resolve('h1' | 'MyComponent', MyComponent.class) + protected ExpressionStatement getContextResolveStmt( + TypedComponentNode componentNode, + TranspilerState state + ) { + final BinaryExpression assignment = new BinaryExpression( + new VariableExpression(state.getCurrentResolved()), + getAssignToken(), + this.getContextResolveExpr(componentNode, state) + ); + return new ExpressionStatement(assignment); + } + + /* RESOLVE CATCH */ + + protected CatchStatement getResolveCatch(TypedComponentNode componentNode) { + final Parameter exceptionParameter = new Parameter( + COMPONENT_RESOLVE_EXCEPTION_TYPE, + "componentResolveException" + ); + final VariableExpression exceptionVariable = new VariableExpression(exceptionParameter); + + final VariableScope variableScope = new VariableScope(); + variableScope.putDeclaredVariable(exceptionParameter); + + final BinaryExpression setTemplateExpression = new BinaryExpression( + new PropertyExpression(exceptionVariable, new ConstantExpression("template")), + getAssignToken(), + VariableExpression.THIS_EXPRESSION + ); + final Statement setTemplateStatement = new ExpressionStatement(setTemplateExpression); + + final SourcePosition position = componentNode.getTokenRange().getStartPosition(); + + final BinaryExpression setLineExpression = new BinaryExpression( + new PropertyExpression(exceptionVariable, new ConstantExpression("line")), + getAssignToken(), + new ConstantExpression(position.line()) + ); + final Statement setLineStatement = new ExpressionStatement(setLineExpression); + + final BinaryExpression setColumnExpression = new BinaryExpression( + new PropertyExpression(exceptionVariable, new ConstantExpression("column")), + getAssignToken(), + new ConstantExpression(position.column()) + ); + final Statement setColumnStatement = new ExpressionStatement(setColumnExpression); + + final Statement throwStatement = new ThrowStatement(exceptionVariable); + + final List statements = new ArrayList<>(); + statements.add(setTemplateStatement); + statements.add(setLineStatement); + statements.add(setColumnStatement); + statements.add(throwStatement); + + final BlockStatement block = new BlockStatement(statements, variableScope); + + return new CatchStatement(exceptionParameter, block); + } + + /* RESOLVE BLOCK */ + + protected List getResolveStatements( + TypedComponentNode componentNode, + TranspilerState state + ) { + final Statement declaration = this.getResolvedDeclaration(state); + final Statement resolveStatement = this.getContextResolveStmt(componentNode, state); + + final TryCatchStatement resolveTryCatch = new TryCatchStatement(resolveStatement, EmptyStatement.INSTANCE); + resolveTryCatch.addCatch(this.getResolveCatch(componentNode)); + return List.of(declaration, resolveTryCatch); + } + + /* TYPED COMPONENT DECLARATION */ + + // ViewComponent c0 + protected Statement getTypedComponentDeclaration(TranspilerState state) { + final VariableExpression componentVariable = new VariableExpression( + this.getComponentName(state.getCurrentComponentNumber()), WEB_VIEW_COMPONENT_TYPE + ); + state.pushComponent(componentVariable); + state.getCurrentScope().putDeclaredVariable(componentVariable); + + final Expression declarationExpr = new DeclarationExpression( + componentVariable, + getAssignToken(), + EmptyExpression.INSTANCE + ); + return new ExpressionStatement(declarationExpr); + } + + /* TYPED COMPONENT CREATE: attributes map */ + // key: value protected MapEntryExpression getAttrExpression(AttrNode attrNode, TranspilerState state) { - final var keyExpr = makeStringLiteral(attrNode.getKeyNode().getKey()); + final ConstantExpression keyExpr = getStringLiteral(attrNode.getKeyNode().getKey()); // TODO: pos final Expression valueExpr = switch (attrNode) { case BooleanValueAttrNode ignored -> ConstantExpression.PRIM_TRUE; case KeyValueAttrNode keyValueAttrNode -> - this.valueNodeTranspiler.createExpression(keyValueAttrNode.getValueNode(), state); + this.getValueNodeTranspiler().createExpression(keyValueAttrNode.getValueNode(), state); }; return new MapEntryExpression(keyExpr, valueExpr); } @@ -108,7 +285,7 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { // [key: value, ...] protected MapExpression getAttrMap(List attributeNodes, TranspilerState state) { if (attributeNodes.isEmpty()) { - throw new IllegalArgumentException("attributeNodes cannot be empty"); + throw new WebViewComponentBugError(new IllegalArgumentException("attributeNodes cannot be empty.")); } final var result = new MapExpression(); attributeNodes.stream() @@ -117,343 +294,234 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { return result; } + /* TYPED COMPONENT CREATE: component constructor */ + // arg0, arg1, arg2, etc protected List getConstructorArgs(ComponentConstructorNode componentConstructorNode) { final ConvertResult convertResult = GroovyUtil.convert(componentConstructorNode.getGroovyCode() .getAsValidGroovyCode()); - final var blockStatement = convertResult.blockStatement(); + final BlockStatement blockStatement = convertResult.blockStatement(); if (blockStatement == null) { - throw new IllegalStateException("Did not expect blockStatement to be null"); + throw new WebViewComponentBugError(new IllegalStateException("Did not expect blockStatement to be null.")); } - final var statements = blockStatement.getStatements(); + final List statements = blockStatement.getStatements(); if (statements.size() != 1) { - throw new IllegalStateException("statements size is not 1"); + throw new WebViewComponentBugError(new IllegalStateException("statements.size() != 1")); } final ExpressionStatement exprStmt = (ExpressionStatement) statements.getFirst(); final ListExpression listExpr = (ListExpression) exprStmt.getExpression(); - return listExpr.getExpressions(); + return listExpr.getExpressions(); // TODO: pos for each expression } - private void addLineAndColumn(Node sourceNode, ArgumentListExpression args) { - final var lineAndColumn = lineAndColumn(sourceNode.getTokenRange().getStartPosition()); - args.addExpression(lineAndColumn.getV1()); - args.addExpression(lineAndColumn.getV2()); - } + /* COMPONENT CHILDREN */ - protected MethodCallExpression getOutCall(BodyChildNode sourceNode, TranspilerState state, Expression toOutput) { - final VariableExpression outVariableExpr = new VariableExpression(state.out()); - final ArgumentListExpression args = new ArgumentListExpression(); - args.addExpression(toOutput); - switch (sourceNode) { - case GStringBodyTextNode ignored -> this.addLineAndColumn(sourceNode.asNode(), args); - case ComponentNode ignored -> this.addLineAndColumn(sourceNode.asNode(), args); - default -> { - } - } - return new MethodCallExpression(outVariableExpr, APPEND, args); - } - - // { out << jString | gString | component } - protected ClosureExpression getOutClosure(BodyChildNode sourceNode, TranspilerState state, Expression toRender) { - if (toRender instanceof VariableExpression variableExpression) { - variableExpression.setClosureSharedVariable(true); - } - final Statement stmt = new ExpressionStatement(this.getOutCall(sourceNode, state, toRender)); - return new ClosureExpression(Parameter.EMPTY_ARRAY, stmt); - } - - // c0_childCollector.add (jString | gString | component) { out << ... } - protected Statement getChildCollectorAdd( - BodyChildNode sourceNode, - TranspilerState state, - Variable childCollector, - Expression toAdd - ) { - final var childCollectorVariableExpr = new VariableExpression(childCollector); - final ClosureExpression renderChild = this.getOutClosure(sourceNode, state, toAdd); + // c0cc.add (jString | gString | component) + protected Statement getChildCollectorAdd(Variable childCollector, Expression toAdd) { + final VariableExpression childCollectorVariableExpr = new VariableExpression(childCollector); final MethodCallExpression methodCall = new MethodCallExpression( childCollectorVariableExpr, - ADD, - new ArgumentListExpression(List.of(toAdd, renderChild)) + "add", + new ArgumentListExpression(List.of(toAdd)) ); return new ExpressionStatement(methodCall); } - /** - * @return Tuple containing 1. body ClosureExpression, and 2. childCollector Variable - */ - // { WebViewComponentChildCollector c0_childCollector -> ... } - protected Tuple2 getBodyClosure( + // { WebViewComponentChildCollector c0cc -> ... } + protected ClosureExpression getChildCollectorClosure( BodyNode bodyNode, - TranspilerState state, - String componentVariableName + TranspilerState state ) { - final Parameter childCollectorParam = new Parameter( - CHILD_COLLECTION, - componentVariableName + "_childCollector" + final Parameter ccParam = new Parameter( + CHILD_COLLECTOR_TYPE, + this.getComponentName(state.getCurrentComponentNumber()) + "cc" ); final var scope = state.pushScope(); - scope.putDeclaredVariable(childCollectorParam); - state.pushChildCollector(childCollectorParam); - final BlockStatement bodyStatements = this.bodyTranspiler.transpileBody( + scope.putDeclaredVariable(ccParam); + state.pushChildCollector(ccParam); + + final BlockStatement bodyStatements = this.getBodyTranspiler().transpileBody( bodyNode, - (sourceNode, expr) -> this.getChildCollectorAdd(sourceNode, state, childCollectorParam, expr), + (sourceNode, expr) -> this.getChildCollectorAdd(ccParam, expr), state ); + + // clean up state.popChildCollector(); state.popScope(); - final ClosureExpression bodyClosure = new ClosureExpression( - new Parameter[] { childCollectorParam }, + return new ClosureExpression( + new Parameter[] { ccParam }, bodyStatements ); - return new Tuple2<>(bodyClosure, childCollectorParam); } - /** - * @return Tuple containing 1. create Expression, - * and 2. childCollector Variable, possibly {@code null}. - */ + protected StaticMethodCallExpression getChildCollectorGetter( + BodyNode bodyNode, + TranspilerState state + ) { + final ArgumentListExpression args = new ArgumentListExpression(); + args.addExpression(VariableExpression.THIS_EXPRESSION); + args.addExpression(this.getChildCollectorClosure(bodyNode, state)); + + return new StaticMethodCallExpression( + CHILD_COLLECTOR_CLOSURE_TYPE, + "get", + args + ); + } + + /* TYPED COMPONENT CREATE: expression and statement */ + // context.create(...) {...} - protected Tuple2 getCreateExpression( - ComponentNode componentNode, - TranspilerState state, - String componentVariableName + protected MethodCallExpression getTypedComponentCreateExpression( + TypedComponentNode componentNode, + TranspilerState state ) { final var createArgs = new ArgumentListExpression(); - final String createName; - Variable childCollector = null; - if (componentNode instanceof TypedComponentNode typedComponentNode) { - createName = CREATE; - final var contextResolve = this.getContextResolveExpr(componentNode, state.context()); - createArgs.addExpression(contextResolve); - final List attributeNodes = typedComponentNode.getArgs().getAttributes(); - if (!attributeNodes.isEmpty()) { - createArgs.addExpression(this.getAttrMap(attributeNodes, state)); - } - final ComponentConstructorNode constructorNode = typedComponentNode.getArgs().getConstructor(); - if (constructorNode != null) { - this.getConstructorArgs(constructorNode).forEach(createArgs::addExpression); - } + createArgs.addExpression(new VariableExpression(state.getCurrentResolved())); - final @Nullable BodyNode bodyNode = componentNode.getBody(); - if (bodyNode != null) { - final var bodyResult = this.getBodyClosure(bodyNode, state, componentVariableName); - childCollector = bodyResult.getV2(); - createArgs.addExpression(bodyResult.getV1()); - } - } else if (componentNode instanceof FragmentComponentNode fragmentComponentNode) { - createName = CREATE_FRAGMENT; - final BodyNode bodyNode = Objects.requireNonNull( - fragmentComponentNode.getBody(), - "FragmentComponentNode cannot have a null body." - ); - final var bodyResult = this.getBodyClosure(bodyNode, state, componentVariableName); - childCollector = bodyResult.getV2(); - createArgs.addExpression(bodyResult.getV1()); - } else { - throw new IllegalArgumentException("Unsupported ComponentNode type: " + componentNode.getClass().getName()); + final List attributeNodes = componentNode.getArgs().getAttributes(); + if (!attributeNodes.isEmpty()) { + createArgs.addExpression(this.getAttrMap(attributeNodes, state)); + } + final ComponentConstructorNode constructorNode = componentNode.getArgs().getConstructor(); + if (constructorNode != null) { + this.getConstructorArgs(constructorNode).forEach(createArgs::addExpression); } - final var createCall = new MethodCallExpression( - new VariableExpression(state.context()), - createName, - createArgs - ); + final @Nullable BodyNode bodyNode = componentNode.getBody(); + if (bodyNode != null) { + createArgs.addExpression(this.getChildCollectorGetter(bodyNode, state)); + } - return new Tuple2<>(createCall, childCollector); + return new MethodCallExpression(new VariableExpression(state.getRenderContext()), "create", createArgs); } - /** - * @return Tuple containing 1. assignment ExpressionStatement, - * and 2. childCollector Variable, possibly {@code null}. - */ // c0 = context.create(context.resolve(''), [:], ...) {...} - protected Tuple2 getCreateAssignStatement( - ComponentNode componentNode, - TranspilerState state, - String componentVariableName, - Variable component + protected ExpressionStatement getTypedComponentCreateStatement( + TypedComponentNode componentNode, + TranspilerState state ) { - final var componentAssignLeft = new VariableExpression(component); - final var createExprResult = this.getCreateExpression(componentNode, state, componentVariableName); - final var componentAssignExpr = new BinaryExpression( - componentAssignLeft, - new Token(Types.ASSIGN, "=", -1, -1), - createExprResult.getV1() - ); - return new Tuple2<>(new ExpressionStatement(componentAssignExpr), createExprResult.getV2()); + final var left = new VariableExpression(state.getCurrentComponent()); + final var right = this.getTypedComponentCreateExpression(componentNode, state); + final var componentAssignExpr = new BinaryExpression(left, getAssignToken(), right); + return new ExpressionStatement(componentAssignExpr); } - // catch (NoFactoryMissingException c0nfme) { - // throw new MissingClassComponentException(this, 'ComponentType', c0nfme) - // } - protected CatchStatement getNoMissingFactoryExceptionCatch( - ComponentNode componentNode, - String componentVariableName - ) { - final String exceptionName = componentVariableName + "nfme"; - final Parameter fmeParam = new Parameter(NO_FACTORY_MISSING_EXCEPTION, exceptionName); - final VariableExpression fmeVar = new VariableExpression(exceptionName); + /* CREATE CATCH */ - final var lineAndColumn = lineAndColumn(componentNode.getTokenRange().getStartPosition()); - final ConstantExpression line = lineAndColumn.getV1(); - final ConstantExpression column = lineAndColumn.getV2(); - - final ConstructorCallExpression mcceConstructorExpr = switch (componentNode) { - case TypedComponentNode typedComponentNode -> switch (typedComponentNode.getArgs().getType()) { - case StringComponentTypeNode stringComponentTypeNode -> - new ConstructorCallExpression(MISSING_STRING_TYPE_EXCEPTION, new ArgumentListExpression(List.of( - VariableExpression.THIS_EXPRESSION, - makeStringLiteral(stringComponentTypeNode.getIdentifier()), - line, - column, - fmeVar - ))); - case ClassComponentTypeNode classComponentTypeNode -> - new ConstructorCallExpression(MISSING_CLASS_TYPE_EXCEPTION, new ArgumentListExpression(List.of( - VariableExpression.THIS_EXPRESSION, - makeStringLiteral(classComponentTypeNode.getIdentifier()), - line, - column, - fmeVar - ))); - }; - case FragmentComponentNode ignored -> new ConstructorCallExpression( - MISSING_FRAGMENT_TYPE_EXCEPTION, - new ArgumentListExpression(List.of(VariableExpression.THIS_EXPRESSION, line, column, fmeVar)) - ); - }; - final Statement throwMcceStmt = new ThrowStatement(mcceConstructorExpr); - return new CatchStatement(fmeParam, throwMcceStmt); - } - - // catch (MissingComponentException c0mce) { throw c0mce } - protected CatchStatement getMissingComponentExceptionCatch(String componentVariableName) { - final String exceptionName = componentVariableName + "mce"; - final Parameter exceptionParam = new Parameter(MISSING_COMPONENT_EXCEPTION, exceptionName); - final VariableExpression mceVar = new VariableExpression(exceptionName); - final Statement throwMceStmt = new ThrowStatement(mceVar); - return new CatchStatement(exceptionParam, throwMceStmt); - } - - // catch (Exception c0ce) { throw new ComponentCreateException(c0ce) } - protected CatchStatement getGeneralCreateExceptionCatch( - ComponentNode componentNode, - String componentVariableName - ) { - final String exceptionName = componentVariableName + "ce"; - final Parameter exceptionParam = new Parameter(EXCEPTION, exceptionName); + // catch (ComponentCreateException c0CreateException) { ... } + protected CatchStatement getTypedCreateCatch(TypedComponentNode componentNode, TranspilerState state) { + final String exceptionName = this.getComponentName(state.getCurrentComponentNumber()) + "CreateException"; + final Parameter exceptionParam = new Parameter(COMPONENT_CREATE_EXCEPTION_TYPE, exceptionName); final VariableExpression exceptionVar = new VariableExpression(exceptionName); - final ConstantExpression componentTypeExpression = switch (componentNode) { - case TypedComponentNode typedComponentNode -> switch (typedComponentNode.getArgs().getType()) { - case StringComponentTypeNode stringComponentTypeNode -> - makeStringLiteral(stringComponentTypeNode.getIdentifier()); - case ClassComponentTypeNode classComponentTypeNode -> - makeStringLiteral(classComponentTypeNode.getFullyQualifiedName()); - }; - case FragmentComponentNode ignored -> makeStringLiteral(FRAGMENT_FQN); - }; + final VariableScope scope = new VariableScope(); + scope.putDeclaredVariable(exceptionParam); - final var lineAndColumn = lineAndColumn(componentNode.getTokenRange().getStartPosition()); + final List statements = new ArrayList<>(); - final ConstructorCallExpression cce = new ConstructorCallExpression( - COMPONENT_CREATE, - new ArgumentListExpression(List.of( - componentTypeExpression, - VariableExpression.THIS_EXPRESSION, - lineAndColumn.getV1(), - lineAndColumn.getV2(), - exceptionVar - )) + final BinaryExpression setTemplateExpression = new BinaryExpression( + new PropertyExpression(exceptionVar, "template"), + getAssignToken(), + VariableExpression.THIS_EXPRESSION ); - final Statement throwCcStmt = new ThrowStatement(cce); - return new CatchStatement(exceptionParam, throwCcStmt); - } + statements.add(new ExpressionStatement(setTemplateExpression)); - protected List getCreateCatches(ComponentNode componentNode, String componentVariableName) { - final List catches = new ArrayList<>(); - catches.add(this.getNoMissingFactoryExceptionCatch(componentNode, componentVariableName)); - catches.add(this.getMissingComponentExceptionCatch(componentVariableName)); - catches.add(this.getGeneralCreateExceptionCatch(componentNode, componentVariableName)); - return catches; - } + final SourcePosition start = componentNode.getTokenRange().getStartPosition(); - protected Statement createSetContext(TranspilerState state, Variable component) { - final VariableExpression componentExpr = new VariableExpression(component); - final VariableExpression contextExpr = new VariableExpression(state.context()); - final var args = new ArgumentListExpression(contextExpr); - final var setContext = new MethodCallExpression(componentExpr, "setContext", args); - return new ExpressionStatement(setContext); - } - - protected String getComponentVariableName(int componentNumber) { - return "c" + componentNumber; - } - - @Override - public BlockStatement createComponentStatements(ComponentNode componentNode, TranspilerState state) { - final var componentVariableName = this.getComponentVariableName(state.newComponentNumber()); - final VariableExpression component = new VariableExpression(componentVariableName, VIEW_COMPONENT); - - final BlockStatement result = new BlockStatement(); - final VariableScope scope = state.currentScope(); - result.setVariableScope(scope); - scope.putDeclaredVariable(component); - - // ViewComponent c0; - result.addStatement(this.getComponentDeclaration(component)); - - // c0 = context.create(...) { ... } - final var createAssignStatementResult = this.getCreateAssignStatement( - componentNode, - state, - componentVariableName, - component + final BinaryExpression setLineExpression = new BinaryExpression( + new PropertyExpression(exceptionVar, "line"), + getAssignToken(), + new ConstantExpression(start.line()) ); + statements.add(new ExpressionStatement(setLineExpression)); - // try { ... } catch { ... } - final var tryCreateStatement = new TryCatchStatement( - createAssignStatementResult.getV1(), + final BinaryExpression setColumnExpression = new BinaryExpression( + new PropertyExpression(exceptionVar, "column"), + getAssignToken(), + new ConstantExpression(start.column()) + ); + statements.add(new ExpressionStatement(setColumnExpression)); + + statements.add(new ThrowStatement(exceptionVar)); + + return new CatchStatement(exceptionParam, new BlockStatement(statements, scope)); + } + + protected List getTypedCreateStatements(TypedComponentNode componentNode, TranspilerState state) { + final Statement declaration = this.getTypedComponentDeclaration(state); + final TryCatchStatement createTryCatch = new TryCatchStatement( + this.getTypedComponentCreateStatement(componentNode, state), EmptyStatement.INSTANCE ); - this.getCreateCatches(componentNode, componentVariableName).forEach(tryCreateStatement::addCatch); - result.addStatement(tryCreateStatement); + createTryCatch.addCatch(this.getTypedCreateCatch(componentNode, state)); + return List.of(declaration, createTryCatch); + } - // component.setContext(context) - result.addStatement(this.createSetContext(state, component)); + /* FRAGMENT COMPONENT */ - // out or collect - final var addOrAppend = this.appendOrAddStatementFactory.addOrAppend( - componentNode, - state, - action -> switch (action) { - case ADD -> { - final var args = new ArgumentListExpression(); - args.addExpression(component); - final var outComponent = new VariableExpression(component); - outComponent.setClosureSharedVariable(true); - final Statement renderStatement = this.appendOrAddStatementFactory.appendOnly( - componentNode, - state, - outComponent - ); - final ClosureExpression renderArg = new ClosureExpression( - Parameter.EMPTY_ARRAY, - renderStatement - ); - args.addExpression(renderArg); - yield args; - } - case APPEND -> new VariableExpression(component); - } + // context.createFragment(new Fragment(), ) + protected MethodCallExpression getFragmentCreateExpression( + FragmentComponentNode componentNode, + TranspilerState state + ) { + final Expression fragmentConstructor = new ConstructorCallExpression( + FRAGMENT_TYPE, + ArgumentListExpression.EMPTY_ARGUMENTS ); - result.addStatement(addOrAppend); + final Expression ccClosure = this.getChildCollectorGetter(componentNode.getBody(), state); - return result; + final ArgumentListExpression args = new ArgumentListExpression(List.of(fragmentConstructor, ccClosure)); + + return new MethodCallExpression( + new VariableExpression(state.getRenderContext()), + "createFragment", + args + ); + } + + /* MAIN */ + + @Override + public List createComponentStatements(ComponentNode componentNode, TranspilerState state) { + if (componentNode instanceof TypedComponentNode typedComponentNode) { + // Resolve + final List resolveStatements = this.getResolveStatements(typedComponentNode, state); + // Create + final List createStatements = this.getTypedCreateStatements(typedComponentNode, state); + // Append/Add + final Statement addOrAppend = this.getAppendOrAddStatementFactory().addOrAppend( + componentNode, + state, + new VariableExpression(state.getCurrentComponent()) + ); + + // cleanup + state.popResolved(); + state.popComponent(); + + final List allStatements = new ArrayList<>(); + allStatements.addAll(resolveStatements); + allStatements.addAll(createStatements); + allStatements.add(addOrAppend); + + return allStatements; + } else if (componentNode instanceof FragmentComponentNode fragmentComponentNode) { + // Create and add all at once + final Statement addOrAppend = this.getAppendOrAddStatementFactory().addOrAppend( + componentNode, + state, + this.getFragmentCreateExpression(fragmentComponentNode, state) + ); + return List.of(addOrAppend); + } else { + throw new WebViewComponentBugError(new IllegalArgumentException( + "Cannot handle a ComponentNode not of type TypedComponentNode or FragmentComponentNode." + )); + } } } diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultGroovyTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultGroovyTranspiler.java index b9518da..86addef 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultGroovyTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultGroovyTranspiler.java @@ -1,28 +1,32 @@ package groowt.view.web.transpile; -import groowt.view.web.antlr.TokenList; +import groovy.transform.Field; +import groowt.view.component.compiler.ComponentTemplateCompileException; +import groowt.view.component.compiler.ComponentTemplateCompilerConfiguration; +import groowt.view.web.WebViewComponentBugError; import groowt.view.web.ast.node.BodyNode; import groowt.view.web.ast.node.CompilationUnitNode; import groowt.view.web.ast.node.PreambleNode; +import groowt.view.web.compiler.MultipleWebViewComponentCompileErrorsException; +import groowt.view.web.compiler.WebViewComponentTemplateCompileException; +import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit; +import groowt.view.web.runtime.DefaultWebViewComponentRenderContext; +import groowt.view.web.transpile.resolve.ClassLoaderComponentClassNodeResolver; import groowt.view.web.transpile.util.GroovyUtil; import org.codehaus.groovy.ast.*; -import org.codehaus.groovy.ast.expr.ClosureExpression; -import org.codehaus.groovy.ast.expr.DeclarationExpression; +import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.ExpressionStatement; import org.codehaus.groovy.ast.stmt.ReturnStatement; import org.codehaus.groovy.ast.stmt.Statement; import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.ErrorCollector; import org.codehaus.groovy.control.SourceUnit; -import org.codehaus.groovy.control.io.ReaderSource; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; -import java.util.function.Supplier; import static groowt.view.web.transpile.TranspilerUtil.*; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; @@ -38,34 +42,18 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler { private static final Logger logger = LoggerFactory.getLogger(DefaultGroovyTranspiler.class); - private final CompilationUnit groovyCompilationUnit; - private final String defaultPackageName; - private final Supplier configurationSupplier; + private static final ClassNode FIELD_ANNOTATION = ClassHelper.make(Field.class); + private static final ClassNode RENDER_CONTEXT_IMPLEMENTATION = + ClassHelper.make(DefaultWebViewComponentRenderContext.class); - public DefaultGroovyTranspiler( - CompilationUnit groovyCompilationUnit, - @Nullable String defaultPackageName, - Supplier configurationSupplier + protected TranspilerConfiguration getConfiguration( + WebViewComponentTemplateCompileUnit compileUnit, + ModuleNode moduleNode, + ClassLoader classLoader ) { - this.groovyCompilationUnit = groovyCompilationUnit; - this.defaultPackageName = defaultPackageName; - this.configurationSupplier = configurationSupplier; - } - - protected TranspilerConfiguration getConfiguration() { - return this.configurationSupplier.get(); - } - - public @NotNull String getDefaultPackageName() { - return this.defaultPackageName != null ? this.defaultPackageName : GROOWT_VIEW_WEB; - } - - protected @NotNull String getPackageName(ModuleNode moduleNode) { - if (moduleNode.hasPackageName()) { - return moduleNode.getPackageName(); - } else { - return this.getDefaultPackageName(); - } + return new DefaultTranspilerConfiguration(new ClassLoaderComponentClassNodeResolver( + compileUnit, moduleNode, classLoader + )); } protected void checkPreambleClasses(String templateName, List classNodes) { @@ -81,7 +69,10 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler { } } - protected List convertPreambleClassesToInnerClasses(ClassNode mainClassNode, List classNodes) { + protected List convertPreambleClassesToInnerClasses( + ClassNode mainClassNode, + List classNodes + ) { final List result = new ArrayList<>(); for (final var classNode : classNodes) { if (classNode instanceof InnerClassNode innerClassNode) { @@ -102,18 +93,23 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler { } protected void handlePreamble( - String templateName, + String templateClassName, PreambleNode preambleNode, ClassNode mainClassNode, WebViewComponentModuleNode moduleNode ) { - final GroovyUtil.ConvertResult preambleConvert = GroovyUtil.convert( + final GroovyUtil.ConvertResult convertResult = GroovyUtil.convert( preambleNode.getGroovyCode().getAsValidGroovyCode() ); - WebViewComponentModuleNode.copyTo(preambleConvert.moduleNode(), moduleNode); + WebViewComponentModuleNode.copyTo(convertResult.moduleNode(), moduleNode); - final BlockStatement preambleBlock = preambleConvert.blockStatement(); + if (convertResult.moduleNode().hasPackage()) { + moduleNode.setPackage(convertResult.moduleNode().getPackage()); + mainClassNode.setName(moduleNode.getPackageName() + "." + templateClassName); + } + + final BlockStatement preambleBlock = convertResult.blockStatement(); if (preambleBlock != null) { // Fields final List preambleStatements = preambleBlock.getStatements(); @@ -133,7 +129,7 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler { "Currently, only classes, methods, and field declarations " + "(marked with @groovy.transform.Field) " + "are supported. The rest will be ignored.", - templateName + templateClassName ); } declarationsWithField.forEach(declaration -> { @@ -142,16 +138,16 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler { } // move methods from script class - final ClassNode scriptClass = preambleConvert.scriptClass(); + final ClassNode scriptClass = convertResult.scriptClass(); if (scriptClass != null) { scriptClass.getMethods().forEach(mainClassNode::addMethod); } // handle classes - final List classNodes = preambleConvert.classNodes(); - this.checkPreambleClasses(templateName, classNodes); + final List classNodes = convertResult.classNodes(); + this.checkPreambleClasses(templateClassName, classNodes); final List toInner = classNodes.stream() - .filter(classNode -> classNode != preambleConvert.scriptClass()) + .filter(classNode -> classNode != convertResult.scriptClass()) .filter(classNode -> !classNode.isScript()) .toList(); final List innerClassNodes = this.convertPreambleClassesToInnerClasses(mainClassNode, toInner); @@ -164,30 +160,35 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler { // - preamble with script -> use the script class from the converted preamble, // and don't forget to call run in our render method @Override - public void transpile( + public WebViewComponentSourceUnit transpile( + ComponentTemplateCompilerConfiguration compilerConfiguration, + WebViewComponentTemplateCompileUnit compileUnit, CompilationUnitNode compilationUnitNode, - TokenList tokens, - String ownerComponentName, - ReaderSource readerSource - ) { - final var configuration = this.getConfiguration(); - final String templateName = ownerComponentName + "Template"; - + String templateClassName + ) throws ComponentTemplateCompileException { + final var groovyCompilerConfiguration = compilerConfiguration.getGroovyCompilerConfiguration(); final var sourceUnit = new WebViewComponentSourceUnit( - templateName, - readerSource, - this.groovyCompilationUnit.getConfiguration(), - this.groovyCompilationUnit.getClassLoader(), - this.groovyCompilationUnit.getErrorCollector() + templateClassName, + compileUnit, + groovyCompilerConfiguration, + compilerConfiguration.getGroovyClassLoader(), + new ErrorCollector(groovyCompilerConfiguration) ); + final var moduleNode = new WebViewComponentModuleNode(sourceUnit); sourceUnit.setModuleNode(moduleNode); - final String packageName = this.getPackageName(moduleNode); - moduleNode.setPackageName(packageName); + moduleNode.setPackageName(compileUnit.getDefaultPackageName()); + + moduleNode.addStarImport(GROOWT_VIEW_WEB + ".lib"); + moduleNode.addImport(COMPONENT_TEMPLATE.getNameWithoutPackage(), COMPONENT_TEMPLATE); + moduleNode.addImport(COMPONENT_CONTEXT_TYPE.getNameWithoutPackage(), COMPONENT_CONTEXT_TYPE); + moduleNode.addImport(WEB_VIEW_COMPONENT_TYPE.getNameWithoutPackage(), WEB_VIEW_COMPONENT_TYPE); + moduleNode.addStarImport("groowt.view.component.runtime"); + moduleNode.addStarImport(GROOWT_VIEW_WEB + ".runtime"); final ClassNode mainClassNode = new ClassNode( - packageName + "." + templateName, + compileUnit.getDefaultPackageName() + "." + templateClassName, ACC_PUBLIC, ClassHelper.OBJECT_TYPE ); @@ -199,15 +200,73 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler { // preamble final PreambleNode preambleNode = compilationUnitNode.getPreambleNode(); if (preambleNode != null) { - this.handlePreamble(templateName, preambleNode, mainClassNode, moduleNode); + this.handlePreamble(templateClassName, preambleNode, mainClassNode, moduleNode); } - // renderer - final var renderBlock = new BlockStatement(); + // getRenderer + // params + final Parameter componentContextParam = new Parameter(COMPONENT_CONTEXT_TYPE, COMPONENT_CONTEXT_NAME); + final Parameter writerParam = new Parameter(COMPONENT_WRITER_TYPE, COMPONENT_WRITER_NAME); + final VariableExpression renderContextVariable = new VariableExpression( + RENDER_CONTEXT_NAME, + RENDER_CONTEXT_TYPE + ); - final TranspilerState state = TranspilerState.withDefaultRootScope(); - renderBlock.setVariableScope(state.currentScope()); + // closure body + final BlockStatement renderBlock = new BlockStatement(); + final TranspilerState state = TranspilerState.withRootScope( + componentContextParam, + writerParam, + renderContextVariable + ); + renderBlock.setVariableScope(state.getCurrentScope()); + + // init: construct RenderContext + final ConstructorCallExpression renderContextConstructor = new ConstructorCallExpression( + RENDER_CONTEXT_IMPLEMENTATION, + new ArgumentListExpression( + new VariableExpression(componentContextParam), // component context + new VariableExpression(writerParam) + ) + ); + final BinaryExpression renderContextAssignExpr = new DeclarationExpression( + renderContextVariable, + getAssignToken(), + renderContextConstructor + ); + renderBlock.addStatement(new ExpressionStatement(renderContextAssignExpr)); + + // init: componentContext.renderContext = renderContext + final BinaryExpression componentContextRenderContextAssign = new BinaryExpression( + new PropertyExpression(new VariableExpression(componentContextParam), "renderContext"), + getAssignToken(), + renderContextVariable + ); + renderBlock.addStatement(new ExpressionStatement(componentContextRenderContextAssign)); + + // init: writer.renderContext = renderContext + final BinaryExpression writerRenderContextAssign = new BinaryExpression( + new PropertyExpression(new VariableExpression(writerParam), "renderContext"), + getAssignToken(), + renderContextVariable + ); + renderBlock.addStatement(new ExpressionStatement(writerRenderContextAssign)); + + // init: writer.componentContext = componentContext + final BinaryExpression writerComponentContextAssign = new BinaryExpression( + new PropertyExpression(new VariableExpression(writerParam), "componentContext"), + getAssignToken(), + new VariableExpression(componentContextParam) + ); + renderBlock.addStatement(new ExpressionStatement(writerComponentContextAssign)); + + // actual rendering of body + final var configuration = this.getConfiguration( + compileUnit, + moduleNode, + compilerConfiguration.getGroovyClassLoader() + ); final BodyNode bodyNode = compilationUnitNode.getBodyNode(); if (bodyNode != null) { final var appendOrAddStatementFactory = configuration.getAppendOrAddStatementFactory(); @@ -215,22 +274,26 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler { configuration.getBodyTranspiler() .transpileBody( compilationUnitNode.getBodyNode(), - (source, expr) -> appendOrAddStatementFactory.addOrAppend(source, state, action -> { - if (action == AppendOrAddStatementFactory.Action.ADD) { - throw new IllegalStateException("Should not be adding here!"); - } - return expr; - }), - state + (source, expr) -> appendOrAddStatementFactory.addOrAppend( + source, + state, + action -> { + if (action == AppendOrAddStatementFactory.Action.ADD) { + throw new WebViewComponentBugError(new IllegalStateException( + "Should not be adding from document root!" + )); + } + return expr; + }), + state ) ); } + renderBlock.addStatement(new ReturnStatement(ConstantExpression.NULL)); + final ClosureExpression renderer = new ClosureExpression( - new Parameter[] { - (Parameter) state.context(), - (Parameter) state.out() - }, + new Parameter[] { componentContextParam, writerParam }, renderBlock ); @@ -250,7 +313,16 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler { ); mainClassNode.addMethod(getRenderer); - this.groovyCompilationUnit.addSource(sourceUnit); + if (state.hasErrors()) { + final List errors = state.getErrors(); + if (errors.size() == 1) { + throw new WebViewComponentTemplateCompileException(compileUnit, errors.getFirst()); + } else { + throw new MultipleWebViewComponentCompileErrorsException(compileUnit, errors); + } + } + + return sourceUnit; } } diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultTranspilerConfiguration.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultTranspilerConfiguration.java index d15d171..16c55b7 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultTranspilerConfiguration.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultTranspilerConfiguration.java @@ -1,25 +1,26 @@ package groowt.view.web.transpile; -import jakarta.inject.Inject; +import groowt.view.web.transpile.resolve.ComponentClassNodeResolver; +import groowt.view.web.util.Provider; public class DefaultTranspilerConfiguration implements TranspilerConfiguration { private final AppendOrAddStatementFactory appendOrAddStatementFactory = new DefaultAppendOrAddStatementFactory(); private final BodyTranspiler bodyTranspiler; + private final ValueNodeTranspiler valueNodeTranspiler; - @Inject - public DefaultTranspilerConfiguration() { + public DefaultTranspilerConfiguration(ComponentClassNodeResolver classNodeResolver) { final var positionSetter = new SimplePositionSetter(); final var jStringTranspiler = new DefaultJStringTranspiler(positionSetter); final var gStringTranspiler = new DefaultGStringTranspiler(positionSetter, jStringTranspiler); - final var componentTranspiler = new DefaultComponentTranspiler(); - final var valueNodeTranspiler = new DefaultValueNodeTranspiler(componentTranspiler); - + final var componentTranspiler = new DefaultComponentTranspiler( + Provider.of(this.appendOrAddStatementFactory), + Provider.of(classNodeResolver), + Provider.ofLazy(this::getValueNodeTranspiler), + Provider.ofLazy(this::getBodyTranspiler) + ); + this.valueNodeTranspiler = new DefaultValueNodeTranspiler(componentTranspiler); this.bodyTranspiler = new DefaultBodyTranspiler(gStringTranspiler, jStringTranspiler, componentTranspiler); - - componentTranspiler.setBodyTranspiler(this.bodyTranspiler); - componentTranspiler.setValueNodeTranspiler(valueNodeTranspiler); - componentTranspiler.setAppendOrAddStatementFactory(this.appendOrAddStatementFactory); } @Override @@ -32,4 +33,8 @@ public class DefaultTranspilerConfiguration implements TranspilerConfiguration { return this.appendOrAddStatementFactory; } + protected ValueNodeTranspiler getValueNodeTranspiler() { + return this.valueNodeTranspiler; + } + } diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultValueNodeTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultValueNodeTranspiler.java index 3f603e9..ea103cb 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultValueNodeTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultValueNodeTranspiler.java @@ -4,7 +4,6 @@ import groowt.view.web.ast.node.*; import groowt.view.web.transpile.TranspilerUtil.TranspilerState; import groowt.view.web.transpile.util.GroovyUtil; import groowt.view.web.transpile.util.GroovyUtil.ConvertResult; -import jakarta.inject.Inject; import org.codehaus.groovy.ast.Parameter; import org.codehaus.groovy.ast.expr.ClosureExpression; import org.codehaus.groovy.ast.expr.ConstantExpression; @@ -14,8 +13,9 @@ import org.codehaus.groovy.ast.stmt.EmptyStatement; import org.codehaus.groovy.ast.stmt.ExpressionStatement; import org.jetbrains.annotations.Nullable; -import static groowt.view.web.transpile.TranspilerUtil.makeStringLiteral; +import static groowt.view.web.transpile.TranspilerUtil.getStringLiteral; +// TODO: set positions public class DefaultValueNodeTranspiler implements ValueNodeTranspiler { private final ComponentTranspiler componentTranspiler; @@ -32,6 +32,7 @@ public class DefaultValueNodeTranspiler implements ValueNodeTranspiler { throw new IllegalStateException("block statement is null or empty"); } final ExpressionStatement exprStmt = (ExpressionStatement) blockStatement.getStatements().getFirst(); + // TODO: set pos return (ClosureExpression) exprStmt.getExpression(); } @@ -43,25 +44,26 @@ public class DefaultValueNodeTranspiler implements ValueNodeTranspiler { throw new IllegalStateException("block statement is null or empty"); } final ExpressionStatement exprStmt = (ExpressionStatement) blockStatement.getStatements().getFirst(); + // TODO: set pos return exprStmt.getExpression(); } private ConstantExpression jStringValue(JStringValueNode jStringValueNode) { - return makeStringLiteral(jStringValueNode.getContent()); + return getStringLiteral(jStringValueNode.getContent()); // TODO: set pos } private ClosureExpression emptyClosureValue(EmptyClosureValueNode emptyClosureValueNode) { - return new ClosureExpression(Parameter.EMPTY_ARRAY, EmptyStatement.INSTANCE); + return new ClosureExpression(Parameter.EMPTY_ARRAY, EmptyStatement.INSTANCE); // TODO: set pos } private ClosureExpression componentValue(ComponentValueNode componentValueNode, TranspilerState state) { return new ClosureExpression( Parameter.EMPTY_ARRAY, - this.componentTranspiler.createComponentStatements( + new BlockStatement(this.componentTranspiler.createComponentStatements( componentValueNode.getComponentNode(), state - ) - ); + ), state.getCurrentScope()) + ); // TODO: set pos } @Override diff --git a/web-views/src/main/java/groowt/view/web/transpile/GroovyTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/GroovyTranspiler.java index 2d5b494..5a2fa78 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/GroovyTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/GroovyTranspiler.java @@ -1,16 +1,17 @@ package groowt.view.web.transpile; -import groowt.view.web.antlr.TokenList; +import groowt.view.component.compiler.ComponentTemplateCompileException; +import groowt.view.component.compiler.ComponentTemplateCompilerConfiguration; import groowt.view.web.ast.node.CompilationUnitNode; -import org.codehaus.groovy.control.io.ReaderSource; +import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit; public interface GroovyTranspiler { - void transpile( + WebViewComponentSourceUnit transpile( + ComponentTemplateCompilerConfiguration compilerConfiguration, + WebViewComponentTemplateCompileUnit compileUnit, CompilationUnitNode compilationUnitNode, - TokenList tokens, - String ownerComponentName, - ReaderSource readerSource - ); + String templateClassName + ) throws ComponentTemplateCompileException; } diff --git a/web-views/src/main/java/groowt/view/web/transpile/TranspilerUtil.java b/web-views/src/main/java/groowt/view/web/transpile/TranspilerUtil.java index 98773b1..bb9e9e8 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/TranspilerUtil.java +++ b/web-views/src/main/java/groowt/view/web/transpile/TranspilerUtil.java @@ -1,29 +1,35 @@ package groowt.view.web.transpile; import groovy.lang.Tuple2; -import groovy.transform.Field; -import groowt.view.component.context.ComponentContext; import groowt.view.component.ComponentTemplate; -import groowt.view.web.runtime.WebViewComponentWriter; +import groowt.view.component.compiler.ComponentTemplateCompileException; +import groowt.view.component.context.ComponentContext; +import groowt.view.component.runtime.ComponentWriter; +import groowt.view.web.WebViewComponent; +import groowt.view.web.runtime.WebViewComponentRenderContext; import groowt.view.web.util.SourcePosition; import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.expr.ConstantExpression; +import org.codehaus.groovy.ast.expr.VariableExpression; +import org.codehaus.groovy.syntax.Token; +import org.codehaus.groovy.syntax.Types; +import org.jetbrains.annotations.TestOnly; -import java.util.Deque; -import java.util.LinkedList; -import java.util.Objects; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; public final class TranspilerUtil { public static final ClassNode COMPONENT_TEMPLATE = ClassHelper.make(ComponentTemplate.class); - public static final ClassNode OUT_TYPE = ClassHelper.make(WebViewComponentWriter.class); - public static final ClassNode CONTEXT_CLASSNODE = ClassHelper.make(ComponentContext.class); - public static final ClassNode FIELD_ANNOTATION = ClassHelper.make(Field.class); + public static final ClassNode COMPONENT_CONTEXT_TYPE = ClassHelper.make(ComponentContext.class); + public static final ClassNode COMPONENT_WRITER_TYPE = ClassHelper.make(ComponentWriter.class); + public static final ClassNode RENDER_CONTEXT_TYPE = ClassHelper.make(WebViewComponentRenderContext.class); + public static final ClassNode WEB_VIEW_COMPONENT_TYPE = ClassHelper.make(WebViewComponent.class); public static final String GROOWT_VIEW_WEB = "groowt.view.web"; - public static final String OUT = "out"; - public static final String CONTEXT = "context"; + public static final String COMPONENT_CONTEXT_NAME = "componentContext"; + public static final String COMPONENT_WRITER_NAME = "out"; + public static final String RENDER_CONTEXT_NAME = "renderContext"; public static final String GET_RENDERER = "getRenderer"; public static final String APPEND = "append"; public static final String ADD = "add"; @@ -35,37 +41,62 @@ public final class TranspilerUtil { ); } - public static ConstantExpression makeStringLiteral(String content) { + public static ConstantExpression getStringLiteral(String content) { final var e = new ConstantExpression(content); e.setNodeMetaData("_IS_STRING", true); return e; } + public static Token getAssignToken() { + return new Token(Types.ASSIGN, "=", -1, -1); + } + public static final class TranspilerState { - public static TranspilerState withDefaultRootScope() { - final var contextParam = new Parameter(CONTEXT_CLASSNODE, CONTEXT); - final var outParam = new Parameter(OUT_TYPE, OUT); - + public static TranspilerState withRootScope( + Parameter componentContext, + Parameter writer, + Variable renderContext + ) { final VariableScope rootScope = new VariableScope(); - rootScope.putDeclaredVariable(contextParam); - rootScope.putDeclaredVariable(outParam); + rootScope.putDeclaredVariable(componentContext); + rootScope.putDeclaredVariable(writer); + rootScope.putDeclaredVariable(renderContext); return new TranspilerState(rootScope); } - public static TranspilerState withRootScope(VariableScope rootScope) { + @TestOnly + public static TranspilerState withDefaultRootScope() { + final VariableScope rootScope = new VariableScope(); + rootScope.putDeclaredVariable(new Parameter(COMPONENT_CONTEXT_TYPE, COMPONENT_CONTEXT_NAME)); + rootScope.putDeclaredVariable(new Parameter(COMPONENT_WRITER_TYPE, COMPONENT_WRITER_NAME)); + rootScope.putDeclaredVariable(new VariableExpression(RENDER_CONTEXT_NAME, RENDER_CONTEXT_TYPE)); return new TranspilerState(rootScope); } - private final AtomicInteger componentCounter = new AtomicInteger(); + private final AtomicInteger componentNumberCounter = new AtomicInteger(); private final Deque scopeStack = new LinkedList<>(); + private final Deque componentStack = new LinkedList<>(); + private final Deque resolvedStack = new LinkedList<>(); private final Deque childCollectorStack = new LinkedList<>(); + private final List errors = new ArrayList<>(); + + private int lastComponentNumber; private TranspilerState(VariableScope rootScope) { this.scopeStack.push(rootScope); } + public int getCurrentComponentNumber() { + return this.lastComponentNumber; + } + + public int newComponentNumber() { + this.lastComponentNumber = this.componentNumberCounter.getAndIncrement(); + return this.lastComponentNumber; + } + public VariableScope pushScope() { final VariableScope parent = this.scopeStack.peek(); final VariableScope result = new VariableScope(parent); @@ -77,24 +108,16 @@ public final class TranspilerUtil { this.scopeStack.pop(); } - public VariableScope currentScope() { + public VariableScope getCurrentScope() { return Objects.requireNonNull(this.scopeStack.peek()); } - public int newComponentNumber() { - return this.componentCounter.getAndIncrement(); + public void putToCurrentScope(Variable variable) { + this.getCurrentScope().putDeclaredVariable(variable); } - public Variable out() { - return this.getDeclaredVariable(OUT); - } - - public Variable context() { - return this.getDeclaredVariable(CONTEXT); - } - - public Variable getDeclaredVariable(String name) { - VariableScope scope = this.currentScope(); + private Variable getDeclaredVariable(String name) { + VariableScope scope = this.getCurrentScope(); while (scope != null) { final Variable potential = scope.getDeclaredVariable(name); if (potential != null) { @@ -106,14 +129,46 @@ public final class TranspilerUtil { throw new NullPointerException("Cannot find variable: " + name); } - public void popChildCollector() { - this.childCollectorStack.pop(); + public Variable getWriter() { + return this.getDeclaredVariable(COMPONENT_WRITER_NAME); + } + + public Variable getRenderContext() { + return this.getDeclaredVariable(RENDER_CONTEXT_NAME); + } + + public void pushComponent(Variable componentVariable) { + this.componentStack.push(componentVariable); + } + + public void popComponent() { + this.componentStack.pop(); + } + + public Variable getCurrentComponent() { + return Objects.requireNonNull(this.componentStack.peek()); + } + + public void pushResolved(Variable resolvedVariable) { + this.resolvedStack.push(resolvedVariable); + } + + public void popResolved() { + this.resolvedStack.pop(); + } + + public Variable getCurrentResolved() { + return Objects.requireNonNull(this.resolvedStack.peek()); } public void pushChildCollector(Variable childCollector) { this.childCollectorStack.push(childCollector); } + public void popChildCollector() { + this.childCollectorStack.pop(); + } + public Variable getCurrentChildCollector() { return Objects.requireNonNull(this.childCollectorStack.peek()); } @@ -122,6 +177,22 @@ public final class TranspilerUtil { return this.childCollectorStack.peek() != null; } + public void addError(ComponentTemplateCompileException error) { + this.errors.add(error); + } + + public void addErrors(Collection errors) { + this.errors.addAll(errors); + } + + public boolean hasErrors() { + return !this.errors.isEmpty(); + } + + public List getErrors() { + return new ArrayList<>(this.errors); + } + } private TranspilerUtil() {} diff --git a/web-views/src/main/java/groowt/view/web/transpile/resolve/CachingComponentClassNodeResolver.java b/web-views/src/main/java/groowt/view/web/transpile/resolve/CachingComponentClassNodeResolver.java new file mode 100644 index 0000000..6a7e013 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/resolve/CachingComponentClassNodeResolver.java @@ -0,0 +1,60 @@ +package groowt.view.web.transpile.resolve; + +import groowt.view.web.WebViewComponent; +import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit; +import groowt.view.web.util.Either; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; + +import java.util.ArrayList; +import java.util.List; + +public class CachingComponentClassNodeResolver implements ComponentClassNodeResolver { + + private final List classNodes = new ArrayList<>(); + + protected final WebViewComponentTemplateCompileUnit compileUnit; + + public CachingComponentClassNodeResolver(WebViewComponentTemplateCompileUnit compileUnit) { + this.compileUnit = compileUnit; + } + + public void addClass(Class clazz) { + this.classNodes.add(ClassHelper.make(clazz)); + } + + public void addClassNode(ClassNode classNode) { + this.classNodes.add(classNode); + } + + @Override + public Either getClassForFqn(String fqn) { + for (final var classNode : this.classNodes) { + if (classNode.getName().equals(fqn)) { + return Either.right(classNode); + } + } + return Either.left(new ClassNodeResolveException( + this.compileUnit, + fqn, + "Could not resolve ClassNode for fqn: " + fqn, + null + )); + } + + @Override + public Either getClassForNameWithoutPackage(String nameWithoutPackage) { + for (final var classNode : this.classNodes) { + if (classNode.getNameWithoutPackage().equals(nameWithoutPackage)) { + return Either.right(classNode); + } + } + return Either.left(new ClassNodeResolveException( + this.compileUnit, + nameWithoutPackage, + "Could not resolve ClassNode for nameWithoutPackage: " + nameWithoutPackage, + null + )); + } + +} 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 deleted file mode 100644 index c8debb3..0000000 --- a/web-views/src/main/java/groowt/view/web/transpile/resolve/ClassIdentifier.java +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index 4855165..0000000 --- a/web-views/src/main/java/groowt/view/web/transpile/resolve/ClassIdentifierWithFqn.java +++ /dev/null @@ -1,23 +0,0 @@ -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/ClassLoaderComponentClassNodeResolver.java b/web-views/src/main/java/groowt/view/web/transpile/resolve/ClassLoaderComponentClassNodeResolver.java new file mode 100644 index 0000000..47f9c98 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/resolve/ClassLoaderComponentClassNodeResolver.java @@ -0,0 +1,55 @@ +package groowt.view.web.transpile.resolve; + +import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit; +import groowt.view.web.util.Either; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ModuleNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ClassLoaderComponentClassNodeResolver extends ModuleNodeComponentClassNodeResolver { + + private static final Logger logger = LoggerFactory.getLogger(ModuleNodeComponentClassNodeResolver.class); + + protected final ClassLoader classLoader; + + public ClassLoaderComponentClassNodeResolver( + WebViewComponentTemplateCompileUnit compileUnit, + ModuleNode moduleNode, + ClassLoader classLoader + ) { + super(compileUnit, moduleNode); + this.classLoader = classLoader; + } + + protected final Either resolveWithClassLoader(String fqn) { + logger.debug("Trying to resolve {}", fqn); + try { + Class clazz = this.classLoader.loadClass(ResolveUtil.convertCanonicalNameToBinaryName(fqn)); + final var classNode = ResolveUtil.getClassNode(clazz); + return Either.right(classNode); + } catch (ClassNotFoundException classNotFoundException) { + return Either.left( + new ClassNodeResolveException( + this.compileUnit, + fqn, + "Could not find class " + fqn + " with classLoader " + + this.classLoader, + classNotFoundException + ) + ); + } + } + + @Override + public Either getClassForFqn(String fqn) { + return super.getClassForFqn(fqn).flatMapLeft(ignored -> { + final var classLoaderResult = this.resolveWithClassLoader(fqn); + if (classLoaderResult.isRight()) { + this.addClassNode(classLoaderResult.getRight()); + } + return classLoaderResult; + }); + } + +} 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 index 0d9e6de..853d3fb 100644 --- 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 @@ -1,14 +1,42 @@ package groowt.view.web.transpile.resolve; +import groowt.view.web.compiler.WebViewComponentTemplateCompileException; +import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit; 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 message, @Nullable Throwable cause) {} + final class ClassNodeResolveException extends WebViewComponentTemplateCompileException { - Either getClassForFqn(String fqn); - Either getClassForNameWithoutPackage(String nameWithoutPackage); + private final String identifier; + + public ClassNodeResolveException( + WebViewComponentTemplateCompileUnit compileUnit, + String identifier, + String message + ) { + super(compileUnit, message); + this.identifier = identifier; + } + + public ClassNodeResolveException( + WebViewComponentTemplateCompileUnit compileUnit, + String identifier, + String message, + Throwable cause + ) { + super(compileUnit, message, cause); + this.identifier = identifier; + } + + public String getIdentifier() { + return this.identifier; + } + + } + + 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 deleted file mode 100644 index ea5b348..0000000 --- a/web-views/src/main/java/groowt/view/web/transpile/resolve/DefaultComponentClassNodeResolver.java +++ /dev/null @@ -1,187 +0,0 @@ -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; - } else { - // Return the left only if we had an error that we weren't possibly expecting. - final Throwable cause = classLoaderResolved.asLeft().get().cause(); - if (!(cause instanceof ClassNotFoundException)) { - return classLoaderResolved; - } - } - - // We have a fqn which is outside the expected packages/classpath - // Make a custom class node and hope that the groovy compiler - // can resolve it later. - final ClassNode classNode = ClassHelper.make(fqn); - this.addToCache(new ClassIdentifierWithFqn(getAlias(fqn), fqn), classNode); - return Either.right(classNode); - } - - @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 given imports (not from compilation customizer) - 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/transpile/resolve/ModuleNodeComponentClassNodeResolver.java b/web-views/src/main/java/groowt/view/web/transpile/resolve/ModuleNodeComponentClassNodeResolver.java new file mode 100644 index 0000000..912afcf --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/resolve/ModuleNodeComponentClassNodeResolver.java @@ -0,0 +1,51 @@ +package groowt.view.web.transpile.resolve; + +import groowt.view.web.WebViewComponentBugError; +import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit; +import groowt.view.web.util.Either; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ModuleNode; + +public class ModuleNodeComponentClassNodeResolver extends CachingComponentClassNodeResolver { + + private final ModuleNode moduleNode; + + public ModuleNodeComponentClassNodeResolver( + WebViewComponentTemplateCompileUnit compileUnit, + ModuleNode moduleNode + ) { + super(compileUnit); + this.moduleNode = moduleNode; + } + + @Override + public Either getClassForNameWithoutPackage(String nameWithoutPackage) { + return super.getClassForNameWithoutPackage(nameWithoutPackage).flatMapLeft(ignored -> { + // try imports first + final var importedClassNode = this.moduleNode.getImportType(nameWithoutPackage); + if (importedClassNode != null) { + this.addClassNode(importedClassNode); + return Either.right(importedClassNode); + } + + // try pre-pending package and asking for fqn + final var packageName = this.moduleNode.getPackageName(); + if (packageName.endsWith(".")) { + throw new WebViewComponentBugError(new IllegalStateException("Package name illegally ends with '.'")); + } + final var fqn = this.moduleNode.getPackageName() + "." + nameWithoutPackage; + final var withPackage = this.getClassForFqn(fqn); + if (withPackage.isRight()) { + return withPackage; + } else { + return Either.left(new ClassNodeResolveException( + this.compileUnit, + nameWithoutPackage, + "Cannot resolve " + nameWithoutPackage + + " from imports, package-local classes, or pre-added classes." + )); + } + }); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/resolve/ResolveUtil.java b/web-views/src/main/java/groowt/view/web/transpile/resolve/ResolveUtil.java new file mode 100644 index 0000000..ad0ee94 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/resolve/ResolveUtil.java @@ -0,0 +1,31 @@ +package groowt.view.web.transpile.resolve; + +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; + +import java.util.regex.Pattern; + +public final class ResolveUtil { + + private static final Pattern packageSplitter = Pattern.compile("^(?(?>\\p{Ll}[^.]*\\.)*)(?\\p{Lu}[^.]*)(?(?>\\.\\p{Lu}[^.]*)*)$"); + + public static ClassNode getClassNode(Class clazz) { + return ClassHelper.makeCached(clazz); + } + + public static String convertCanonicalNameToBinaryName(String canonicalName) { + final var matcher = packageSplitter.matcher(canonicalName); + if (matcher.matches()) { + return new StringBuilder() + .append(matcher.group("package")) + .append(matcher.group("top")) + .append(matcher.group("members").replaceAll("\\.", "\\$")) + .toString(); + } else { + throw new IllegalArgumentException("Cannot split apart " + canonicalName); + } + } + + private ResolveUtil() {} + +} 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 index 2e9719d..d1ef6c1 100644 --- a/web-views/src/main/java/groowt/view/web/util/Either.java +++ b/web-views/src/main/java/groowt/view/web/util/Either.java @@ -1,28 +1,43 @@ package groowt.view.web.util; +import java.util.function.Function; + public sealed interface Either { @SuppressWarnings("unchecked") static Either left(E error) { - return (Either) new Left<>((Class) error.getClass(), error); + return (Either) new Left<>(error); } @SuppressWarnings("unchecked") static Either right(T item) { - return (Either) new Right<>((Class) item.getClass(), item); + return (Either) new Right<>(item); } - record Left(Class errorClass, E error) implements Either { + final class Left implements Either { + + private final E error; + + public Left(E error) { + this.error = error; + } public E get() { - return this.errorClass.cast(this.error); + return this.error; } + } - record Right(Class itemClass, T item) implements Either { + final class Right implements Either { + + private final T item; + + public Right(T item) { + this.item = item; + } public T get() { - return this.itemClass.cast(this.item); + return this.item; } } @@ -45,4 +60,31 @@ public sealed interface Either { return (Right) this; } + default E getLeft() { + return this.asLeft().get(); + } + + default T getRight() { + return this.asRight().get(); + } + + @SuppressWarnings("unchecked") + default Either mapLeft(Function onLeft) { + if (this.isLeft()) { + return (Either) new Right<>(onLeft.apply(this.getLeft())); + } else { + return this; + } + } + + @SuppressWarnings("unchecked") + default Either flatMapLeft(Function> onLeft) { + if (this.isLeft()) { + final var error = this.getLeft(); + return (Either) onLeft.apply(error); + } else { + return this; + } + } + } diff --git a/web-views/src/main/java/groowt/view/web/util/LazyProvider.java b/web-views/src/main/java/groowt/view/web/util/LazyProvider.java new file mode 100644 index 0000000..7db26c1 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/LazyProvider.java @@ -0,0 +1,26 @@ +package groowt.view.web.util; + +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +final class LazyProvider implements Provider { + + private final Provider lazy; + + public LazyProvider(Supplier supplier) { + this.lazy = () -> { + final @Nullable T t = supplier.get(); + if (t == null) { + throw new NullPointerException("This Provider has a null value."); + } + return t; + }; + } + + @Override + public T get() { + return this.lazy.get(); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/Property.java b/web-views/src/main/java/groowt/view/web/util/Property.java new file mode 100644 index 0000000..719a980 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/Property.java @@ -0,0 +1,5 @@ +package groowt.view.web.util; + +public interface Property extends Provider { + void set(T t); +} diff --git a/web-views/src/main/java/groowt/view/web/util/Provider.java b/web-views/src/main/java/groowt/view/web/util/Provider.java new file mode 100644 index 0000000..aa4a4fe --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/Provider.java @@ -0,0 +1,17 @@ +package groowt.view.web.util; + +import java.util.function.Supplier; + +public interface Provider { + + static Provider of(T t) { + return new SimpleProvider<>(t); + } + + static Provider ofLazy(Supplier tSupplier) { + return new LazyProvider<>(tSupplier); + } + + T get(); + +} diff --git a/web-views/src/main/java/groowt/view/web/util/SimpleProvider.java b/web-views/src/main/java/groowt/view/web/util/SimpleProvider.java new file mode 100644 index 0000000..9f13f5f --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/SimpleProvider.java @@ -0,0 +1,19 @@ +package groowt.view.web.util; + +final class SimpleProvider implements Provider { + + private final T t; + + public SimpleProvider(T t) { + this.t = t; + } + + @Override + public T get() { + if (this.t == null) { + throw new NullPointerException("This Provider has a null value."); + } + return t; + } + +} diff --git a/web-views/src/sketching/groovy/groowt/view/web/sketching/Greeters.groovy b/web-views/src/sketching/groovy/groowt/view/web/sketching/Greeters.groovy new file mode 100644 index 0000000..623b830 --- /dev/null +++ b/web-views/src/sketching/groovy/groowt/view/web/sketching/Greeters.groovy @@ -0,0 +1,18 @@ +package groowt.view.web.sketching + +import groowt.view.web.BaseWebViewComponent + +class Greeters { + + static class Simple extends BaseWebViewComponent { + + String target + + Simple(Map attr) { + super('Hello, $target!') + this.target = attr.target ?: 'world' + } + + } + +} diff --git a/web-views/src/test/groovy/groowt/view/web/DefaultWebViewComponentTests.groovy b/web-views/src/test/groovy/groowt/view/web/BaseWebViewComponentTests.groovy similarity index 51% rename from web-views/src/test/groovy/groowt/view/web/DefaultWebViewComponentTests.groovy rename to web-views/src/test/groovy/groowt/view/web/BaseWebViewComponentTests.groovy index 19eb6f6..a758d7f 100644 --- a/web-views/src/test/groovy/groowt/view/web/DefaultWebViewComponentTests.groovy +++ b/web-views/src/test/groovy/groowt/view/web/BaseWebViewComponentTests.groovy @@ -1,15 +1,17 @@ package groowt.view.web -import groowt.view.component.factory.ComponentFactoryBase +import groowt.view.component.factory.ComponentFactories +import groowt.view.component.factory.ComponentFactory import groowt.view.web.lib.AbstractWebViewComponentTests -import groowt.view.web.lib.WithContext import org.junit.jupiter.api.Test -import static org.junit.jupiter.api.Assertions.assertEquals +class BaseWebViewComponentTests extends AbstractWebViewComponentTests { -class DefaultWebViewComponentTests extends AbstractWebViewComponentTests { + private static final ComponentFactory greeterFactory = WebViewComponentFactories.withAttr(Greeter) { + new Greeter(it) + } - private static final class Greeter extends DefaultWebViewComponent { + static final class Greeter extends BaseWebViewComponent { private final String target @@ -24,18 +26,10 @@ class DefaultWebViewComponentTests extends AbstractWebViewComponentTests { } - private static final class GreeterFactory extends ComponentFactoryBase { - - Greeter doCreate(Map attr) { - return new Greeter(attr) - } - - } - - private static final class UsingGreeter extends DefaultWebViewComponent { + static final class UsingGreeter extends BaseWebViewComponent { UsingGreeter() { - super("") + super("") } } @@ -57,19 +51,19 @@ class DefaultWebViewComponentTests extends AbstractWebViewComponentTests { void nestedGreeter() { def context = this.context { this.configureContext(it) - currentScope.add('Greeter', new GreeterFactory()) + currentScope.add(Greeter, greeterFactory) } - this.doTest('', 'Hello, World!', context) + this.doTest('', 'Hello, World!', context) } @Test void doubleNested() { def context = this.context { this.configureContext(it) - currentScope.add('UsingGreeter') { new UsingGreeter() } - currentScope.add('Greeter', new GreeterFactory()) + currentScope.add(UsingGreeter, ComponentFactories.ofSupplier { new UsingGreeter() }) + currentScope.add(Greeter, greeterFactory) } - this.doTest('', 'Hello, World!', context) + this.doTest('', 'Hello, World!', context) } } diff --git a/web-views/src/test/groovy/groowt/view/web/lib/EchoTests.groovy b/web-views/src/test/groovy/groowt/view/web/lib/EchoTests.groovy index 25e92c0..82d27e2 100644 --- a/web-views/src/test/groovy/groowt/view/web/lib/EchoTests.groovy +++ b/web-views/src/test/groovy/groowt/view/web/lib/EchoTests.groovy @@ -1,31 +1,23 @@ package groowt.view.web.lib -import groowt.view.web.WebViewComponentContext -import org.junit.jupiter.api.Disabled + import org.junit.jupiter.api.Test class EchoTests extends AbstractWebViewComponentTests { - @Override - void configureContext(WebViewComponentContext context) { - super.configureContext(context) - context.currentScope.add('Echo', Echo.FACTORY) - } - @Test void selfClose() { - this.doTest('', '') + this.doTest('', '') } @Test void noSelfClose() { - this.doTest('', '') + this.doTest('', '') } @Test - @Disabled("Not possible to render children directly to a writer yet.") void withChildren() { - this.doTest('Hello, World!', 'Hello, World!') + this.doTest('Hello, World!', 'Hello, World!') } } diff --git a/web-views/src/test/groovy/groowt/view/web/lib/FragmentTests.groovy b/web-views/src/test/groovy/groowt/view/web/lib/FragmentTests.groovy index 80b71f7..16c4566 100644 --- a/web-views/src/test/groovy/groowt/view/web/lib/FragmentTests.groovy +++ b/web-views/src/test/groovy/groowt/view/web/lib/FragmentTests.groovy @@ -1,35 +1,33 @@ package groowt.view.web.lib -import groowt.view.component.context.ComponentContext -import groowt.view.web.DefaultWebViewComponent -import groowt.view.web.DefaultWebViewComponentContext -import groowt.view.component.factory.ComponentTemplateSource +import groowt.view.web.BaseWebViewComponent +import groowt.view.web.WebViewComponentContext import org.junit.jupiter.api.Test import static groowt.view.web.WebViewComponentFactories.withAttr class FragmentTests extends AbstractWebViewComponentTests { - static class Greeter extends DefaultWebViewComponent { + static class Greeter extends BaseWebViewComponent { String greeting Greeter(Map attr) { - super(ComponentTemplateSource.of('$greeting')) + super('$greeting') greeting = attr.greeting } } - private final ComponentContext greeterContext = new DefaultWebViewComponentContext().tap { - pushDefaultScope() - def greeterFactory = withAttr(Greeter.&new) - currentScope.add('Greeter', greeterFactory) + @Override + void configureContext(WebViewComponentContext context) { + def greeterFactory = withAttr(Greeter, Greeter.&new) + context.currentScope.add(Greeter, greeterFactory) } @Test void simple() { - this.doTest('<>', 'Hello, World!', this.greeterContext) + this.doTest('<>', 'Hello, World!') } @Test @@ -37,9 +35,9 @@ class FragmentTests extends AbstractWebViewComponentTests { this.doTest( ''' <> -   +   - '''.stripIndent(), 'Hello, one! Hello, two!', this.greeterContext + '''.stripIndent(), 'Hello, one! Hello, two!' ) } diff --git a/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultBodyTranspilerTests.java b/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultBodyTranspilerTests.java index d820e1a..da762ba 100644 --- a/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultBodyTranspilerTests.java +++ b/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultBodyTranspilerTests.java @@ -1,12 +1,21 @@ package groowt.view.web.transpiler; -import groowt.view.web.transpile.*; +import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit; +import groowt.view.web.transpile.DefaultTranspilerConfiguration; +import groowt.view.web.transpile.TranspilerConfiguration; +import groowt.view.web.transpile.resolve.CachingComponentClassNodeResolver; +import org.codehaus.groovy.ast.ModuleNode; public class DefaultBodyTranspilerTests extends BodyTranspilerTests { @Override - protected TranspilerConfiguration getConfiguration() { - return new DefaultTranspilerConfiguration(); + protected TranspilerConfiguration getConfiguration( + WebViewComponentTemplateCompileUnit compileUnit, + ModuleNode moduleNode + ) { + return new DefaultTranspilerConfiguration( + new CachingComponentClassNodeResolver(compileUnit) + ); } } diff --git a/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultGroovyTranspilerTests.java b/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultGroovyTranspilerTests.java index 16c903c..88d583c 100644 --- a/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultGroovyTranspilerTests.java +++ b/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultGroovyTranspilerTests.java @@ -1,20 +1,14 @@ package groowt.view.web.transpiler; import groovy.lang.Tuple2; -import groowt.util.di.DefaultRegistryObjectFactory; import groowt.view.web.transpile.DefaultGroovyTranspiler; -import groowt.view.web.transpile.DefaultTranspilerConfiguration; import groowt.view.web.transpile.GroovyTranspiler; import org.codehaus.groovy.control.CompilationUnit; public class DefaultGroovyTranspilerTests extends GroovyTranspilerTests { protected static Tuple2 getDefaultGroovyTranspiler() { - final var cu = new CompilationUnit(); - return new Tuple2<>( - new DefaultGroovyTranspiler(cu, "groowt.view.web.transpiler", DefaultTranspilerConfiguration::new), - cu - ); + return new Tuple2<>(new DefaultGroovyTranspiler(), new CompilationUnit()); } public DefaultGroovyTranspilerTests() { diff --git a/web-views/src/test/groovy/groowt/view/web/transpiler/resolve/ResolveUtilTests.java b/web-views/src/test/groovy/groowt/view/web/transpiler/resolve/ResolveUtilTests.java new file mode 100644 index 0000000..a1be720 --- /dev/null +++ b/web-views/src/test/groovy/groowt/view/web/transpiler/resolve/ResolveUtilTests.java @@ -0,0 +1,25 @@ +package groowt.view.web.transpiler.resolve; + +import org.junit.jupiter.api.Test; + +import static groowt.view.web.transpile.resolve.ResolveUtil.convertCanonicalNameToBinaryName; +import static org.junit.jupiter.api.Assertions.*; + +public class ResolveUtilTests { + + @Test + public void abcABC() { + assertEquals("a.b.c.A$B$C", convertCanonicalNameToBinaryName("a.b.c.A.B.C")); + } + + @Test + public void ABC() { + assertEquals("A$B$C", convertCanonicalNameToBinaryName("A.B.C")); + } + + @Test + public void abcA() { + assertEquals("a.b.c.A", convertCanonicalNameToBinaryName("a.b.c.A")); + } + +} diff --git a/web-views/src/main/resources/log4j2.xml b/web-views/src/test/resources/log4j2.xml similarity index 95% rename from web-views/src/main/resources/log4j2.xml rename to web-views/src/test/resources/log4j2.xml index 5081d05..62ffc5c 100644 --- a/web-views/src/main/resources/log4j2.xml +++ b/web-views/src/test/resources/log4j2.xml @@ -10,7 +10,7 @@ - + diff --git a/web-views/src/testFixtures/groovy/groowt/view/web/lib/AbstractWebViewComponentTests.groovy b/web-views/src/testFixtures/groovy/groowt/view/web/lib/AbstractWebViewComponentTests.groovy index ea98ec0..d1bc6c9 100644 --- a/web-views/src/testFixtures/groovy/groowt/view/web/lib/AbstractWebViewComponentTests.groovy +++ b/web-views/src/testFixtures/groovy/groowt/view/web/lib/AbstractWebViewComponentTests.groovy @@ -1,30 +1,28 @@ package groowt.view.web.lib - +import groowt.view.component.compiler.DefaultComponentTemplateCompilerConfiguration +import groowt.view.component.compiler.SimpleComponentTemplateClassFactory +import groowt.view.component.compiler.source.ComponentTemplateSource import groowt.view.component.context.ComponentContext -import groowt.view.component.factory.ComponentTemplateSource +import groowt.view.component.runtime.DefaultComponentWriter import groowt.view.web.compiler.DefaultWebViewComponentTemplateCompiler -import groowt.view.web.compiler.WebViewComponentTemplateCompiler -import groowt.view.web.runtime.DefaultWebViewComponentWriter -import org.codehaus.groovy.control.CompilerConfiguration import static org.junit.jupiter.api.Assertions.assertEquals abstract class AbstractWebViewComponentTests implements WithContext { - protected WebViewComponentTemplateCompiler compiler() { - new DefaultWebViewComponentTemplateCompiler( - new GroovyClassLoader(this.class.classLoader), - CompilerConfiguration.DEFAULT, - this.class.packageName - ) - } - protected void doTest(Reader sourceReader, String expected, ComponentContext context) { - def template = this.compiler().compileAndGetAnonymous(ComponentTemplateSource.of(sourceReader)) - def renderer = template.getRenderer() + def compileResult = new DefaultWebViewComponentTemplateCompiler( + new DefaultComponentTemplateCompilerConfiguration() + ).compileAnonymous(ComponentTemplateSource.of(sourceReader), this.class.packageName) + + def factory = new SimpleComponentTemplateClassFactory() + def templateClass = factory.getTemplateClass(compileResult) + + def renderer = templateClass.getConstructor().newInstance().getRenderer() + def sw = new StringWriter() - def out = new DefaultWebViewComponentWriter(sw) + def out = new DefaultComponentWriter(sw) renderer.call(context, out) assertEquals(expected, sw.toString()) } diff --git a/web-views/src/testFixtures/groovy/groowt/view/web/lib/WithContext.groovy b/web-views/src/testFixtures/groovy/groowt/view/web/lib/WithContext.groovy index b55cdd6..375c624 100644 --- a/web-views/src/testFixtures/groovy/groowt/view/web/lib/WithContext.groovy +++ b/web-views/src/testFixtures/groovy/groowt/view/web/lib/WithContext.groovy @@ -10,23 +10,14 @@ trait WithContext { WebViewComponentContext context( @ClosureParams(value = SimpleType, options = 'groowt.view.web.WebViewComponentContext') @DelegatesTo(value = WebViewComponentContext) - Closure configure + Closure configure = { configureContext(it) } ) { new DefaultWebViewComponentContext().tap { - pushDefaultScope() configure.delegate = it configure(it) } } - WebViewComponentContext context() { - new DefaultWebViewComponentContext().tap { - configureContext(it) - } - } - - void configureContext(WebViewComponentContext context) { - context.pushDefaultScope() - } + void configureContext(WebViewComponentContext context) {} } diff --git a/web-views/src/testFixtures/java/groowt/view/web/transpiler/BodyTranspilerTests.java b/web-views/src/testFixtures/java/groowt/view/web/transpiler/BodyTranspilerTests.java index 9ca8ce5..921e04e 100644 --- a/web-views/src/testFixtures/java/groowt/view/web/transpiler/BodyTranspilerTests.java +++ b/web-views/src/testFixtures/java/groowt/view/web/transpiler/BodyTranspilerTests.java @@ -6,23 +6,34 @@ import groowt.view.web.ast.DefaultAstBuilder; import groowt.view.web.ast.DefaultNodeFactory; import groowt.view.web.ast.node.BodyNode; import groowt.view.web.ast.node.CompilationUnitNode; +import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit; import groowt.view.web.transpile.BodyTranspiler; import groowt.view.web.transpile.TranspilerConfiguration; import groowt.view.web.transpile.TranspilerUtil; +import org.codehaus.groovy.ast.ModuleNode; import org.codehaus.groovy.ast.expr.ConstantExpression; import org.codehaus.groovy.ast.expr.MethodCallExpression; import org.codehaus.groovy.ast.expr.TupleExpression; import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.ExpressionStatement; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.*; +@ExtendWith(MockitoExtension.class) public abstract class BodyTranspilerTests { - protected abstract TranspilerConfiguration getConfiguration(); + protected abstract TranspilerConfiguration getConfiguration( + WebViewComponentTemplateCompileUnit compileUnit, + ModuleNode moduleNode + ); - protected record BuildResult(BodyNode bodyNode, TokenList tokenList) {} + protected record BuildResult(BodyNode bodyNode, TokenList tokenList) { + + } protected BuildResult build(String source) { final var parseResult = ParserUtil.parseCompilationUnit(source); @@ -36,24 +47,29 @@ public abstract class BodyTranspilerTests { return new BuildResult(bodyNode, tokenList); } - protected BodyTranspiler getBodyTranspiler() { - return this.getConfiguration().getBodyTranspiler(); + protected BodyTranspiler getBodyTranspiler(TranspilerConfiguration configuration) { + return configuration.getBodyTranspiler(); } @Test - public void smokeScreen() { + public void smokeScreen(@Mock WebViewComponentTemplateCompileUnit compileUnit, @Mock ModuleNode moduleNode) { assertDoesNotThrow(() -> { - this.getBodyTranspiler(); + final var configuration = this.getConfiguration(compileUnit, moduleNode); + this.getBodyTranspiler(configuration); }); } @Test - public void simpleGStringOutStatement() { + public void simpleGStringOutStatement( + @Mock WebViewComponentTemplateCompileUnit compileUnit, + @Mock ModuleNode moduleNode + ) { final var source = "Hello, $target!"; final var buildResult = this.build(source); - final var transpiler = this.getBodyTranspiler(); + final var configuration = this.getConfiguration(compileUnit, moduleNode); + final var transpiler = this.getBodyTranspiler(configuration); final var state = TranspilerUtil.TranspilerState.withDefaultRootScope(); - final var addOrAppend = this.getConfiguration().getAppendOrAddStatementFactory(); + final var addOrAppend = configuration.getAppendOrAddStatementFactory(); final BlockStatement blockStatement = transpiler.transpileBody( buildResult.bodyNode(), (node, expression) -> addOrAppend.addOrAppend(node, state, ignored -> expression), @@ -63,12 +79,16 @@ public abstract class BodyTranspilerTests { } @Test - public void simpleJStringOutStatement() { + public void simpleJStringOutStatement( + @Mock WebViewComponentTemplateCompileUnit compileUnit, + @Mock ModuleNode moduleNode + ) { final var source = "Hello, World!"; final var buildResult = this.build(source); - final var transpiler = this.getBodyTranspiler(); + final var configuration = this.getConfiguration(compileUnit, moduleNode); + final var transpiler = this.getBodyTranspiler(configuration); final var state = TranspilerUtil.TranspilerState.withDefaultRootScope(); - final var addOrAppend = this.getConfiguration().getAppendOrAddStatementFactory(); + final var addOrAppend = configuration.getAppendOrAddStatementFactory(); final BlockStatement blockStatement = transpiler.transpileBody( buildResult.bodyNode(), (node, expression) -> addOrAppend.addOrAppend(node, state, ignored -> expression), diff --git a/web-views/src/testFixtures/java/groowt/view/web/transpiler/GroovyTranspilerTests.java b/web-views/src/testFixtures/java/groowt/view/web/transpiler/GroovyTranspilerTests.java index 994dff4..2872192 100644 --- a/web-views/src/testFixtures/java/groowt/view/web/transpiler/GroovyTranspilerTests.java +++ b/web-views/src/testFixtures/java/groowt/view/web/transpiler/GroovyTranspilerTests.java @@ -1,20 +1,22 @@ package groowt.view.web.transpiler; import groovy.lang.Tuple2; -import groowt.view.component.context.ComponentContext; +import groowt.view.component.compiler.ComponentTemplateCompileException; +import groowt.view.component.compiler.DefaultComponentTemplateCompilerConfiguration; +import groowt.view.component.compiler.source.StringSource; +import groowt.view.web.BaseWebViewComponent; import groowt.view.web.antlr.ParserUtil; import groowt.view.web.antlr.TokenList; import groowt.view.web.ast.DefaultAstBuilder; import groowt.view.web.ast.DefaultNodeFactory; import groowt.view.web.ast.node.CompilationUnitNode; +import groowt.view.web.compiler.AnonymousWebViewComponent; +import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit; import groowt.view.web.transpile.GroovyTranspiler; import org.codehaus.groovy.control.CompilationUnit; import org.codehaus.groovy.control.CompilePhase; -import org.codehaus.groovy.control.CompilerConfiguration; -import org.codehaus.groovy.control.io.StringReaderSource; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.*; @@ -33,20 +35,25 @@ public abstract class GroovyTranspilerTests { @Test public void smokeScreen() {} - private void doTranspile( - String source, - String ownerComponentName - ) { + private void doTranspile(String source) { final var parseResult = ParserUtil.parseCompilationUnit(source); final var tokenList = new TokenList(parseResult.getTokenStream()); final var astBuilder = new DefaultAstBuilder(new DefaultNodeFactory(tokenList)); final var cuNode = (CompilationUnitNode) astBuilder.build(parseResult.getCompilationUnitContext()); - this.transpiler.transpile( - cuNode, - tokenList, - ownerComponentName, - new StringReaderSource(source, new CompilerConfiguration()) - ); + try { + this.transpiler.transpile( + new DefaultComponentTemplateCompilerConfiguration(), + new WebViewComponentTemplateCompileUnit( + AnonymousWebViewComponent.class, + new StringSource(source, null), + "groowt.view.web.transpiler" + ), + cuNode, + "Template" + source.hashCode() + ); + } catch (ComponentTemplateCompileException e) { + fail(e); + } assertDoesNotThrow(() -> { this.groovyCompilationUnit.compile(CompilePhase.CLASS_GENERATION.getPhaseNumber()); @@ -57,16 +64,15 @@ public abstract class GroovyTranspilerTests { * Absolute woot! 4/30/24 */ @Test - public void helloTarget(@Mock ComponentContext componentContext) { - this.doTranspile( - "Hello, $target!", - "HelloTarget" - ); + public void helloTarget() { + this.doTranspile("Hello, $target!"); } + public static final class Greeter extends BaseWebViewComponent {} + @Test public void withComponent() { - this.doTranspile("", "WithComponent"); + this.doTranspile(""); } } diff --git a/web-views/src/tools/binTemplate.gst b/web-views/src/tools/binTemplate.gst index 316501a..de59ea7 100644 --- a/web-views/src/tools/binTemplate.gst +++ b/web-views/src/tools/binTemplate.gst @@ -1,3 +1,3 @@ #/usr/bin/env bash -../gradlew toolsJar && java -cp build/libs/web-tools.jar:build/libs/web.jar $mainClassName "\$@" +../gradlew toolsJar && java -cp build/libs/web-tools-0.1.0.jar:build/libs/web-views-0.1.0.jar $mainClassName "\$@" diff --git a/web-views/src/tools/groovy/groowt/view/web/tools/ConvertToGroovy.groovy b/web-views/src/tools/groovy/groowt/view/web/tools/ConvertToGroovy.groovy index 20e9c8b..3c6ad35 100644 --- a/web-views/src/tools/groovy/groowt/view/web/tools/ConvertToGroovy.groovy +++ b/web-views/src/tools/groovy/groowt/view/web/tools/ConvertToGroovy.groovy @@ -1,17 +1,10 @@ package groowt.view.web.tools import groovy.console.ui.AstNodeToScriptVisitor -import groowt.util.di.DefaultRegistryObjectFactory -import groowt.view.web.antlr.ParserUtil -import groowt.view.web.antlr.TokenList -import groowt.view.web.ast.DefaultAstBuilder -import groowt.view.web.ast.DefaultNodeFactory -import groowt.view.web.ast.node.CompilationUnitNode -import groowt.view.web.transpile.DefaultGroovyTranspiler -import groowt.view.web.transpile.DefaultTranspilerConfiguration -import org.codehaus.groovy.control.CompilationUnit -import org.codehaus.groovy.control.CompilerConfiguration -import org.codehaus.groovy.control.io.FileReaderSource +import groowt.view.component.compiler.source.ComponentTemplateSource +import groowt.view.web.compiler.AnonymousWebViewComponent +import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit +import org.codehaus.groovy.tools.GroovyClass import picocli.CommandLine import java.util.concurrent.Callable @@ -59,46 +52,47 @@ class ConvertToGroovy implements Callable { ) File classesDir + private void writeClass(File classesDir, GroovyClass groovyClass) { + new File(classesDir, groovyClass.name + '.class').withOutputStream { + it.write(groovyClass.bytes) + } + } + @Override Integer call() throws Exception { boolean success = this.targets.inject(true) { acc, target -> def name = target.name.takeBefore('.wvc') try { - def parseResult = ParserUtil.parseCompilationUnit(target) - def tokenList = new TokenList(parseResult.tokenStream) - def astBuilder = new DefaultAstBuilder(new DefaultNodeFactory(tokenList)) - def cuNode = astBuilder.build(parseResult.compilationUnitContext) as CompilationUnitNode - def config = new CompilerConfiguration().tap { - it.targetDirectory = this.doClasses - ? this.classesDir ?: new File(target.parentFile, 'classes') - : File.createTempDir() - } - def gcu = new CompilationUnit(config) + def compileUnit = new WebViewComponentTemplateCompileUnit( + AnonymousWebViewComponent, + ComponentTemplateSource.of(target), + AnonymousWebViewComponent.packageName + ) def w = new StringWriter() def astVisitor = new AstNodeToScriptVisitor(w) - gcu.addPhaseOperation(astVisitor, this.compilePhase) + compileUnit.groovyCompilationUnit.addPhaseOperation(astVisitor, this.compilePhase) - def transpiler = new DefaultGroovyTranspiler( - gcu, - this.defaultPackageName, - { new DefaultTranspilerConfiguration() } - ) - transpiler.transpile( - cuNode, - tokenList, - name.capitalize(), - new FileReaderSource(target, new CompilerConfiguration()) - ) - gcu.compile(this.compilePhase) + def compileResult = compileUnit.compile() + + if (!this.quiet) { + println w.toString() + } if (this.writeOut) { def outFile = new File(target.parentFile, name + '.groovy') outFile.write(w.toString()) } - if (!this.quiet) { - println w.toString() + + if (this.doClasses) { + def classesDir = this.classesDir != null + ? this.classesDir + : new File(target.parentFile, 'classes') + classesDir.mkdirs() + this.writeClass(classesDir, compileResult.templateClass) + compileResult.otherClasses.each { this.writeClass(classesDir, it) } } + return true } catch (Exception e) { e.printStackTrace() diff --git a/web-views/src/tools/groovy/groowt/view/web/tools/RunTemplate.groovy b/web-views/src/tools/groovy/groowt/view/web/tools/RunTemplate.groovy index f19cd4d..c0917da 100644 --- a/web-views/src/tools/groovy/groowt/view/web/tools/RunTemplate.groovy +++ b/web-views/src/tools/groovy/groowt/view/web/tools/RunTemplate.groovy @@ -1,10 +1,12 @@ package groowt.view.web.tools +import groowt.view.component.compiler.SimpleComponentTemplateClassFactory +import groowt.view.component.compiler.source.ComponentTemplateSource import groowt.view.component.context.DefaultComponentContext -import groowt.view.component.factory.ComponentTemplateSource -import groowt.view.web.compiler.DefaultWebViewComponentTemplateCompiler -import groowt.view.web.runtime.DefaultWebViewComponentWriter -import org.codehaus.groovy.control.CompilerConfiguration +import groowt.view.component.runtime.DefaultComponentWriter +import groowt.view.web.BaseWebViewComponent +import groowt.view.web.compiler.AnonymousWebViewComponent +import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit import picocli.CommandLine import picocli.CommandLine.Command import picocli.CommandLine.Option @@ -27,20 +29,44 @@ class RunTemplate implements Callable { ) Map properties + static class PropertiesComponent extends BaseWebViewComponent { + + private final Map properties + + @SuppressWarnings('GroovyAssignabilityCheck') + PropertiesComponent(Map attr) { + super('${renderChildren()}') + this.properties = attr.properties ?: [:] + } + + @Override + Object getProperty(String propertyName) { + try { + return super.getProperty(propertyName) + } catch (Exception ignored) { + return this.properties.get(propertyName) + } + } + + } + @Override Integer call() throws Exception { - def gcl = new GroovyClassLoader(this.class.classLoader) - def compiler = new DefaultWebViewComponentTemplateCompiler( - gcl, - CompilerConfiguration.DEFAULT, + def compileUnit = new WebViewComponentTemplateCompileUnit( + AnonymousWebViewComponent, + ComponentTemplateSource.of(this.template), 'groowt.view.web.tools' ) - def template = compiler.compileAndGetAnonymous(ComponentTemplateSource.of(this.template)) + + def compileResult = compileUnit.compile() + def templateLoader = new SimpleComponentTemplateClassFactory() + def templateClass = templateLoader.getTemplateClass(compileResult) + def template = templateClass.getConstructor().newInstance() def context = new DefaultComponentContext() context.pushDefaultScope() - def componentWriter = new DefaultWebViewComponentWriter(new OutputStreamWriter(System.out)) + def componentWriter = new DefaultComponentWriter(new OutputStreamWriter(System.out)) template.renderer.call(context, componentWriter) diff --git a/web-views/src/tools/resources/log4j2.xml b/web-views/src/tools/resources/log4j2.xml new file mode 100644 index 0000000..62ffc5c --- /dev/null +++ b/web-views/src/tools/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + +