From cd63bd60a7378fd4bc5c77d2bb8dd17e97bfd1b5 Mon Sep 17 00:00:00 2001 From: JesseBrault0709 <62299747+JesseBrault0709@users.noreply.github.com> Date: Fri, 3 May 2024 12:01:45 +0200 Subject: [PATCH] Initial commit. Woot! --- .gitattributes | 9 + .gitignore | 6 + buildSrc/build.gradle | 30 + buildSrc/settings.gradle | 7 + .../groowt/gradle/GroowtConventions.gradle | 28 + .../gradle/antlr/GroowtAntlrAllTask.java | 5 + .../gradle/antlr/GroowtAntlrExtension.java | 34 ++ .../gradle/antlr/GroowtAntlrPlugin.java | 216 +++++++ .../groowt/gradle/antlr/GroowtAntlrTask.java | 70 +++ .../groowt/gradle/antlr/GroowtAntlrUtil.java | 147 +++++ .../gradle/antlr/NullableProviderList.java | 160 +++++ .../groowt/gradle/antlr/ResolvedSource.java | 90 +++ .../java/groowt/gradle/antlr/SourceSpec.java | 43 ++ .../gradle/antlr/SourceSpecContainer.java | 89 +++ .../antlr/GroowtAntlrPluginTests.groovy | 53 ++ .../gradle/antlr/GroowtAntlrUtilTests.groovy | 132 +++++ gradle.properties | 5 + gradle/libs.versions.toml | 32 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 249 ++++++++ gradlew.bat | 92 +++ settings.gradle | 12 + util/di/build.gradle | 10 + .../di/AbstractInjectingObjectFactory.java | 197 +++++++ .../di/AbstractRegistryObjectFactory.java | 113 ++++ .../src/main/java/groowt/util/di/Binding.java | 3 + .../groowt/util/di/BindingConfigurator.java | 12 + .../main/java/groowt/util/di/BindingUtil.java | 32 + .../java/groowt/util/di/ClassBinding.java | 3 + .../di/DefaultNamedRegistryExtension.java | 80 +++ .../java/groowt/util/di/DefaultRegistry.java | 200 +++++++ .../util/di/DefaultRegistryObjectFactory.java | 313 ++++++++++ .../groowt/util/di/ExtensionContainer.java | 10 + .../main/java/groowt/util/di/KeyBinder.java | 16 + .../main/java/groowt/util/di/KeyHolder.java | 7 + .../groowt/util/di/LazySingletonBinding.java | 5 + .../util/di/NamedRegistryExtension.java | 9 + .../java/groowt/util/di/ObjectFactory.java | 61 ++ .../groowt/util/di/ObjectFactoryUtil.java | 23 + .../java/groowt/util/di/ProviderBinding.java | 5 + .../java/groowt/util/di/QualifierHandler.java | 10 + .../util/di/QualifierHandlerContainer.java | 20 + .../main/java/groowt/util/di/Registry.java | 20 + .../groowt/util/di/RegistryExtension.java | 3 + .../groowt/util/di/RegistryObjectFactory.java | 99 ++++ .../util/di/RegistryObjectFactoryUtil.java | 69 +++ .../java/groowt/util/di/ScopeHandler.java | 8 + .../groowt/util/di/ScopeHandlerContainer.java | 20 + .../util/di/SimpleBindingConfigurator.java | 43 ++ .../java/groowt/util/di/SimpleKeyHolder.java | 4 + .../java/groowt/util/di/SingletonBinding.java | 3 + .../util/di/SingletonRegistryExtension.java | 22 + .../groowt/util/di/SingletonScopeHandler.java | 35 ++ .../java/groowt/util/di/annotation/Given.java | 10 + .../java/groowt/util/di/filters/Filter.java | 10 + .../groowt/util/di/filters/FilterHandler.java | 39 ++ .../util/di/filters/FilterHandlers.java | 35 ++ .../groowt/util/di/filters/FilterUtil.java | 16 + .../util/di/filters/IterableFilter.java | 10 + .../di/filters/IterableFilterHandler.java | 8 + .../di/filters/IterableFilterHandlers.java | 33 ++ .../util/di/filters/SimpleFilterHandler.java | 33 ++ .../filters/SimpleIterableFilterHandler.java | 32 + .../di/DefaultRegistryObjectFactoryTests.java | 170 ++++++ util/di/src/test/resources/log4j2.xml | 17 + util/extensible/build.gradle | 8 + .../AbstractExtensionContainer.java | 68 +++ .../groowt/util/extensible/Extensible.java | 11 + .../util/extensible/ExtensionAware.java | 12 + .../util/extensible/ExtensionContainer.java | 5 + view-components/build.gradle | 35 ++ view-components/spec.md | 349 +++++++++++ .../component/AbstractComponentFactory.java | 81 +++ .../view/component/AbstractViewComponent.java | 51 ++ .../CachingComponentTemplateCompiler.java | 27 + .../view/component/ComponentContext.java | 52 ++ .../component/ComponentCreateException.java | 22 + .../view/component/ComponentFactory.java | 20 + .../component/ComponentRenderException.java | 43 ++ .../groowt/view/component/ComponentScope.java | 41 ++ .../view/component/ComponentTemplate.java | 7 + .../component/ComponentTemplateCompiler.java | 16 + .../ComponentTemplateCreateException.java | 49 ++ .../component/DefaultComponentContext.java | 144 +++++ .../view/component/DefaultComponentScope.java | 30 + .../component/DelegatingComponentFactory.java | 20 + .../component/MissingClassTypeException.java | 17 + .../component/MissingComponentException.java | 23 + .../MissingFragmentTypeException.java | 14 + .../component/MissingStringTypeException.java | 17 + .../component/NoFactoryMissingException.java | 19 + .../groowt/view/component/ViewComponent.java | 14 + views/build.gradle | 35 ++ .../main/groovy/groowt/view/AbstractView.java | 34 ++ .../groowt/view/GStringTemplateView.java | 196 ++++++ .../view/StandardGStringTemplateView.java | 46 ++ .../StandardGStringTemplateViewMetaClass.java | 178 ++++++ .../main/groovy/groowt/view/TemplateView.java | 27 + views/src/main/groovy/groowt/view/View.java | 57 ++ .../groowt/view/GStringTemplateViewTests.java | 100 ++++ .../StandardGStringTemplateViewTests.groovy | 44 ++ .../resources/groowt/view/simplePartial.gst | 1 + web-views/.gitignore | 1 + web-views/build.gradle | 196 ++++++ web-views/sketching.gst | 20 + web-views/sketching/.gitignore | 2 + web-views/sketching/helloTarget.wvc | 1 + web-views/sketching/preambleHelloTarget.wvc | 9 + web-views/sketching/simpleGreeter.wvc | 1 + web-views/src/main/antlr/LexerFragments.g4 | 70 +++ .../main/antlr/WebViewComponentsLexerBase.g4 | 543 +++++++++++++++++ .../src/main/antlr/WebViewComponentsParser.g4 | 123 ++++ .../groowt/view/web/ChildRenderException.java | 9 + .../DefaultWebComponentTemplateCompiler.java | 208 +++++++ .../view/web/DefaultWebViewComponent.java | 113 ++++ .../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 | 11 + .../web/WebViewTemplateComponentSource.java | 55 ++ .../view/web/analysis/AnalysisError.java | 6 + .../groowt/view/web/analysis/Analyzer.java | 7 + .../groowt/view/web/analysis/AstAnalyzer.java | 6 + .../groowt/view/web/analysis/AstError.java | 5 + .../MismatchedComponentTypeAnalyzer.java | 16 + .../MismatchedComponentTypeError.java | 6 + .../MismatchedComponentTypeErrorAnalyzer.kt | 58 ++ .../web/analysis/ParseTreeAnalysisError.java | 5 + .../view/web/analysis/ParseTreeAnalyzer.java | 7 + .../classes/ClassLoaderClassLocator.java | 129 ++++ .../web/analysis/classes/ClassLocator.java | 5 + .../classes/PreambleAwareClassLocator.java | 90 +++ .../antlr/AbstractWebViewComponentsLexer.java | 276 +++++++++ .../java/groowt/view/web/antlr/AntlrUtil.java | 76 +++ .../web/antlr/GroovyTokenSourceIterable.kt | 35 ++ .../view/web/antlr/LexerSemanticPredicates.kt | 83 +++ .../java/groowt/view/web/antlr/LexerUtil.kt | 7 + .../view/web/antlr/MergedGroovyCodeToken.kt | 48 ++ .../groowt/view/web/antlr/PairCounter.java | 21 + .../java/groowt/view/web/antlr/ParserUtil.kt | 167 ++++++ .../view/web/antlr/SimplePairCounter.kt | 83 +++ .../java/groowt/view/web/antlr/TokenList.java | 126 ++++ .../java/groowt/view/web/antlr/TokenUtil.kt | 103 ++++ .../web/antlr/WebViewComponentsLexer.java | 17 + .../web/antlr/WebViewComponentsTokenStream.kt | 204 +++++++ .../java/groowt/view/web/ast/AstBuilder.java | 16 + .../view/web/ast/DefaultAstBuilder.java | 19 + .../web/ast/DefaultAstBuilderVisitor.java | 392 ++++++++++++ .../view/web/ast/DefaultNodeFactory.java | 218 +++++++ .../java/groowt/view/web/ast/NodeFactory.java | 68 +++ .../java/groowt/view/web/ast/NodeUtil.java | 57 ++ .../main/java/groowt/view/web/ast/NodeUtil.kt | 22 + .../view/web/ast/extension/ExtensionUtil.java | 35 ++ .../ast/extension/GStringNodeExtension.java | 33 ++ .../ast/extension/GStringPathExtension.java | 21 + .../extension/GStringScriptletExtension.java | 21 + .../extension/GroovyCodeNodeExtension.java | 48 ++ .../web/ast/extension/HasExtensionOneOf.java | 15 + .../view/web/ast/extension/HasExtensions.java | 15 + .../extension/IterableHasExtensionOneOf.java | 15 + .../ast/extension/IterableHasExtensions.java | 15 + .../view/web/ast/extension/NodeExtension.java | 7 + .../ast/extension/NodeExtensionContainer.java | 8 + .../ast/extension/NodeExtensionFactory.java | 8 + .../view/web/ast/extension/SelfNode.java | 13 + .../extension/SelfNodeRegistryExtension.java | 44 ++ .../SimpleNodeExtensionContainer.java | 25 + .../extension/SimpleNodeExtensionFactory.java | 32 + .../view/web/ast/node/AbstractLeafNode.java | 60 ++ .../view/web/ast/node/AbstractTreeNode.java | 119 ++++ .../groowt/view/web/ast/node/AttrNode.java | 26 + .../view/web/ast/node/BodyChildNode.java | 13 + .../groowt/view/web/ast/node/BodyNode.java | 28 + .../web/ast/node/BooleanValueAttrNode.java | 21 + .../web/ast/node/ClassComponentTypeNode.java | 32 + .../view/web/ast/node/ClosureValueNode.java | 52 ++ .../web/ast/node/CompilationUnitNode.java | 34 ++ .../view/web/ast/node/ComponentArgsNode.java | 55 ++ .../ast/node/ComponentConstructorNode.java | 56 ++ .../view/web/ast/node/ComponentNode.java | 30 + .../view/web/ast/node/ComponentTypeNode.java | 33 ++ .../view/web/ast/node/ComponentValueNode.java | 28 + .../web/ast/node/DollarReferenceNode.java | 34 ++ .../web/ast/node/DollarScriptletNode.java | 33 ++ .../web/ast/node/EmptyClosureValueNode.java | 39 ++ .../web/ast/node/FragmentComponentNode.java | 20 + .../web/ast/node/GStringBodyTextNode.java | 34 ++ .../view/web/ast/node/GStringValueNode.java | 46 ++ .../web/ast/node/JStringBodyTextNode.java | 26 + .../view/web/ast/node/JStringValueNode.java | 26 + .../groowt/view/web/ast/node/KeyNode.java | 28 + .../view/web/ast/node/KeyValueAttrNode.java | 27 + .../groowt/view/web/ast/node/LeafNode.java | 3 + .../java/groowt/view/web/ast/node/Node.java | 22 + .../view/web/ast/node/PlainScriptletNode.java | 52 ++ .../view/web/ast/node/PreambleNode.java | 26 + .../web/ast/node/StringComponentTypeNode.java | 21 + .../groowt/view/web/ast/node/TreeNode.java | 11 + .../view/web/ast/node/TypedComponentNode.java | 28 + .../groowt/view/web/ast/node/ValueNode.java | 13 + .../view/web/lib/FragmentComponent.java | 8 + .../runtime/DefaultSourceMapDeserializer.java | 47 ++ .../WebViewComponentChildCollector.java | 34 ++ .../web/runtime/WebViewComponentWriter.java | 82 +++ .../view/web/transpile/BodyTranspiler.java | 26 + .../web/transpile/ComponentTranspiler.java | 15 + .../web/transpile/DefaultBodyTranspiler.java | 74 +++ .../transpile/DefaultComponentTranspiler.java | 371 ++++++++++++ .../transpile/DefaultGStringTranspiler.java | 186 ++++++ .../transpile/DefaultGroovyTranspiler.java | 159 +++++ .../transpile/DefaultJStringTranspiler.java | 48 ++ .../transpile/DefaultPreambleTranspiler.java | 32 + .../transpile/DefaultSourceMapSerializer.java | 33 ++ .../DefaultTranspilerConfiguration.java | 38 ++ .../transpile/DefaultValueNodeTranspiler.java | 81 +++ .../view/web/transpile/GStringTranspiler.java | 11 + .../view/web/transpile/GroovyTranspiler.java | 16 + .../view/web/transpile/JStringTranspiler.java | 12 + .../web/transpile/OutStatementFactory.java | 8 + .../view/web/transpile/PositionSetter.java | 12 + .../web/transpile/PreambleTranspiler.java | 20 + .../transpile/SimpleOutStatementFactory.java | 27 + .../web/transpile/SimplePositionSetter.java | 47 ++ .../groowt/view/web/transpile/SourceMap.java | 58 ++ .../web/transpile/SourceMapDeserializer.java | 8 + .../web/transpile/SourceMapSerializer.java | 6 + .../transpile/TranspilerConfiguration.java | 7 + .../view/web/transpile/TranspilerUtil.java | 108 ++++ .../web/transpile/ValueNodeTranspiler.java | 14 + .../transpile/WebViewComponentModuleNode.java | 125 ++++ .../WebViewComponentReaderSource.java | 265 +++++++++ .../transpile/WebViewComponentSourceUnit.java | 57 ++ .../transpile/util/GroovyPrettyPrinter.java | 556 ++++++++++++++++++ .../view/web/transpile/util/GroovyUtil.java | 102 ++++ .../view/web/transpile/util/GroovyUtil.kt | 6 + .../view/web/util/AbstractClosedRange.java | 69 +++ .../groowt/view/web/util/ClosedRange.java | 25 + .../view/web/util/ComparableClosedRange.java | 20 + .../view/web/util/ComparatorClosedRange.java | 26 + .../java/groowt/view/web/util/EmptyRange.java | 52 ++ .../groowt/view/web/util/EmptyTokenRange.java | 30 + .../view/web/util/FilteringIterable.java | 62 ++ .../view/web/util/FilteringIterator.java | 73 +++ .../groowt/view/web/util/MappingIterable.java | 24 + .../groowt/view/web/util/MappingIterator.java | 30 + .../java/groowt/view/web/util/Monoid.java | 21 + .../java/groowt/view/web/util/OpenRange.java | 32 + .../java/groowt/view/web/util/Option.java | 158 +++++ .../main/java/groowt/view/web/util/Range.java | 7 + .../groowt/view/web/util/RangeIterator.java | 19 + .../java/groowt/view/web/util/SemiGroup.java | 13 + .../view/web/util/SimpleRangeIterator.java | 42 ++ .../view/web/util/SimpleTokenRange.java | 21 + .../groowt/view/web/util/SourcePosition.java | 49 ++ .../java/groowt/view/web/util/TokenRange.java | 38 ++ web-views/src/main/resources/log4j2.xml | 17 + web-views/src/test/ast/complicated.wvc | 20 + web-views/src/test/ast/helloTarget.wvc | 1 + .../src/test/ast/simpleComponentWithBody.wvc | 1 + .../src/test/ast/trees/complicated_ast.txt | 75 +++ .../src/test/ast/trees/helloTarget_ast.txt | 10 + .../ast/trees/simpleComponentWithBody_ast.txt | 9 + ...aultWebComponentTemplateCompilerTests.java | 71 +++ .../antlr/WebViewComponentsLexerTests.java | 51 ++ .../antlr/WebViewComponentsParserTests.java | 110 ++++ .../WebViewComponentsTokenStreamTests.groovy | 60 ++ .../view/web/ast/DefaultAstBuilderTests.java | 34 ++ .../ast/DefaultAstBuilderVisitorTests.groovy | 79 +++ .../view/web/ast/DefaultNodeFactoryTests.java | 20 + .../DefaultBodyTranspilerTests.java | 25 + .../DefaultGStringTranspilerTests.java | 16 + .../DefaultGroovyTranspilerTests.java | 24 + .../DefaultPreambleTranspilerTests.java | 15 + .../src/test/parser/blankPreambleOnly.wvc | 2 + .../parser/blankPreambleWithExtraLines.wvc | 7 + web-views/src/test/parser/complicated.wvc | 20 + web-views/src/test/parser/helloTarget.wvc | 1 + .../src/test/parser/preambleWithClass.wvc | 5 + .../trees/blankPreambleOnly_parseTree.txt | 5 + .../blankPreambleWithExtraLines_parseTree.txt | 6 + .../parser/trees/complicated_parseTree.txt | 212 +++++++ .../parser/trees/helloTarget_parseTree.txt | 13 + .../trees/preambleWithClass_parseTree.txt | 6 + .../view/web/antlr/mergesGroovyTokens.gst | 3 + .../groowt/view/web/ast/AstBuilderTests.java | 91 +++ .../groowt/view/web/ast/NodeFactoryTests.java | 223 +++++++ .../web/transpiler/BodyTranspilerTests.java | 80 +++ .../transpiler/GStringTranspilerTests.java | 42 ++ .../web/transpiler/GroovyTranspilerTests.java | 72 +++ .../transpiler/PreambleTranspilerTests.java | 17 + .../web/transpiler/TranspilerTestsUtil.java | 7 + web-views/src/tools/binTemplate.gst | 3 + .../web/tools/AbstractTreeFileMaker.groovy | 43 ++ .../groowt/view/web/tools/AstFileMaker.groovy | 135 +++++ .../view/web/tools/AstFileMakerCli.groovy | 29 + .../view/web/tools/ConvertToGroovy.groovy | 99 ++++ .../view/web/tools/ParseTreeFileMaker.groovy | 88 +++ .../web/tools/ParseTreeFileMakerCli.groovy | 30 + .../view/web/tools/SourceFileProcessor.groovy | 6 + .../web/tools/SourceFileProcessorSpec.groovy | 100 ++++ .../groowt/view/web/tools/inspectNodes.groovy | 47 ++ .../kotlin/groowt/view/web/tools/LexerTool.kt | 12 + .../groowt/view/web/tools/ParserTool.kt | 18 + .../kotlin/groowt/view/web/tools/ToolUtil.kt | 80 +++ 306 files changed, 16200 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 buildSrc/build.gradle create mode 100644 buildSrc/settings.gradle create mode 100644 buildSrc/src/main/groovy/groowt/gradle/GroowtConventions.gradle create mode 100644 buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrAllTask.java create mode 100644 buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrExtension.java create mode 100644 buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrPlugin.java create mode 100644 buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrTask.java create mode 100644 buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrUtil.java create mode 100644 buildSrc/src/main/java/groowt/gradle/antlr/NullableProviderList.java create mode 100644 buildSrc/src/main/java/groowt/gradle/antlr/ResolvedSource.java create mode 100644 buildSrc/src/main/java/groowt/gradle/antlr/SourceSpec.java create mode 100644 buildSrc/src/main/java/groowt/gradle/antlr/SourceSpecContainer.java create mode 100644 buildSrc/src/test/groovy/groowt/gradle/antlr/GroowtAntlrPluginTests.groovy create mode 100644 buildSrc/src/test/groovy/groowt/gradle/antlr/GroowtAntlrUtilTests.groovy create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 util/di/build.gradle create mode 100644 util/di/src/main/java/groowt/util/di/AbstractInjectingObjectFactory.java create mode 100644 util/di/src/main/java/groowt/util/di/AbstractRegistryObjectFactory.java create mode 100644 util/di/src/main/java/groowt/util/di/Binding.java create mode 100644 util/di/src/main/java/groowt/util/di/BindingConfigurator.java create mode 100644 util/di/src/main/java/groowt/util/di/BindingUtil.java create mode 100644 util/di/src/main/java/groowt/util/di/ClassBinding.java create mode 100644 util/di/src/main/java/groowt/util/di/DefaultNamedRegistryExtension.java create mode 100644 util/di/src/main/java/groowt/util/di/DefaultRegistry.java create mode 100644 util/di/src/main/java/groowt/util/di/DefaultRegistryObjectFactory.java create mode 100644 util/di/src/main/java/groowt/util/di/ExtensionContainer.java create mode 100644 util/di/src/main/java/groowt/util/di/KeyBinder.java create mode 100644 util/di/src/main/java/groowt/util/di/KeyHolder.java create mode 100644 util/di/src/main/java/groowt/util/di/LazySingletonBinding.java create mode 100644 util/di/src/main/java/groowt/util/di/NamedRegistryExtension.java create mode 100644 util/di/src/main/java/groowt/util/di/ObjectFactory.java create mode 100644 util/di/src/main/java/groowt/util/di/ObjectFactoryUtil.java create mode 100644 util/di/src/main/java/groowt/util/di/ProviderBinding.java create mode 100644 util/di/src/main/java/groowt/util/di/QualifierHandler.java create mode 100644 util/di/src/main/java/groowt/util/di/QualifierHandlerContainer.java create mode 100644 util/di/src/main/java/groowt/util/di/Registry.java create mode 100644 util/di/src/main/java/groowt/util/di/RegistryExtension.java create mode 100644 util/di/src/main/java/groowt/util/di/RegistryObjectFactory.java create mode 100644 util/di/src/main/java/groowt/util/di/RegistryObjectFactoryUtil.java create mode 100644 util/di/src/main/java/groowt/util/di/ScopeHandler.java create mode 100644 util/di/src/main/java/groowt/util/di/ScopeHandlerContainer.java create mode 100644 util/di/src/main/java/groowt/util/di/SimpleBindingConfigurator.java create mode 100644 util/di/src/main/java/groowt/util/di/SimpleKeyHolder.java create mode 100644 util/di/src/main/java/groowt/util/di/SingletonBinding.java create mode 100644 util/di/src/main/java/groowt/util/di/SingletonRegistryExtension.java create mode 100644 util/di/src/main/java/groowt/util/di/SingletonScopeHandler.java create mode 100644 util/di/src/main/java/groowt/util/di/annotation/Given.java create mode 100644 util/di/src/main/java/groowt/util/di/filters/Filter.java create mode 100644 util/di/src/main/java/groowt/util/di/filters/FilterHandler.java create mode 100644 util/di/src/main/java/groowt/util/di/filters/FilterHandlers.java create mode 100644 util/di/src/main/java/groowt/util/di/filters/FilterUtil.java create mode 100644 util/di/src/main/java/groowt/util/di/filters/IterableFilter.java create mode 100644 util/di/src/main/java/groowt/util/di/filters/IterableFilterHandler.java create mode 100644 util/di/src/main/java/groowt/util/di/filters/IterableFilterHandlers.java create mode 100644 util/di/src/main/java/groowt/util/di/filters/SimpleFilterHandler.java create mode 100644 util/di/src/main/java/groowt/util/di/filters/SimpleIterableFilterHandler.java create mode 100644 util/di/src/test/java/groowt/util/di/DefaultRegistryObjectFactoryTests.java create mode 100644 util/di/src/test/resources/log4j2.xml create mode 100644 util/extensible/build.gradle create mode 100644 util/extensible/src/main/java/groowt/util/extensible/AbstractExtensionContainer.java create mode 100644 util/extensible/src/main/java/groowt/util/extensible/Extensible.java create mode 100644 util/extensible/src/main/java/groowt/util/extensible/ExtensionAware.java create mode 100644 util/extensible/src/main/java/groowt/util/extensible/ExtensionContainer.java create mode 100644 view-components/build.gradle create mode 100644 view-components/spec.md create mode 100644 view-components/src/main/java/groowt/view/component/AbstractComponentFactory.java create mode 100644 view-components/src/main/java/groowt/view/component/AbstractViewComponent.java create mode 100644 view-components/src/main/java/groowt/view/component/CachingComponentTemplateCompiler.java create mode 100644 view-components/src/main/java/groowt/view/component/ComponentContext.java create mode 100644 view-components/src/main/java/groowt/view/component/ComponentCreateException.java create mode 100644 view-components/src/main/java/groowt/view/component/ComponentFactory.java create mode 100644 view-components/src/main/java/groowt/view/component/ComponentRenderException.java create mode 100644 view-components/src/main/java/groowt/view/component/ComponentScope.java create mode 100644 view-components/src/main/java/groowt/view/component/ComponentTemplate.java create mode 100644 view-components/src/main/java/groowt/view/component/ComponentTemplateCompiler.java create mode 100644 view-components/src/main/java/groowt/view/component/ComponentTemplateCreateException.java create mode 100644 view-components/src/main/java/groowt/view/component/DefaultComponentContext.java create mode 100644 view-components/src/main/java/groowt/view/component/DefaultComponentScope.java create mode 100644 view-components/src/main/java/groowt/view/component/DelegatingComponentFactory.java create mode 100644 view-components/src/main/java/groowt/view/component/MissingClassTypeException.java create mode 100644 view-components/src/main/java/groowt/view/component/MissingComponentException.java create mode 100644 view-components/src/main/java/groowt/view/component/MissingFragmentTypeException.java create mode 100644 view-components/src/main/java/groowt/view/component/MissingStringTypeException.java create mode 100644 view-components/src/main/java/groowt/view/component/NoFactoryMissingException.java create mode 100644 view-components/src/main/java/groowt/view/component/ViewComponent.java create mode 100644 views/build.gradle create mode 100644 views/src/main/groovy/groowt/view/AbstractView.java create mode 100644 views/src/main/groovy/groowt/view/GStringTemplateView.java create mode 100644 views/src/main/groovy/groowt/view/StandardGStringTemplateView.java create mode 100644 views/src/main/groovy/groowt/view/StandardGStringTemplateViewMetaClass.java create mode 100644 views/src/main/groovy/groowt/view/TemplateView.java create mode 100644 views/src/main/groovy/groowt/view/View.java create mode 100644 views/src/test/groovy/groowt/view/GStringTemplateViewTests.java create mode 100644 views/src/test/groovy/groowt/view/StandardGStringTemplateViewTests.groovy create mode 100644 views/src/test/resources/groowt/view/simplePartial.gst create mode 100644 web-views/.gitignore create mode 100644 web-views/build.gradle create mode 100644 web-views/sketching.gst create mode 100644 web-views/sketching/.gitignore create mode 100644 web-views/sketching/helloTarget.wvc create mode 100644 web-views/sketching/preambleHelloTarget.wvc create mode 100644 web-views/sketching/simpleGreeter.wvc create mode 100644 web-views/src/main/antlr/LexerFragments.g4 create mode 100644 web-views/src/main/antlr/WebViewComponentsLexerBase.g4 create mode 100644 web-views/src/main/antlr/WebViewComponentsParser.g4 create mode 100644 web-views/src/main/java/groowt/view/web/ChildRenderException.java create mode 100644 web-views/src/main/java/groowt/view/web/DefaultWebComponentTemplateCompiler.java create mode 100644 web-views/src/main/java/groowt/view/web/DefaultWebViewComponent.java create mode 100644 web-views/src/main/java/groowt/view/web/WebViewChildComponentRenderer.java create mode 100644 web-views/src/main/java/groowt/view/web/WebViewChildGStringRenderer.java create mode 100644 web-views/src/main/java/groowt/view/web/WebViewChildJStringRenderer.java create mode 100644 web-views/src/main/java/groowt/view/web/WebViewChildRenderer.java create mode 100644 web-views/src/main/java/groowt/view/web/WebViewComponent.java create mode 100644 web-views/src/main/java/groowt/view/web/WebViewTemplateComponentSource.java create mode 100644 web-views/src/main/java/groowt/view/web/analysis/AnalysisError.java create mode 100644 web-views/src/main/java/groowt/view/web/analysis/Analyzer.java create mode 100644 web-views/src/main/java/groowt/view/web/analysis/AstAnalyzer.java create mode 100644 web-views/src/main/java/groowt/view/web/analysis/AstError.java create mode 100644 web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeAnalyzer.java create mode 100644 web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeError.java create mode 100644 web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeErrorAnalyzer.kt create mode 100644 web-views/src/main/java/groowt/view/web/analysis/ParseTreeAnalysisError.java create mode 100644 web-views/src/main/java/groowt/view/web/analysis/ParseTreeAnalyzer.java create mode 100644 web-views/src/main/java/groowt/view/web/analysis/classes/ClassLoaderClassLocator.java create mode 100644 web-views/src/main/java/groowt/view/web/analysis/classes/ClassLocator.java create mode 100644 web-views/src/main/java/groowt/view/web/analysis/classes/PreambleAwareClassLocator.java create mode 100644 web-views/src/main/java/groowt/view/web/antlr/AbstractWebViewComponentsLexer.java create mode 100644 web-views/src/main/java/groowt/view/web/antlr/AntlrUtil.java create mode 100644 web-views/src/main/java/groowt/view/web/antlr/GroovyTokenSourceIterable.kt create mode 100644 web-views/src/main/java/groowt/view/web/antlr/LexerSemanticPredicates.kt create mode 100644 web-views/src/main/java/groowt/view/web/antlr/LexerUtil.kt create mode 100644 web-views/src/main/java/groowt/view/web/antlr/MergedGroovyCodeToken.kt create mode 100644 web-views/src/main/java/groowt/view/web/antlr/PairCounter.java create mode 100644 web-views/src/main/java/groowt/view/web/antlr/ParserUtil.kt create mode 100644 web-views/src/main/java/groowt/view/web/antlr/SimplePairCounter.kt create mode 100644 web-views/src/main/java/groowt/view/web/antlr/TokenList.java create mode 100644 web-views/src/main/java/groowt/view/web/antlr/TokenUtil.kt create mode 100644 web-views/src/main/java/groowt/view/web/antlr/WebViewComponentsLexer.java create mode 100644 web-views/src/main/java/groowt/view/web/antlr/WebViewComponentsTokenStream.kt create mode 100644 web-views/src/main/java/groowt/view/web/ast/AstBuilder.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/DefaultAstBuilder.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/DefaultAstBuilderVisitor.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/DefaultNodeFactory.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/NodeFactory.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/NodeUtil.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/NodeUtil.kt create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/ExtensionUtil.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/GStringNodeExtension.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/GStringPathExtension.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/GStringScriptletExtension.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/GroovyCodeNodeExtension.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/HasExtensionOneOf.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/HasExtensions.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/IterableHasExtensionOneOf.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/IterableHasExtensions.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/NodeExtension.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/NodeExtensionContainer.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/NodeExtensionFactory.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/SelfNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/SelfNodeRegistryExtension.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/SimpleNodeExtensionContainer.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/extension/SimpleNodeExtensionFactory.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/AbstractLeafNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/AbstractTreeNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/AttrNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/BodyChildNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/BodyNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/BooleanValueAttrNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/ClassComponentTypeNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/ClosureValueNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/CompilationUnitNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/ComponentArgsNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/ComponentConstructorNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/ComponentNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/ComponentTypeNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/ComponentValueNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/DollarReferenceNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/DollarScriptletNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/EmptyClosureValueNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/FragmentComponentNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/GStringBodyTextNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/GStringValueNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/JStringBodyTextNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/JStringValueNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/KeyNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/KeyValueAttrNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/LeafNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/Node.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/PlainScriptletNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/PreambleNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/StringComponentTypeNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/TreeNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/TypedComponentNode.java create mode 100644 web-views/src/main/java/groowt/view/web/ast/node/ValueNode.java create mode 100644 web-views/src/main/java/groowt/view/web/lib/FragmentComponent.java create mode 100644 web-views/src/main/java/groowt/view/web/runtime/DefaultSourceMapDeserializer.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/WebViewComponentWriter.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/BodyTranspiler.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/ComponentTranspiler.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/DefaultBodyTranspiler.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/DefaultGStringTranspiler.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/DefaultGroovyTranspiler.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/DefaultJStringTranspiler.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/DefaultPreambleTranspiler.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/DefaultSourceMapSerializer.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/DefaultTranspilerConfiguration.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/DefaultValueNodeTranspiler.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/GStringTranspiler.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/GroovyTranspiler.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/JStringTranspiler.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/OutStatementFactory.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/PositionSetter.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/PreambleTranspiler.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/SimpleOutStatementFactory.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/SimplePositionSetter.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/SourceMap.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/SourceMapDeserializer.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/SourceMapSerializer.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/TranspilerConfiguration.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/TranspilerUtil.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/ValueNodeTranspiler.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/WebViewComponentModuleNode.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/WebViewComponentReaderSource.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/WebViewComponentSourceUnit.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/util/GroovyPrettyPrinter.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/util/GroovyUtil.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/util/GroovyUtil.kt create mode 100644 web-views/src/main/java/groowt/view/web/util/AbstractClosedRange.java create mode 100644 web-views/src/main/java/groowt/view/web/util/ClosedRange.java create mode 100644 web-views/src/main/java/groowt/view/web/util/ComparableClosedRange.java create mode 100644 web-views/src/main/java/groowt/view/web/util/ComparatorClosedRange.java create mode 100644 web-views/src/main/java/groowt/view/web/util/EmptyRange.java create mode 100644 web-views/src/main/java/groowt/view/web/util/EmptyTokenRange.java create mode 100644 web-views/src/main/java/groowt/view/web/util/FilteringIterable.java create mode 100644 web-views/src/main/java/groowt/view/web/util/FilteringIterator.java create mode 100644 web-views/src/main/java/groowt/view/web/util/MappingIterable.java create mode 100644 web-views/src/main/java/groowt/view/web/util/MappingIterator.java create mode 100644 web-views/src/main/java/groowt/view/web/util/Monoid.java create mode 100644 web-views/src/main/java/groowt/view/web/util/OpenRange.java create mode 100644 web-views/src/main/java/groowt/view/web/util/Option.java create mode 100644 web-views/src/main/java/groowt/view/web/util/Range.java create mode 100644 web-views/src/main/java/groowt/view/web/util/RangeIterator.java create mode 100644 web-views/src/main/java/groowt/view/web/util/SemiGroup.java create mode 100644 web-views/src/main/java/groowt/view/web/util/SimpleRangeIterator.java create mode 100644 web-views/src/main/java/groowt/view/web/util/SimpleTokenRange.java create mode 100644 web-views/src/main/java/groowt/view/web/util/SourcePosition.java create mode 100644 web-views/src/main/java/groowt/view/web/util/TokenRange.java create mode 100644 web-views/src/main/resources/log4j2.xml create mode 100644 web-views/src/test/ast/complicated.wvc create mode 100644 web-views/src/test/ast/helloTarget.wvc create mode 100644 web-views/src/test/ast/simpleComponentWithBody.wvc create mode 100644 web-views/src/test/ast/trees/complicated_ast.txt create mode 100644 web-views/src/test/ast/trees/helloTarget_ast.txt create mode 100644 web-views/src/test/ast/trees/simpleComponentWithBody_ast.txt create mode 100644 web-views/src/test/groovy/groowt/view/web/DefaultWebComponentTemplateCompilerTests.java create mode 100644 web-views/src/test/groovy/groowt/view/web/antlr/WebViewComponentsLexerTests.java create mode 100644 web-views/src/test/groovy/groowt/view/web/antlr/WebViewComponentsParserTests.java create mode 100644 web-views/src/test/groovy/groowt/view/web/antlr/WebViewComponentsTokenStreamTests.groovy create mode 100644 web-views/src/test/groovy/groowt/view/web/ast/DefaultAstBuilderTests.java create mode 100644 web-views/src/test/groovy/groowt/view/web/ast/DefaultAstBuilderVisitorTests.groovy create mode 100644 web-views/src/test/groovy/groowt/view/web/ast/DefaultNodeFactoryTests.java create mode 100644 web-views/src/test/groovy/groowt/view/web/transpiler/DefaultBodyTranspilerTests.java create mode 100644 web-views/src/test/groovy/groowt/view/web/transpiler/DefaultGStringTranspilerTests.java create mode 100644 web-views/src/test/groovy/groowt/view/web/transpiler/DefaultGroovyTranspilerTests.java create mode 100644 web-views/src/test/groovy/groowt/view/web/transpiler/DefaultPreambleTranspilerTests.java create mode 100644 web-views/src/test/parser/blankPreambleOnly.wvc create mode 100644 web-views/src/test/parser/blankPreambleWithExtraLines.wvc create mode 100644 web-views/src/test/parser/complicated.wvc create mode 100644 web-views/src/test/parser/helloTarget.wvc create mode 100644 web-views/src/test/parser/preambleWithClass.wvc create mode 100644 web-views/src/test/parser/trees/blankPreambleOnly_parseTree.txt create mode 100644 web-views/src/test/parser/trees/blankPreambleWithExtraLines_parseTree.txt create mode 100644 web-views/src/test/parser/trees/complicated_parseTree.txt create mode 100644 web-views/src/test/parser/trees/helloTarget_parseTree.txt create mode 100644 web-views/src/test/parser/trees/preambleWithClass_parseTree.txt create mode 100644 web-views/src/test/resources/groowt/view/web/antlr/mergesGroovyTokens.gst create mode 100644 web-views/src/testFixtures/java/groowt/view/web/ast/AstBuilderTests.java create mode 100644 web-views/src/testFixtures/java/groowt/view/web/ast/NodeFactoryTests.java create mode 100644 web-views/src/testFixtures/java/groowt/view/web/transpiler/BodyTranspilerTests.java create mode 100644 web-views/src/testFixtures/java/groowt/view/web/transpiler/GStringTranspilerTests.java create mode 100644 web-views/src/testFixtures/java/groowt/view/web/transpiler/GroovyTranspilerTests.java create mode 100644 web-views/src/testFixtures/java/groowt/view/web/transpiler/PreambleTranspilerTests.java create mode 100644 web-views/src/testFixtures/java/groowt/view/web/transpiler/TranspilerTestsUtil.java create mode 100644 web-views/src/tools/binTemplate.gst create mode 100644 web-views/src/tools/groovy/groowt/view/web/tools/AbstractTreeFileMaker.groovy create mode 100644 web-views/src/tools/groovy/groowt/view/web/tools/AstFileMaker.groovy create mode 100644 web-views/src/tools/groovy/groowt/view/web/tools/AstFileMakerCli.groovy create mode 100644 web-views/src/tools/groovy/groowt/view/web/tools/ConvertToGroovy.groovy create mode 100644 web-views/src/tools/groovy/groowt/view/web/tools/ParseTreeFileMaker.groovy create mode 100644 web-views/src/tools/groovy/groowt/view/web/tools/ParseTreeFileMakerCli.groovy create mode 100644 web-views/src/tools/groovy/groowt/view/web/tools/SourceFileProcessor.groovy create mode 100644 web-views/src/tools/groovy/groowt/view/web/tools/SourceFileProcessorSpec.groovy create mode 100644 web-views/src/tools/groovy/groowt/view/web/tools/inspectNodes.groovy create mode 100644 web-views/src/tools/kotlin/groowt/view/web/tools/LexerTool.kt create mode 100644 web-views/src/tools/kotlin/groowt/view/web/tools/ParserTool.kt create mode 100644 web-views/src/tools/kotlin/groowt/view/web/tools/ToolUtil.kt diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e25fce7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build +.idea \ No newline at end of file diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000..a33be5e --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java-gradle-plugin' + id 'groovy-gradle-plugin' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation libs.junit.jupiter.api +} + +gradlePlugin { + plugins { + create('GroowtAntlrPlugin') { + id = 'GroowtAntlrPlugin' + implementationClass = 'groowt.gradle.antlr.GroowtAntlrPlugin' + } + + } +} + +testing { + suites { + test { + useJUnitJupiter() + } + } +} diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle new file mode 100644 index 0000000..8358f7b --- /dev/null +++ b/buildSrc/settings.gradle @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + versionCatalogs { + create('libs') { + from files('../gradle/libs.versions.toml') + } + } +} diff --git a/buildSrc/src/main/groovy/groowt/gradle/GroowtConventions.gradle b/buildSrc/src/main/groovy/groowt/gradle/GroowtConventions.gradle new file mode 100644 index 0000000..8df6dc1 --- /dev/null +++ b/buildSrc/src/main/groovy/groowt/gradle/GroowtConventions.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation libs.junit.jupiter.api + testRuntimeOnly libs.log4j.slf4jBinding +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +// TODO: hook into testResources and add a default log4j2.xml config for testing + +testing { + suites { + test { + useJUnitJupiter() + } + } +} diff --git a/buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrAllTask.java b/buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrAllTask.java new file mode 100644 index 0000000..7b14bcf --- /dev/null +++ b/buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrAllTask.java @@ -0,0 +1,5 @@ +package groowt.gradle.antlr; + +import org.gradle.api.plugins.antlr.AntlrTask; + +public class GroowtAntlrAllTask extends AntlrTask {} diff --git a/buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrExtension.java b/buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrExtension.java new file mode 100644 index 0000000..ae028a2 --- /dev/null +++ b/buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrExtension.java @@ -0,0 +1,34 @@ +package groowt.gradle.antlr; + +import org.gradle.api.Action; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.Property; + +import javax.inject.Inject; + +public abstract class GroowtAntlrExtension { + + private final SourceSpecContainer sourceSpecs; + + @Inject + public GroowtAntlrExtension(ObjectFactory objectFactory) { + this.sourceSpecs = objectFactory.newInstance(SourceSpecContainer.class, (Action) sourceSpec -> { + sourceSpec.getPackageName().convention(this.getPackageName()); + sourceSpec.getVisitor().convention(this.getVisitor()); + sourceSpec.getIsCompileDependency().convention(true); + sourceSpec.getDebug().convention(false); + }); + } + + public abstract Property getPackageName(); + public abstract Property getVisitor(); + + public SourceSpecContainer getSourceSpecs() { + return this.sourceSpecs; + } + + public void sourceSpecs(Action configure) { + configure.execute(this.getSourceSpecs()); + } + +} diff --git a/buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrPlugin.java b/buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrPlugin.java new file mode 100644 index 0000000..622022a --- /dev/null +++ b/buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrPlugin.java @@ -0,0 +1,216 @@ +package groowt.gradle.antlr; + +import org.gradle.api.Action; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.file.FileTree; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.plugins.antlr.AntlrPlugin; +import org.gradle.api.plugins.antlr.AntlrSourceDirectorySet; +import org.gradle.api.plugins.antlr.AntlrTask; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.compile.JavaCompile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.inject.Inject; +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; + +import static groowt.gradle.antlr.GroowtAntlrUtil.*; + +public final class GroowtAntlrPlugin implements Plugin { + + public static final String taskGroup = "groowtAntlr"; + + private static final String packageArg = "-package"; + private static final String traceArg = "-trace"; + private static final String visitorArg = "-visitor"; + + private final ObjectFactory objectFactory; + + @Inject + public GroowtAntlrPlugin(ObjectFactory objectFactory) { + this.objectFactory = objectFactory; + } + + private static NullableProviderList getArguments(SourceSpec sourceSpec) { + final NullableProviderList arguments = new NullableProviderList<>(); + arguments.addCollectionProvider(sourceSpec.getPackageName().map(packageName -> List.of(packageArg, packageName))); + arguments.addProvider(sourceSpec.getVisitor().map(isVisitor -> isVisitor ? visitorArg : null)); + arguments.addProvider(sourceSpec.getDebug().map(isDebug -> isDebug ? traceArg : null)); + return arguments; + } + + // For afterEvaluate! + private static List createAndRegister(Project project, GroowtAntlrExtension extension) { + final TaskContainer taskContainer = project.getTasks(); + return extension.getSourceSpecs().stream().map(sourceSpec -> { + return taskContainer.create( + getGenerateTaskName(sourceSpec), + GroowtAntlrTask.class, + sourceSpec, + (Action) task -> { + task.setGroup(taskGroup); + task.setArguments(getArguments(sourceSpec)); + task.setSource(sourceSpec.getResolvedSource().getSourceFile()); + task.setOutputDirectory( + getOutputDirectory( + project, + sourceSpec.getResolvedSource().getSourceSet(), + sourceSpec.getPackageName() + ) + ); + } + ); + }).toList(); + } + + // For afterEvaluate! + private static void addCompileDependencies(Project project, List tasks) { + tasks.forEach(task -> { + // if it is a compile dependency, add it as input to java source set + final var isCompileDependency = task.getSourceSpec().getIsCompileDependency().get(); + if (isCompileDependency) { + project.getTasks().withType(JavaCompile.class).configureEach(javaCompile -> { + javaCompile.dependsOn(task); + }); + } + }); + } + + private record GenerateAllSpec( + @Nullable FileTree source, + @NotNull List args, + @NotNull Provider packageName + ) {} + + private GenerateAllSpec getBlankGenerateAllSpec() { + return new GenerateAllSpec(null, List.of(), this.objectFactory.property(String.class)); + } + + private static GenerateAllSpec getGenerateAllSpecFromTask(GroowtAntlrTask task) { + return new GenerateAllSpec(task.getSource(), task.getArguments(), task.getSourceSpec().getPackageName()); + } + + private static @Nullable FileTree combineFileTrees(@Nullable FileTree f0, @Nullable FileTree f1) { + if (f0 != null && f1 != null) { + return f0.plus(f1); + } else if (f0 != null) { + return f0; + } else { + return f1; // null + } + } + + private static List combineArguments(List a0, List a1) { + final List result = new ArrayList<>(a0); + final Iterator a1Iter = a1.iterator(); + while (a1Iter.hasNext()) { + final String arg = a1Iter.next(); + if (arg.equals(packageArg) && result.contains(packageArg)) { + if (!a1Iter.hasNext()) { + throw new IllegalStateException("shouldn't get here"); + } + final String a0PackageName = result.get(result.indexOf(arg) + 1); + final String a1PackageName = a1Iter.next(); + if (!a0PackageName.equals(a1PackageName)) { + throw new IllegalArgumentException("Cannot have separate package arguments for two files from the same source set."); + } + } else if (!result.contains(arg)) { + result.add(arg); + } + } + return result; + } + + private static Provider combinePackageNames(Provider p0, Provider p1) { + return p0.zip(p1, (pn0, pn1) -> { + if (!pn0.equals(pn1)) { + throw new IllegalArgumentException("Cannot have separate package names for two files from the same source set."); + } + return pn0; + }); + } + + private static GenerateAllSpec combineGenerateAllSpecs(GenerateAllSpec s0, GenerateAllSpec s1) { + return new GenerateAllSpec( + combineFileTrees(s0.source(), s1.source()), + combineArguments(s0.args(), s1.args()), + combinePackageNames(s0.packageName(), s1.packageName()) + ); + } + + // For afterEvaluate! + private static void addGenerateAllTasks(Project project, List tasks) { + final Map> sourceSetToTasks = tasks.stream().collect(Collectors.groupingBy( + task -> task.getSourceSpec().getResolvedSource().getSourceSet() + )); + + final Map sourceSetToSpec = new HashMap<>(); + sourceSetToTasks.forEach((sourceSet, sourceSetTasks) -> { + List specs = sourceSetTasks.stream().map(task -> + new GenerateAllSpec(task.getSource(), task.getArguments(), task.getSourceSpec().getPackageName()) + ).toList(); + specs.stream().reduce(GroowtAntlrPlugin::combineGenerateAllSpecs).ifPresent(allSpec -> { + sourceSetToSpec.put(sourceSet, allSpec); + }); + }); + + sourceSetToSpec.forEach((sourceSet, spec) -> { + project.getTasks().register( + getGenerateAllTaskName(sourceSet), + GroowtAntlrAllTask.class, + task -> { + task.setGroup(taskGroup); + if (spec.source() != null) { + task.setSource(spec.source()); + } + task.setArguments(spec.args()); + task.setOutputDirectory( + getOutputDirectory(project, sourceSet, spec.packageName()).get().getAsFile() + ); + } + ); + }); + } + + @Override + public void apply(Project project) { + project.getPluginManager().apply(AntlrPlugin.class); + + // undo the antlr plugin creating its own tasks + project.getTasks().withType(AntlrTask.class, antlrTask -> { + if (!(antlrTask instanceof GroowtAntlrTask || antlrTask instanceof GroowtAntlrAllTask)) { + antlrTask.setEnabled(false); + } + }); + + // create extension + final GroowtAntlrExtension extension = project.getExtensions().create("groowtAntlr", GroowtAntlrExtension.class); + extension.getPackageName().convention(""); + extension.getVisitor().convention(false); + + // find all antlr files first and add them to extension + project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets().forEach(sourceSet -> { + final Set antlrFiles = sourceSet.getExtensions().getByType(AntlrSourceDirectorySet.class).getFiles(); + for (final File antlrFile : antlrFiles) { + if (isAntlrSourceFile(antlrFile)) { + extension.getSourceSpecs().register(sourceSet, antlrFile); + } + } + }); + + // after evaluate, generate tasks for each registered sourceSpec + project.afterEvaluate(postEvaluateProject -> { + final List tasks = createAndRegister(postEvaluateProject, extension); + addCompileDependencies(project, tasks); + addGenerateAllTasks(project, tasks); + }); + } + +} diff --git a/buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrTask.java b/buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrTask.java new file mode 100644 index 0000000..a255954 --- /dev/null +++ b/buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrTask.java @@ -0,0 +1,70 @@ +package groowt.gradle.antlr; + +import org.gradle.api.Action; +import org.gradle.api.file.Directory; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.antlr.AntlrTask; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.OutputDirectory; +import org.jetbrains.annotations.NotNull; + +import javax.inject.Inject; +import java.io.File; +import java.util.List; + +public class GroowtAntlrTask extends AntlrTask { + + private final ObjectFactory objectFactory; + private final SourceSpec sourceSpec; + + private Provider outputDirectory; + private NullableProviderList arguments; + + @Inject + public GroowtAntlrTask(ObjectFactory objectFactory, SourceSpec sourceSpec, Action configure) { + this.objectFactory = objectFactory; + this.sourceSpec = sourceSpec; + configure.execute(this); + } + + @Nested + public SourceSpec getSourceSpec() { + return this.sourceSpec; + } + + @Override + public @NotNull File getOutputDirectory() { + return this.outputDirectory.get().getAsFile(); + } + + @Override + public void setOutputDirectory(@NotNull File outputDirectory) { + final DirectoryProperty directoryProperty = this.objectFactory.directoryProperty(); + directoryProperty.set(outputDirectory); + this.outputDirectory = directoryProperty; + } + + public void setOutputDirectory(Provider outputDirectoryProvider) { + this.outputDirectory = outputDirectoryProvider; + } + + @Override + @Internal + public @NotNull List getArguments() { + return this.arguments.getElements(); + } + + @Override + public void setArguments(@NotNull List arguments) { + this.arguments = new NullableProviderList<>(); + this.arguments.addAllElements(arguments); + } + + public void setArguments(@NotNull NullableProviderList arguments) { + this.arguments = arguments; + } + +} diff --git a/buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrUtil.java b/buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrUtil.java new file mode 100644 index 0000000..370c351 --- /dev/null +++ b/buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrUtil.java @@ -0,0 +1,147 @@ +package groowt.gradle.antlr; + +import org.gradle.api.Project; +import org.gradle.api.file.Directory; +import org.gradle.api.file.SourceDirectorySet; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public final class GroowtAntlrUtil { + + public static final List antlrFileExtensions = List.of("g4", "g"); + + private static final Pattern extensionPattern = Pattern.compile("(?.*)\\.(?.*)$"); + + public static File resolve(File from, File to) { + return from.toPath().resolve(to.toPath()).toFile(); + } + + public static File relativize(File from, File to) { + return from.toPath().relativize(to.toPath()).toFile(); + } + + public static boolean isAntlrSourceFile(File file) { + final var m = extensionPattern.matcher(file.getName()); + if (m.matches()) { + return antlrFileExtensions.contains(m.group("ext")); + } else { + throw new IllegalArgumentException("Cannot determine extension of file: " + file); + } + } + + public static List sourceFileToIdentifierParts(File sourceDir, File sourceFile) { + final var relative = getRelativePathToSourceFile(sourceDir, sourceFile); + final List result = new ArrayList<>(); + for (int i = 0; i < relative.getNameCount(); i++) { + final var name = relative.getName(i); + final var m = extensionPattern.matcher(name.toString()); + if (m.matches()) { + result.add(m.group("name")); + } else { + result.add(name.toString()); + } + } + return result; + } + + private static Path getRelativePathToSourceFile(File sourceDir, File sourceFile) { + if (!sourceDir.isAbsolute()) { + throw new IllegalArgumentException("sourceDir must be absolute, given: " + sourceDir); + } + if (sourceFile.isAbsolute()) { + final var sourceDirPath = sourceDir.toPath(); + final var sourceFilePath = sourceFile.toPath(); + return sourceDirPath.relativize(sourceFilePath); + } else { + return sourceFile.toPath(); + } + } + + public static String getSourceIdentifier(ResolvedSource resolvedSource) { + final List parts = new ArrayList<>(); + if (!resolvedSource.getSourceSet().getName().equals("main")) { + parts.add(resolvedSource.getSourceSet().getName()); + } + parts.addAll(sourceFileToIdentifierParts(resolvedSource.getSourceDir(), resolvedSource.getSourceFile())); + final List capitalizedParts = parts.stream() + .map(part -> { + final var first = part.substring(0, 1); + final var rest = part.substring(1); + return first.toUpperCase() + rest; + }) + .toList(); + return String.join("", capitalizedParts); + } + + public static Provider getOutputDirectory( + Project project, + SourceSet sourceSet, + Provider packageNameProvider + ) { + return project.getLayout().getBuildDirectory().flatMap(buildDir -> { + return buildDir.dir(packageNameProvider.map(packageName -> { + return String.join(File.separator, List.of( + "generated-src", + "antlr", + sourceSet.getName(), + packageName.replace(".", File.separator) + )); + })); + }); + } + + public static String getGenerateTaskName(SourceSpec sourceSpec) { + return sourceSpec.getResolvedSource().getSourceSet().getTaskName("generate", sourceSpec.getIdentifier()); + } + + public static String getGenerateAllTaskName(SourceSet sourceSet) { + return sourceSet.getTaskName("generate", "AllAntlr"); + } + + public static ResolvedSource resolveSource( + Project project, + SourceSet sourceSet, + SourceDirectorySet sourceDirectorySet, + File sourceFile + ) { + if (!isAntlrSourceFile(sourceFile)) { + throw new IllegalArgumentException( + "The given source file " + sourceFile + " is not a recognized antlr file (bad extension)." + ); + } + + final List potentialSrcDirs = new ArrayList<>(); + + if (sourceFile.isAbsolute()) { + for (final File srcDir : sourceDirectorySet.getSrcDirs()) { + if (sourceFile.getPath().startsWith(srcDir.getPath())) { + potentialSrcDirs.add(srcDir); + } + } + } else { + for (final File srcDir : sourceDirectorySet.getSrcDirs()) { + if (resolve(srcDir, sourceFile).exists()) { + potentialSrcDirs.add(srcDir); + } + } + } + + if (potentialSrcDirs.size() > 1) { + throw new IllegalArgumentException("Multiple source directories in " + sourceDirectorySet.getName() + " contain a source file " + sourceFile); + } else if (potentialSrcDirs.isEmpty()) { + throw new IllegalArgumentException("No directories in " + sourceDirectorySet.getName() + " contain a source file " + sourceFile); + } + + final File srcDir = potentialSrcDirs.getFirst(); + return new ResolvedSource(project, sourceSet, srcDir, resolve(srcDir, sourceFile)); + } + + private GroowtAntlrUtil() {} + +} diff --git a/buildSrc/src/main/java/groowt/gradle/antlr/NullableProviderList.java b/buildSrc/src/main/java/groowt/gradle/antlr/NullableProviderList.java new file mode 100644 index 0000000..8caf2ec --- /dev/null +++ b/buildSrc/src/main/java/groowt/gradle/antlr/NullableProviderList.java @@ -0,0 +1,160 @@ +package groowt.gradle.antlr; + +import org.gradle.api.provider.Provider; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.function.Supplier; + +public final class NullableProviderList { + + private sealed interface Element extends Iterable permits BareElement, ElementProvider, CollectionProvider { + boolean isPresent(); + } + + private static final class BareElement implements Element { + + private final T element; + + public BareElement(T element) { + this.element = element; + } + + @Override + public boolean isPresent() { + return this.element != null; + } + + @NotNull + @Override + public Iterator iterator() { + return new Iterator<>() { + private boolean hasNext = true; + + @Override + public boolean hasNext() { + return this.hasNext; + } + + @Override + public T next() { + if (!this.hasNext) throw new IllegalStateException(); + this.hasNext = false; + return BareElement.this.element; + } + }; + } + + } + + private static final class ElementProvider implements Element { + + private final Provider provider; + + public ElementProvider(Provider provider) { + this.provider = provider; + } + + @Override + public boolean isPresent() { + return this.provider.isPresent(); + } + + @NotNull + @Override + public Iterator iterator() { + return new Iterator() { + private boolean hasNext = true; + + @Override + public boolean hasNext() { + return this.hasNext; + } + + @Override + public T next() { + if (!this.hasNext) throw new IllegalStateException(); + this.hasNext = false; + return ElementProvider.this.provider.get(); + } + }; + } + + } + + private static final class CollectionProvider implements Element { + + private final Provider> collectionProvider; + + public CollectionProvider(Provider> collectionProvider) { + this.collectionProvider = collectionProvider; + } + + @Override + public boolean isPresent() { + return this.collectionProvider.isPresent(); + } + + @NotNull + @Override + public Iterator iterator() { + return this.collectionProvider.get().iterator(); + } + + } + + private final List> elements = new ArrayList<>(); + + public void addElement(@Nullable T element) { + this.elements.add(new BareElement<>(element)); + } + + public void addProvider(Provider elementProvider) { + this.elements.add(new ElementProvider<>(elementProvider)); + } + + public void addCollectionProvider(Provider> collectionProvider) { + this.elements.add(new CollectionProvider<>(collectionProvider)); + } + + public void addAllElements(Collection elements) { + for (final T element : elements) { + this.elements.add(new BareElement<>(element)); + } + } + + public void addAllProviders(Collection> providers) { + for (final Provider provider : providers) { + this.elements.add(new ElementProvider<>(provider)); + } + } + + public void addAllCollectionProviders(Collection>> collectionProviders) { + for (final Provider> collectionProvider : collectionProviders) { + this.elements.add(new CollectionProvider<>(collectionProvider)); + } + } + + public List getElements() { + return this.getElements(null); + } + + public List getElements(@Nullable Supplier onNullElement) { + final List result = new ArrayList<>(); + for (final Element element : this.elements) { + if (element.isPresent()) { + for (final T t : element) { + result.add(t); + } + } else if (onNullElement != null) { + result.add(onNullElement.get()); + } + } + return result; + } + +} diff --git a/buildSrc/src/main/java/groowt/gradle/antlr/ResolvedSource.java b/buildSrc/src/main/java/groowt/gradle/antlr/ResolvedSource.java new file mode 100644 index 0000000..a178c90 --- /dev/null +++ b/buildSrc/src/main/java/groowt/gradle/antlr/ResolvedSource.java @@ -0,0 +1,90 @@ +package groowt.gradle.antlr; + +import org.gradle.api.Project; +import org.gradle.api.tasks.InputDirectory; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.SourceSet; + +import java.io.File; +import java.util.List; + +import static groowt.gradle.antlr.GroowtAntlrUtil.relativize; + +public final class ResolvedSource { + + private final Project project; + private final SourceSet sourceSet; + private final File sourceDir; + private final File sourceFile; + + public ResolvedSource(Project project, SourceSet sourceSet, File sourceDir, File sourceFile) { + this.project = project; + this.sourceSet = sourceSet; + this.sourceDir = sourceDir; + this.sourceFile = sourceFile; + } + + @Internal + public Project getProject() { + return this.project; + } + + @Internal + public SourceSet getSourceSet() { + return this.sourceSet; + } + + @InputDirectory + public File getSourceDir() { + return this.sourceDir; + } + + @InputFile + public File getSourceFile() { + return this.sourceFile; + } + + private String relativizeSourceDir() { + return relativize(this.project.getProjectDir(), this.sourceDir).toString(); + } + + private String relativeSourceFile() { + return relativize(this.sourceDir, this.sourceFile).toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (obj instanceof ResolvedSource other) { + return this.project.equals(other.project) + && this.sourceSet.equals(other.sourceSet) + && this.sourceDir.equals(other.sourceDir) + && this.sourceFile.equals(other.sourceFile); + } else { + return false; + } + } + + @Override + public int hashCode() { + int result = this.project.hashCode(); + result = 31 * result + this.sourceSet.hashCode(); + result = 31 * result + this.sourceDir.hashCode(); + result = 31 * result + this.sourceFile.hashCode(); + return result; + } + + @Override + public String toString() { + return "ResolvedSource(" + + String.join(", ", List.of( + "sourceSet: " + this.sourceSet.getName(), + "sourceDir: " + this.relativizeSourceDir(), + "sourceFile: " + this.relativeSourceFile() + )) + + ")"; + } + +} diff --git a/buildSrc/src/main/java/groowt/gradle/antlr/SourceSpec.java b/buildSrc/src/main/java/groowt/gradle/antlr/SourceSpec.java new file mode 100644 index 0000000..b8075a0 --- /dev/null +++ b/buildSrc/src/main/java/groowt/gradle/antlr/SourceSpec.java @@ -0,0 +1,43 @@ +package groowt.gradle.antlr; + +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Nested; + +import javax.inject.Inject; + +public abstract class SourceSpec { + + private final String identifier; + private final ResolvedSource resolvedSource; + + @Inject + public SourceSpec(String identifier, ResolvedSource resolvedSource) { + this.identifier = identifier; + this.resolvedSource = resolvedSource; + } + + @Internal + public String getIdentifier() { + return this.identifier; + } + + @Nested + public ResolvedSource getResolvedSource() { + return this.resolvedSource; + } + + @Input + public abstract Property getIsCompileDependency(); + + @Input + public abstract Property getDebug(); + + @Input + public abstract Property getPackageName(); + + @Input + public abstract Property getVisitor(); + +} diff --git a/buildSrc/src/main/java/groowt/gradle/antlr/SourceSpecContainer.java b/buildSrc/src/main/java/groowt/gradle/antlr/SourceSpecContainer.java new file mode 100644 index 0000000..ceca4ff --- /dev/null +++ b/buildSrc/src/main/java/groowt/gradle/antlr/SourceSpecContainer.java @@ -0,0 +1,89 @@ +package groowt.gradle.antlr; + +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.internal.CollectionCallbackActionDecorator; +import org.gradle.api.internal.DefaultDomainObjectSet; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.antlr.AntlrSourceDirectorySet; +import org.gradle.api.tasks.SourceSet; + +import javax.inject.Inject; +import java.io.File; + +import static groowt.gradle.antlr.GroowtAntlrUtil.getSourceIdentifier; +import static groowt.gradle.antlr.GroowtAntlrUtil.resolveSource; + +public class SourceSpecContainer extends DefaultDomainObjectSet { + + private final Project project; + private final ObjectFactory objectFactory; + private final Action applyConventions; + + @Inject + public SourceSpecContainer(Project project, ObjectFactory objectFactory, Action applyConventions) { + super(SourceSpec.class, CollectionCallbackActionDecorator.NOOP); + this.project = project; + this.objectFactory = objectFactory; + this.applyConventions = applyConventions; + } + + /** + * @param sourceFilePaths instances of File or String + */ + public void ignore(SourceSet sourceSet, Object... sourceFilePaths) { + for (final Object sourceFilePath : sourceFilePaths) { + switch (sourceFilePath) { + case File f -> this.ignoreFile(sourceSet, f); + case String s -> this.ignoreFile(sourceSet, new File(s)); + default -> throw new IllegalArgumentException("Can only ignore Files or Strings, given: " + sourceFilePath); + } + } + } + + private void ignoreFile(SourceSet sourceSet, File target) { + this.removeIf(potentialSourceSpec -> { + final SourceSet potentialSourceSet = potentialSourceSpec.getResolvedSource().getSourceSet(); + if (!sourceSet.equals(potentialSourceSet)) { + return false; + } + final var antlrSourceDirectorySet = sourceSet.getExtensions().getByType(AntlrSourceDirectorySet.class); + final ResolvedSource toIgnore = resolveSource(this.project, sourceSet, antlrSourceDirectorySet, target); + final File potentialSourceFile = potentialSourceSpec.getResolvedSource().getSourceFile(); + return toIgnore.getSourceFile().equals(potentialSourceFile); + }); + } + + public void register(SourceSet sourceSet, String sourceFilePath) { + this.register(sourceSet, new File(sourceFilePath), sourceSpec -> {}); // no-op action + } + + public void register(SourceSet sourceSet, File sourceFile) { + this.register(sourceSet, sourceFile, sourceSpec -> {}); + } + + public void register(SourceSet sourceSet, String sourceFilePath, Action action) { + this.register(sourceSet, new File(sourceFilePath), action); + } + + public void register(SourceSet sourceSet, File sourceFile, Action action) { + final var antlrSourceDirectorySet = sourceSet.getExtensions().getByType(AntlrSourceDirectorySet.class); + final var resolvedSource = resolveSource(this.project, sourceSet, antlrSourceDirectorySet, sourceFile); + + final String identifier = getSourceIdentifier(resolvedSource); + final var specOptional = this.stream().filter(sourceSpec -> sourceSpec.getIdentifier().equals(identifier)).findFirst(); + + if (specOptional.isPresent()) { + // we already have one, so find and run the action against it + final var spec = specOptional.get(); + action.execute(spec); + } else { + // create a new one + final var spec = this.objectFactory.newInstance(SourceSpec.class, identifier, resolvedSource); + this.applyConventions.execute(spec); + action.execute(spec); + this.add(spec); + } + } + +} diff --git a/buildSrc/src/test/groovy/groowt/gradle/antlr/GroowtAntlrPluginTests.groovy b/buildSrc/src/test/groovy/groowt/gradle/antlr/GroowtAntlrPluginTests.groovy new file mode 100644 index 0000000..3636493 --- /dev/null +++ b/buildSrc/src/test/groovy/groowt/gradle/antlr/GroowtAntlrPluginTests.groovy @@ -0,0 +1,53 @@ +//file:noinspection ConfigurationAvoidance +package groowt.gradle.antlr + +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.plugins.antlr.AntlrPlugin +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.function.Executable + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow + +class GroowtAntlrPluginTests { + + @Disabled('TODO: figure out why register(SourceSet, String) is not working.') + @Test + void smokeScreen() { + def projectDir = File.createTempDir() + new FileTreeBuilder(projectDir).tap { + dir(['src', 'main', 'antlr'].join(File.separator)) { + file('MyGrammar.g4') { + write("parser grammar MyGrammar;") + } + } + } + def project = ProjectBuilder.builder().with { + withProjectDir(projectDir) + build() + } + + project.pluginManager.with { + apply(JavaPlugin) + apply(AntlrPlugin) + apply(GroowtAntlrPlugin) + } + + def mainSourceSet = project.extensions.getByType(JavaPluginExtension).sourceSets.findByName('main') + + project.extensions.getByType(GroowtAntlrExtension).sourceSpecs.with { + register(mainSourceSet, 'MyGrammar.g4') + } + + def findTask = { + project.tasks.named('generateMyGrammar', GroowtAntlrTask) + } as Executable + + assertDoesNotThrow(findTask) { + "Could not find task 'generateMyGrammar' (all tasks: ${project.tasks*.name.join(', ')})" + } + } + +} diff --git a/buildSrc/src/test/groovy/groowt/gradle/antlr/GroowtAntlrUtilTests.groovy b/buildSrc/src/test/groovy/groowt/gradle/antlr/GroowtAntlrUtilTests.groovy new file mode 100644 index 0000000..46fd2ba --- /dev/null +++ b/buildSrc/src/test/groovy/groowt/gradle/antlr/GroowtAntlrUtilTests.groovy @@ -0,0 +1,132 @@ +//file:noinspection ConfigurationAvoidance +package groowt.gradle.antlr + +import groovy.transform.TupleConstructor +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.plugins.antlr.AntlrPlugin +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.SourceSet +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory + +import java.nio.file.Path + +import static groowt.gradle.antlr.GroowtAntlrUtil.* +import static org.junit.jupiter.api.Assertions.assertEquals +import static org.junit.jupiter.api.Assertions.assertIterableEquals +import static org.junit.jupiter.api.DynamicTest.dynamicTest + +class GroowtAntlrUtilTests { + + @TupleConstructor + private static class SourceFileToIdentifierTestSpec { + File srcDir + File srcFile + List expected + } + + @TestFactory + Collection sourceFileToIdentifierPartsTests() { + def srcDir = File.createTempDir() + def getSpec = { String path, List expected -> + new SourceFileToIdentifierTestSpec(srcDir, new File(path), expected) + } + + def specs = [ + getSpec('MyGrammar.g4', ['MyGrammar']), + getSpec('subDir/MyGrammar.g4', ['subDir', 'MyGrammar']), + getSpec('subDir/subSubDir/MyGrammar.g4', ['subDir', 'subSubDir', 'MyGrammar']), + getSpec('My.grammar.g4', ['My.grammar']) + ] + + return specs.collect { spec -> + dynamicTest(spec.srcFile.toString()) { + def actual = sourceFileToIdentifierParts(spec.srcDir, spec.srcFile) + assertIterableEquals(spec.expected, actual) { + "Unexpected result: ${actual}" + } + } + } + } + + @TestFactory + Collection getSourceIdentifierTests() { + def projectDir = File.createTempDir() + File srcDir + new FileTreeBuilder(projectDir).with { + srcDir = dir(['src', 'main', 'antlr'].join(File.separator)) + } + + def project = ProjectBuilder.builder() + .withProjectDir(projectDir) + .build() + project.pluginManager.with { + apply(JavaPlugin) + apply(AntlrPlugin) + } + def mainSourceSet = project.extensions.getByType(JavaPluginExtension) + .sourceSets.getByName('main') + + def getResolvedSource = { String path -> + new ResolvedSource(project, mainSourceSet, srcDir, new File(path)) + } + + Closure> getSpec = { String expected, String path -> + new Tuple2(expected, getResolvedSource(path)) + } + + def specs = [ + getSpec('MyGrammar', 'MyGrammar.g4'), + getSpec('SubDirMyGrammar', 'subDir/MyGrammar.g4') + ] + + return specs.collect { spec -> + dynamicTest(spec.v2.sourceFile.toString()) { + def actual = getSourceIdentifier(spec.v2) + assertEquals(spec.v1, actual) + } + } + } + + @TestFactory + Collection getOutputDirTests() { + def project = ProjectBuilder.builder().build() + project.pluginManager.tap { + apply(JavaPlugin) + apply(AntlrPlugin) + } + project.layout.buildDirectory.set(new File('build')) + + def mainSourceSet = project.extensions.getByType(JavaPluginExtension) + .sourceSets.getByName('main') + + Closure>> getSpec = { String givenPackageName -> + def expectedPackagePath = givenPackageName.replace('.', File.separator) + def expected = Path.of('build', ['generated-src', 'antlr', mainSourceSet.name, expectedPackagePath] as String[]) + def packageProperty = project.objects.property(String).tap { set(givenPackageName) } + new Tuple3(expected, mainSourceSet, packageProperty) + } + + List>> specs = [ + getSpec('antlr.one.two.three'), // build/generated-src/antlr/main/antlr/one/two/three + getSpec('test.antlr'), // build/generated-src/antlr/main/test/antlr + getSpec('antlr'), // build/generated-src/antlr/main/antlr + getSpec('') // build/generated-src/antlr/main + ] + + def projectPath = project.layout.projectDirectory.asFile.toPath() + + return specs.collect { spec -> + def givenPackageName = spec.v3.get() + dynamicTest(givenPackageName.empty ? '' : givenPackageName) { + def actualPath = getOutputDirectory(project, spec.v2, spec.v3) + .get().asFile.toPath() + def result = projectPath.relativize(actualPath) + assertEquals(spec.v1, result) + } + } + } + +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..76dc27b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.parallel=true +org.gradle.caching=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..e50a2e0 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,32 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format + +[versions] +antlr = '4.13.1' +groovy = '4.0.21' +junit = '5.10.2' +kotlin = '1.9.23' +log4j = '2.23.1' +mockito = '5.11.0' +slf4j = '2.0.12' + +[libraries] +antlr = { module = 'org.antlr:antlr4', version.ref = 'antlr' } +antlr-runtime = { module = 'org.antlr:antlr4-runtime', version.ref = 'antlr' } +asm = 'org.ow2.asm:asm:9.7' +groovy = { module = 'org.apache.groovy:groovy', version.ref = 'groovy' } +groovy-all = { module = 'org.apache.groovy:groovy-all', version.ref = 'groovy' } +groovy-console = { module = 'org.apache.groovy:groovy-console', version.ref = 'groovy' } +groovy-templates = { module = 'org.apache.groovy:groovy-templates', version.ref = 'groovy' } +jakarta-inject = 'jakarta.inject:jakarta.inject-api:2.0.1' +jansi = 'org.fusesource.jansi:jansi:2.4.1' +jetbrains-anotations = 'org.jetbrains:annotations:24.1.0' +junit-jupiter-api = { module = 'org.junit.jupiter:junit-jupiter-api', version.ref = 'junit' } +kotlin-stdlib = { module = 'org.jetbrains.kotlin:kotlin-stdlib', version.ref = 'kotlin' } +kotlin-test = { module = 'org.jetbrains.kotlin:kotlin-test', version.ref = 'kotlin' } +log4j-core = { module = 'org.apache.logging.log4j:log4j-core', version.ref = 'log4j' } +log4j-slf4jBinding = { module = 'org.apache.logging.log4j:log4j-slf4j2-impl', version.ref = 'log4j' } +mockito-core = { module = 'org.mockito:mockito-core', version.ref = 'mockito' } +mockito-junit = { module = 'org.mockito:mockito-junit-jupiter', version.ref = 'mockito' } +picocli = 'info.picocli:picocli:4.7.5' +slf4j-api = { module = 'org.slf4j:slf4j-api', version.ref = 'slf4j' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d64cd4917707c1f8861d8cb53dd15194d4248596 GIT binary patch literal 43462 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a80b22c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..63c3821 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + plugins { + id 'org.jetbrains.kotlin.jvm' version '1.9.23' + } +} + +rootProject.name = 'groowt' +include 'views', 'view-components', 'web-views' +file('util').eachDir { + include it.name + project(":$it.name").projectDir = it +} diff --git a/util/di/build.gradle b/util/di/build.gradle new file mode 100644 index 0000000..5087da5 --- /dev/null +++ b/util/di/build.gradle @@ -0,0 +1,10 @@ +plugins { + id 'GroowtConventions' + id 'java-library' +} + +dependencies { + api libs.jakarta.inject + compileOnlyApi libs.jetbrains.anotations + implementation libs.slf4j.api, libs.groovy +} diff --git a/util/di/src/main/java/groowt/util/di/AbstractInjectingObjectFactory.java b/util/di/src/main/java/groowt/util/di/AbstractInjectingObjectFactory.java new file mode 100644 index 0000000..679c53f --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/AbstractInjectingObjectFactory.java @@ -0,0 +1,197 @@ +package groowt.util.di; + +import jakarta.inject.Inject; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.*; +import java.util.*; + +import static groowt.util.di.ObjectFactoryUtil.toTypes; + +// TODO: maybe inject fields +public abstract class AbstractInjectingObjectFactory implements ObjectFactory { + + protected record CachedInjectConstructor(Class clazz, Constructor constructor) {} + + protected record CachedNonInjectConstructor( + Class clazz, + Constructor constructor, + Class[] paramTypes + ) {} + + private final Map, Constructor[]> cachedAllConstructors = new HashMap<>(); + private final Collection> cachedInjectConstructors = new ArrayList<>(); + private final Collection> cachedNonInjectConstructors = new ArrayList<>(); + private final Map, Collection> cachedSetters = new HashMap<>(); + private final Map cachedSetterParameters = new HashMap<>(); + + @SuppressWarnings("unchecked") + private @Nullable Constructor findCachedInjectConstructor(Class clazz) { + for (final CachedInjectConstructor cachedConstructor : this.cachedInjectConstructors) { + if (clazz.equals(cachedConstructor.clazz())) { + return (Constructor) cachedConstructor.constructor(); + } + } + return null; + } + + /** + * @implNote If overridden, please cache any found inject constructors using {@link #putCachedInjectConstructor}. + * + * @param clazz the {@link Class} in which to search for an {@literal @}Inject annotated constructor. + * @return the inject constructor, or {@code null} if none found. + * @param the type of the class + */ + @SuppressWarnings("unchecked") + protected @Nullable Constructor findInjectConstructor(Class clazz) { + final Constructor cachedInjectConstructor = this.findCachedInjectConstructor(clazz); + if (cachedInjectConstructor != null) { + return cachedInjectConstructor; + } + + final Constructor[] constructors = this.cachedAllConstructors.computeIfAbsent(clazz, Class::getConstructors); + + final List> injectConstructors = Arrays.stream(constructors) + .filter(constructor -> constructor.isAnnotationPresent(Inject.class)) + .toList(); + + if (injectConstructors.size() > 1) { + // one day maybe support multiple inject constructors + throw new UnsupportedOperationException("Cannot have more than one @Inject constructor in class: " + clazz); + } else if (injectConstructors.size() == 1) { + final Constructor injectConstructor = (Constructor) injectConstructors.getFirst(); + this.putCachedInjectConstructor(new CachedInjectConstructor<>(clazz, injectConstructor)); + return injectConstructor; + } else { + return null; + } + } + + protected final void putCachedInjectConstructor(CachedInjectConstructor cached) { + this.cachedInjectConstructors.add(cached); + } + + @SuppressWarnings("unchecked") + private @Nullable Constructor findCachedNonInjectConstructor(Class clazz, Class[] paramTypes) { + for (final CachedNonInjectConstructor cachedConstructor : this.cachedNonInjectConstructors) { + if (clazz.equals(cachedConstructor.clazz()) && Arrays.equals(cachedConstructor.paramTypes(), paramTypes)) { + return (Constructor) cachedConstructor.constructor(); + } + } + return null; + } + + /** + * @implNote If overridden, please cache any found non-inject constructors using {@link #putCachedNonInjectConstructor}. + * + * @param clazz the {@link Class} in which to search for a constructor which does not have an {@literal @}Inject + * annotation + * @param constructorArgs the given constructor args + * @return the found non-inject constructor appropriate for the given constructor args, or {@code null} if no + * such constructor exists + * @param the type + */ + @SuppressWarnings("unchecked") + protected @Nullable Constructor findNonInjectConstructor(Class clazz, Object[] constructorArgs) { + final Class[] types = toTypes(constructorArgs); + final Constructor cachedConstructor = this.findCachedNonInjectConstructor(clazz, types); + if (cachedConstructor != null) { + return cachedConstructor; + } + + final Constructor[] constructors = this.cachedAllConstructors.computeIfAbsent(clazz, Class::getConstructors); + for (Constructor constructor : constructors) { + if (Arrays.equals(constructor.getParameterTypes(), types)) { + final Constructor found = (Constructor) constructor; + this.putCachedNonInjectConstructor(new CachedNonInjectConstructor<>(clazz, found, types)); + return found; + } + } + return null; + } + + protected final void putCachedNonInjectConstructor(CachedNonInjectConstructor cached) { + this.cachedNonInjectConstructors.add(cached); + } + + /** + * @implNote Please call {@code super.findConstructor()} first, and then implement custom + * constructor finding logic. If the custom logic finds a constructor, please cache it + * using either {@link #putCachedNonInjectConstructor} or {@link #putCachedInjectConstructor}. + */ + protected Constructor findConstructor(Class clazz, Object[] args) { + final Constructor injectConstructor = this.findInjectConstructor(clazz); + if (injectConstructor != null) { + return injectConstructor; + } + final Constructor nonInjectConstructor = this.findNonInjectConstructor(clazz, args); + if (nonInjectConstructor != null) { + return nonInjectConstructor; + } + throw new RuntimeException("Could not find an appropriate constructor for " + clazz.getName() + + " with args " + Arrays.toString(toTypes(args)) + ); + } + + protected Collection getAllInjectSetters(Class clazz) { + final Method[] allMethods = clazz.getMethods(); + final Collection injectSetters = new ArrayList<>(); + for (final var method : allMethods) { + if ( + method.isAnnotationPresent(Inject.class) + && method.getName().startsWith("set") + && !Modifier.isStatic(method.getModifiers()) + && method.getParameterCount() == 1 + ) { + injectSetters.add(method); + } + } + return injectSetters; + } + + protected Collection getCachedSettersFor(Object target) { + return this.cachedSetters.computeIfAbsent(target.getClass(), this::getAllInjectSetters); + } + + protected Parameter getCachedInjectParameter(Method setter) { + return this.cachedSetterParameters.computeIfAbsent(setter, s -> { + if (s.getParameterCount() != 1) { + throw new IllegalArgumentException("Setter " + s.getName() + " has a parameter count other than one (1)!"); + } + return s.getParameters()[0]; + }); + } + + protected void injectSetter(Object target, Method setter) { + try { + setter.invoke(target, this.getSetterInjectArg(target.getClass(), setter, this.getCachedInjectParameter(setter))); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + protected void injectSetters(Object target) { + this.getCachedSettersFor(target).forEach(setter -> this.injectSetter(target, setter)); + } + + /** + * {@inheritDoc} + */ + @Override + public T createInstance(Class clazz, Object... constructorArgs) { + final Constructor constructor = this.findConstructor(clazz, constructorArgs); + final Object[] allArgs = this.createArgs(constructor, constructorArgs); + try { + final T instance = constructor.newInstance(allArgs); + this.injectSetters(instance); + return instance; + } catch (InvocationTargetException | IllegalAccessException | InstantiationException e) { + throw new RuntimeException(e); // In the future, we might have an option to ignore exceptions + } + } + + protected abstract Object[] createArgs(Constructor constructor, Object[] constructorArgs); + + protected abstract Object getSetterInjectArg(Class targetType, Method setter, Parameter toInject); + +} diff --git a/util/di/src/main/java/groowt/util/di/AbstractRegistryObjectFactory.java b/util/di/src/main/java/groowt/util/di/AbstractRegistryObjectFactory.java new file mode 100644 index 0000000..70fa706 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/AbstractRegistryObjectFactory.java @@ -0,0 +1,113 @@ +package groowt.util.di; + +import groowt.util.di.filters.FilterHandler; +import groowt.util.di.filters.IterableFilterHandler; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import static groowt.util.di.RegistryObjectFactoryUtil.orElseSupply; + +public abstract class AbstractRegistryObjectFactory extends AbstractInjectingObjectFactory implements RegistryObjectFactory { + + public static abstract class AbstractBuilder implements Builder { + + private final Collection> filterHandlers = new ArrayList<>(); + private final Collection> iterableFilterHandlers = new ArrayList<>(); + private final Registry registry; + private @Nullable RegistryObjectFactory parent; + + public AbstractBuilder(Registry registry) { + this.registry = registry; + } + + public AbstractBuilder() { + this.registry = new DefaultRegistry(); + } + + protected Registry getRegistry() { + return this.registry; + } + + protected Collection> getFilterHandlers() { + return this.filterHandlers; + } + + protected Collection> getIterableFilterHandlers() { + return this.iterableFilterHandlers; + } + + protected @Nullable RegistryObjectFactory getParent() { + return this.parent; + } + + @Override + public void configureRegistry(Consumer configure) { + configure.accept(this.registry); + } + + public void addFilterHandler(FilterHandler handler) { + this.filterHandlers.add(handler); + } + + public void addIterableFilterHandler(IterableFilterHandler handler) { + this.iterableFilterHandlers.add(handler); + } + + public void setParent(@Nullable RegistryObjectFactory parent) { + this.parent = parent; + } + + } + + protected final Registry registry; + @Nullable private final RegistryObjectFactory parent; + + public AbstractRegistryObjectFactory(Registry registry, @Nullable RegistryObjectFactory parent) { + this.registry = registry; + this.parent = parent; + } + + @Override + public void configureRegistry(Consumer use) { + use.accept(this.registry); + } + + @Override + public @Nullable ScopeHandler findScopeHandler(Class scopeType) { + return this.registry.getScopeHandler(scopeType); + } + + @Override + public @Nullable QualifierHandler findQualifierHandler(Class qualifierType) { + return this.registry.getQualifierHandler(qualifierType); + } + + protected final Optional findInParent(Function finder) { + return this.parent != null ? Optional.ofNullable(finder.apply(this.parent)) : Optional.empty(); + } + + protected final Optional findInSelfOrParent(Function finder) { + return orElseSupply( + finder.apply(this), + () -> this.parent != null ? finder.apply(this.parent) : null + ); + } + + protected final T getInSelfOrParent( + Function finder, + Supplier exceptionSupplier + ) { + return orElseSupply( + finder.apply(this), + () -> this.parent != null ? finder.apply(this.parent) : null + ).orElseThrow(exceptionSupplier); + } + +} diff --git a/util/di/src/main/java/groowt/util/di/Binding.java b/util/di/src/main/java/groowt/util/di/Binding.java new file mode 100644 index 0000000..d9b6ccb --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/Binding.java @@ -0,0 +1,3 @@ +package groowt.util.di; + +sealed public interface Binding permits ClassBinding, ProviderBinding, SingletonBinding, LazySingletonBinding {} diff --git a/util/di/src/main/java/groowt/util/di/BindingConfigurator.java b/util/di/src/main/java/groowt/util/di/BindingConfigurator.java new file mode 100644 index 0000000..4352bc0 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/BindingConfigurator.java @@ -0,0 +1,12 @@ +package groowt.util.di; + +import jakarta.inject.Provider; + +import java.util.function.Supplier; + +public interface BindingConfigurator { + void to(Class target); + void toProvider(Provider provider); + void toSingleton(T target); + void toLazySingleton(Supplier singletonSupplier); +} diff --git a/util/di/src/main/java/groowt/util/di/BindingUtil.java b/util/di/src/main/java/groowt/util/di/BindingUtil.java new file mode 100644 index 0000000..8c4fcf0 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/BindingUtil.java @@ -0,0 +1,32 @@ +package groowt.util.di; + +import jakarta.inject.Provider; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public final class BindingUtil { + + public static Consumer> toClass(Class clazz) { + return bc -> bc.to(clazz); + } + + public static Consumer> toProvider(Provider provider) { + return bc -> bc.toProvider(provider); + } + + public static Consumer> toSingleton(T singleton) { + return bc -> bc.toSingleton(singleton); + } + + public static Consumer> toLazySingleton(Supplier singletonSupplier) { + return bc -> bc.toLazySingleton(singletonSupplier); + } + + public static Consumer> toSelf() { + return bc -> {}; + } + + private BindingUtil() {} + +} diff --git a/util/di/src/main/java/groowt/util/di/ClassBinding.java b/util/di/src/main/java/groowt/util/di/ClassBinding.java new file mode 100644 index 0000000..b01f909 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/ClassBinding.java @@ -0,0 +1,3 @@ +package groowt.util.di; + +public record ClassBinding(Class from, Class to) implements Binding {} diff --git a/util/di/src/main/java/groowt/util/di/DefaultNamedRegistryExtension.java b/util/di/src/main/java/groowt/util/di/DefaultNamedRegistryExtension.java new file mode 100644 index 0000000..22413de --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/DefaultNamedRegistryExtension.java @@ -0,0 +1,80 @@ +package groowt.util.di; + +import jakarta.inject.Named; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class DefaultNamedRegistryExtension implements NamedRegistryExtension { + + protected static class NamedQualifierHandler implements QualifierHandler { + + private final DefaultNamedRegistryExtension extension; + + public NamedQualifierHandler(DefaultNamedRegistryExtension extension) { + this.extension = extension; + } + + @Override + public @Nullable Binding handle(Named named, Class dependencyClass) { + return this.extension.getBinding( + new SimpleKeyHolder<>(NamedRegistryExtension.class, dependencyClass, named.value()) + ); + } + + } + + protected final Map> bindings = new HashMap<>(); + protected final QualifierHandler qualifierHandler = this.getNamedQualifierHandler(); + + protected QualifierHandler getNamedQualifierHandler() { + return new NamedQualifierHandler(this); + } + + @SuppressWarnings("unchecked") + @Override + public @Nullable QualifierHandler getQualifierHandler(Class qualifierType) { + return Named.class.equals(qualifierType) ? (QualifierHandler) this.qualifierHandler : null; + } + + @Override + public Class getKeyClass() { + return String.class; + } + + @Override + public , T> void bind(KeyHolder keyHolder, Consumer> configure) { + final var configurator = new SimpleBindingConfigurator<>(keyHolder.type()); + configure.accept(configurator); + this.bindings.put(keyHolder.key(), configurator.getBinding()); + } + + @SuppressWarnings("unchecked") + @Override + public @Nullable , T> Binding getBinding(KeyHolder keyHolder) { + return (Binding) this.bindings.getOrDefault(keyHolder.key(), null); + } + + @Override + public , T> void removeBinding(KeyHolder keyHolder) { + this.bindings.remove(keyHolder.key()); + } + + @Override + public , T> void removeBindingIf(KeyHolder keyHolder, Predicate> filter) { + final String key = keyHolder.key(); + if (this.bindings.containsKey(key) && filter.test(this.getBinding(keyHolder))) { + this.bindings.remove(key); + } + } + + @Override + public void clearAllBindings() { + this.bindings.clear(); + } + +} diff --git a/util/di/src/main/java/groowt/util/di/DefaultRegistry.java b/util/di/src/main/java/groowt/util/di/DefaultRegistry.java new file mode 100644 index 0000000..419f5e0 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/DefaultRegistry.java @@ -0,0 +1,200 @@ +package groowt.util.di; + +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +public class DefaultRegistry implements Registry { + + protected record ClassKeyBinding(Class key, Binding binding) {} + + protected final Collection> classBindings = new ArrayList<>(); + protected final Collection extensions = new ArrayList<>(); + + @Override + public void removeBinding(Class key) { + this.classBindings.removeIf(classKeyBinding -> classKeyBinding.key().equals(key)); + } + + @SuppressWarnings("unchecked") + @Override + public void removeBindingIf(Class key, Predicate> filter) { + this.classBindings.removeIf(classKeyBinding -> + classKeyBinding.key().equals(key) && filter.test((Binding) classKeyBinding.binding()) + ); + } + + private List getAllRegistryExtensions(Class extensionType) { + return this.extensions.stream() + .filter(extension -> extensionType.isAssignableFrom(extension.getClass())) + .map(extensionType::cast) + .toList(); + } + + private E getOneRegistryExtension(Class extensionType) { + final List extensions = this.getAllRegistryExtensions(extensionType); + if (extensions.size() == 1) { + return extensions.getFirst(); + } else if (extensions.isEmpty()) { + throw new IllegalArgumentException("There is no " + extensionType + " registered for this " + this); + } else { + throw new IllegalArgumentException("There is more than one " + extensionType + " registered for this " + this); + } + } + + @Override + public void addExtension(RegistryExtension extension) { + final List existing = this.getAllRegistryExtensions(extension.getClass()); + if (existing.isEmpty()) { + this.extensions.add(extension); + } else { + throw new IllegalArgumentException("There is already at least one " + extension.getClass() + " registered in " + this); + } + } + + @Override + public E getExtension(Class extensionType) { + return this.getOneRegistryExtension(extensionType); + } + + @Override + public Collection getExtensions(Class extensionType) { + return this.getAllRegistryExtensions(extensionType); + } + + @Override + public void removeExtension(RegistryExtension extension) { + this.extensions.remove(extension); + } + + @Override + public @Nullable QualifierHandler getQualifierHandler(Class qualifierType) { + final List> handlers = new ArrayList<>(); + for (final var extension : this.extensions) { + if (extension instanceof QualifierHandlerContainer handlerContainer) { + final var handler = handlerContainer.getQualifierHandler(qualifierType); + if (handler != null) { + handlers.add(handler); + } + } + } + if (handlers.isEmpty()) { + return null; + } else if (handlers.size() > 1) { + throw new RuntimeException("There is more than one QualifierHandler for " + qualifierType.getName()); + } else { + return handlers.getFirst(); + } + } + + @Override + public @Nullable ScopeHandler getScopeHandler(Class scopeType) { + final List> handlers = new ArrayList<>(); + for (final var extension : this.extensions) { + if (extension instanceof ScopeHandlerContainer handlerContainer) { + final var handler = handlerContainer.getScopeHandler(scopeType); + if (handler != null) { + handlers.add(handler); + } + } + } + if (handlers.isEmpty()) { + return null; + } else if (handlers.size() > 1) { + throw new RuntimeException("There is more than one ScopeHandler for " + scopeType.getName()); + } else { + return handlers.getFirst(); + } + } + + @Override + public void bind(Class key, Consumer> configure) { + final var configurator = new SimpleBindingConfigurator<>(key); + configure.accept(configurator); + this.classBindings.add(new ClassKeyBinding<>(key, configurator.getBinding())); + } + + @SuppressWarnings("unchecked") + @Override + public @Nullable Binding getBinding(Class key) { + for (final var classKeyBinding : this.classBindings) { + if (key.isAssignableFrom(classKeyBinding.key())) { + return (Binding) classKeyBinding.binding(); + } + } + return null; + } + + private KeyBinder findKeyBinder(Class keyClass) { + final List> binders = new ArrayList<>(); + for (final var extension : this.extensions) { + if (extension instanceof KeyBinder keyBinder && keyBinder.getKeyClass().isAssignableFrom(keyClass)) { + binders.add(keyBinder); + } + } + if (binders.isEmpty()) { + throw new IllegalArgumentException("There are no configured RegistryExtensions that can handle keys with type " + keyClass.getName()); + } else if (binders.size() > 1) { + throw new IllegalArgumentException("There is more than one configured RegistryExtension that can handle keys with type " + keyClass.getName()); + } else { + return binders.getFirst(); + } + } + + @SuppressWarnings("rawtypes") + protected final void withKeyBinder(KeyHolder keyHolder, Consumer action) { + action.accept(this.findKeyBinder(keyHolder.key().getClass())); + } + + @SuppressWarnings("rawtypes") + protected final @Nullable R tapKeyBinder( + KeyHolder keyHolder, + Function function + ) { + return function.apply(this.findKeyBinder(keyHolder.key().getClass())); + } + + @SuppressWarnings("unchecked") + @Override + public void bind(KeyHolder keyHolder, Consumer> configure) { + this.withKeyBinder(keyHolder, b -> b.bind(keyHolder, configure)); + } + + @SuppressWarnings("unchecked") + @Override + public @Nullable Binding getBinding(KeyHolder keyHolder) { + return this.tapKeyBinder(keyHolder, b -> b.getBinding(keyHolder)); + } + + @SuppressWarnings("unchecked") + @Override + public void removeBinding(KeyHolder keyHolder) { + this.withKeyBinder(keyHolder, b -> b.removeBinding(keyHolder)); + } + + @SuppressWarnings("unchecked") + @Override + public void removeBindingIf( + KeyHolder keyHolder, + Predicate> filter + ) { + this.withKeyBinder(keyHolder, b -> b.removeBindingIf(keyHolder, filter)); + } + + @Override + public void clearAllBindings() { + this.classBindings.clear(); + for (final var extension : this.extensions) { + if (extension instanceof KeyBinder keyBinder) { + keyBinder.clearAllBindings(); + } + } + } + +} diff --git a/util/di/src/main/java/groowt/util/di/DefaultRegistryObjectFactory.java b/util/di/src/main/java/groowt/util/di/DefaultRegistryObjectFactory.java new file mode 100644 index 0000000..422c146 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/DefaultRegistryObjectFactory.java @@ -0,0 +1,313 @@ +package groowt.util.di; + +import groowt.util.di.filters.FilterHandler; +import groowt.util.di.filters.IterableFilterHandler; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; + +import static groowt.util.di.RegistryObjectFactoryUtil.*; + +public class DefaultRegistryObjectFactory extends AbstractRegistryObjectFactory { + + public static final class Builder extends AbstractRegistryObjectFactory.AbstractBuilder { + + /** + * Creates a {@code Builder} initialized with a {@link DefaultRegistry}, which is in-turn configured with a + * {@link NamedRegistryExtension} and a {@link SingletonScopeHandler}. + * + * @return the builder + */ + public static Builder withDefaults() { + final var b = new Builder(); + + b.configureRegistry(r -> { + r.addExtension(new DefaultNamedRegistryExtension()); + r.addExtension(new SingletonRegistryExtension(r)); + }); + + return b; + } + + /** + * @return a blank builder with a {@link Registry} from the given {@link Supplier}. + */ + public static Builder withRegistry(Supplier registrySupplier) { + return new Builder(registrySupplier.get()); + } + + /** + * @return a blank builder which will use {@link DefaultRegistry}. + */ + public static Builder blank() { + return new Builder(); + } + + private Builder(Registry registry) { + super(registry); + } + + private Builder() { + super(); + } + + @Override + public DefaultRegistryObjectFactory build() { + return new DefaultRegistryObjectFactory( + this.getRegistry(), + this.getParent(), + this.getFilterHandlers(), + this.getIterableFilterHandlers() + ); + } + + } + + private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + private static final Logger logger = LoggerFactory.getLogger(DefaultRegistryObjectFactory.class); // leave it for the future! + + private final Collection> filterHandlers; + private final Collection> iterableFilterHandlers; + + protected DefaultRegistryObjectFactory( + Registry registry, + @Nullable RegistryObjectFactory parent, + Collection> filterHandlers, + Collection> iterableFilterHandlers + ) { + super(registry, parent); + this.filterHandlers = new ArrayList<>(filterHandlers); + this.filterHandlers.forEach(handler -> checkIsValidFilter(handler.getAnnotationClass())); + + this.iterableFilterHandlers = new ArrayList<>(iterableFilterHandlers); + this.iterableFilterHandlers.forEach(handler -> checkIsValidIterableFilter(handler.getAnnotationClass())); + } + + /** + * Checks if the given parameter has any qualifier annotations; if it does, + * it delegates finding the desired object to the registered {@link QualifierHandler}. + * + * @param parameter the parameter + * @return the object returned from the {@code QualifierHandler}, or {@code null} if no qualifier + * is present or the {@code QualifierHandler} itself returns {@code null}. + * + * @throws RuntimeException if no {@code QualifierHandler} is registered for a qualifier annotation present on the + * given parameter, or if the handler itself throws an exception. + */ + @SuppressWarnings("unchecked") + protected final @Nullable Object tryQualifiers(Parameter parameter) { + final Class paramType = parameter.getType(); + final List qualifiers = RegistryObjectFactoryUtil.getQualifierAnnotations(parameter.getAnnotations()); + if (qualifiers.size() > 1) { + throw new RuntimeException("Parameter " + parameter + " cannot have more than one Qualifier annotation."); + } else if (qualifiers.size() == 1) { + final Annotation qualifier = qualifiers.getFirst(); + @SuppressWarnings("rawtypes") + final QualifierHandler handler = this.getInSelfOrParent( + f -> f.findQualifierHandler(qualifier.annotationType()), + () -> new RuntimeException("There is no configured QualifierHandler for " + qualifier.annotationType().getName()) + ); + final Binding binding = handler.handle(qualifier, paramType); + if (binding != null) { + return this.handleBinding(binding, EMPTY_OBJECT_ARRAY); + } + } + // no Qualifier or the QualifierHandler didn't return a Binding + return null; + } + + /** + * Checks the {@code resolvedArg} against all filters present on the given parameter. + * + * @param parameter the parameter + * @param resolvedArg the resolved argument + * + * @throws RuntimeException if the {@link FilterHandler} itself throws an exception. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + protected final void checkFilters(Parameter parameter, Object resolvedArg) { + final Annotation[] allAnnotations = parameter.getAnnotations(); + final Collection filterAnnotations = getFilterAnnotations(allAnnotations); + if (!filterAnnotations.isEmpty()) { + final Collection> filtersForParamType = this.filterHandlers.stream() + .filter(filterHandler -> + filterHandler.getArgumentClass().isAssignableFrom(parameter.getType()) + ) + .toList(); + for (final Annotation filterAnnotation : filterAnnotations) { + for (final FilterHandler filterHandler : filtersForParamType) { + if (filterAnnotation.annotationType().equals(filterHandler.getAnnotationClass())) { + // hopefully we've checked everything + ((FilterHandler) filterHandler).check(filterAnnotation, resolvedArg); + } + } + } + } + final Collection iterableFilterAnnotations = getIterableFilterAnnotations(allAnnotations); + if (!iterableFilterAnnotations.isEmpty() && resolvedArg instanceof Iterable iterable) { + for (final var annotation : iterableFilterAnnotations) { + this.iterableFilterHandlers.stream() + .filter(handler -> handler.getAnnotationClass().equals(annotation.annotationType())) + .forEach(handler -> { + ((IterableFilterHandler) handler).check(annotation, iterable); + }); + } + } + } + + protected final Object resolveInjectedArg(Parameter parameter) { + final Object qualifierProvidedArg = this.tryQualifiers(parameter); + if (qualifierProvidedArg != null) { + this.checkFilters(parameter, qualifierProvidedArg); + return qualifierProvidedArg; + } else { + final Object created = this.get(parameter.getType()); + this.checkFilters(parameter, created); + return created; + } + } + + protected final void resolveInjectedArgs(Object[] dest, Parameter[] params) { + for (int i = 0; i < params.length; i++) { + dest[i] = this.resolveInjectedArg(params[i]); + } + } + + protected final void resolveGivenArgs(Object[] dest, Parameter[] params, Object[] givenArgs, int startIndex) { + for (int i = startIndex; i < dest.length; i++) { + final int resolveIndex = i - startIndex; + final Object arg = givenArgs[resolveIndex]; + this.checkFilters(params[resolveIndex], arg); + dest[i] = arg; + } + } + + // TODO: when there is a null arg, we lose the type. Therefore this algorithm breaks. Fix this. + @Override + protected Object[] createArgs(Constructor constructor, Object[] givenArgs) { + final Class[] paramTypes = constructor.getParameterTypes(); + + // check no arg + if (paramTypes.length == 0 && givenArgs.length == 0) { + // no args given, none needed, so return empty array + return EMPTY_OBJECT_ARRAY; + } else if (paramTypes.length == 0) { // implicit that givenArgs.length != 0 + // zero expected, but got given args + throw new RuntimeException( + "Expected zero args for constructor " + constructor + " but received " + Arrays.toString(givenArgs) + ); + } else if (givenArgs.length > paramTypes.length) { + // expected is more than zero, but received too many given + throw new RuntimeException( + "Too many args given for constructor " + constructor + "; received " + Arrays.toString(givenArgs) + ); + } + + final Parameter[] allParams = constructor.getParameters(); + final Object[] resolvedArgs = new Object[allParams.length]; + + if (givenArgs.length == 0) { + // if no given args, then they are all injected + this.resolveInjectedArgs(resolvedArgs, allParams); + } else if (givenArgs.length == paramTypes.length) { + // all are given + this.resolveGivenArgs(resolvedArgs, allParams, givenArgs, 0); + } else { + // some are injected, some are given + // everything before (non-inclusive) is injected + // everything after (inclusive) is given + // ex: 1 inject, 1 given -> 2 (allParams) - 1 = 1 + // ex: 0 inject, 1 given -> 1 - 1 = 0 + final int firstGivenIndex = allParams.length - givenArgs.length; + + final Parameter[] injectedParams = new Parameter[firstGivenIndex]; + final Parameter[] givenParams = new Parameter[allParams.length - firstGivenIndex]; + + System.arraycopy(allParams, 0, injectedParams, 0, injectedParams.length); + System.arraycopy(allParams, firstGivenIndex, givenParams, 0, allParams.length - firstGivenIndex); + + this.resolveInjectedArgs(resolvedArgs, injectedParams); + this.resolveGivenArgs(resolvedArgs, givenParams, givenArgs, firstGivenIndex); + } + + return resolvedArgs; + } + + @SuppressWarnings("unchecked") + private T handleBinding(Binding binding, Object[] constructorArgs) { + return switch (binding) { + case ClassBinding(Class ignored, Class to) -> { + final Annotation scopeAnnotation = getScopeAnnotation(to); + if (scopeAnnotation != null) { + final Class scopeClass = scopeAnnotation.annotationType(); + @SuppressWarnings("rawtypes") + final ScopeHandler scopeHandler = this.getInSelfOrParent( + f -> f.findScopeHandler(scopeClass), + () -> new RuntimeException("There is no configured ScopeHandler for " + scopeClass.getName()) + ); + final Binding scopedBinding = scopeHandler.onScopedDependencyRequest(scopeAnnotation, to, this); + yield this.handleBinding(scopedBinding, constructorArgs); + } else { + yield this.createInstance(to, constructorArgs); + } + } + case ProviderBinding providerBinding -> providerBinding.provider().get(); + case SingletonBinding singletonBinding -> singletonBinding.to(); + case LazySingletonBinding lazySingletonBinding -> lazySingletonBinding.singletonSupplier().get(); + }; + } + + protected final @Nullable Binding searchRegistry(Class from) { + return this.registry.getBinding(from); + } + + protected @Nullable T tryParent(Class clazz, Object[] constructorArgs) { + return this.findInParent(f -> f.getOrNull(clazz, constructorArgs)).orElse(null); + } + + @Override + protected Object getSetterInjectArg(Class targetType, Method setter, Parameter toInject) { + return this.resolveInjectedArg(toInject); + } + + /** + * {@inheritDoc} + */ + @Override + public T get(Class clazz, Object... constructorArgs) { + final Binding binding = this.searchRegistry(clazz); + if (binding != null) { + return this.handleBinding(binding, constructorArgs); + } + final T parentResult = this.tryParent(clazz, constructorArgs); + if (parentResult != null) { + return parentResult; + } else { + throw new RuntimeException("No bindings for " + clazz + " with args " + Arrays.toString(constructorArgs) + "."); + } + } + + /** + * {@inheritDoc} + */ + @Override + public T getOrDefault(Class clazz, T defaultValue, Object... constructorArgs) { + final Binding binding = this.searchRegistry(clazz); + if (binding != null) { + return this.handleBinding(binding, constructorArgs); + } + final T parentResult = this.tryParent(clazz, constructorArgs); + return parentResult != null ? parentResult : defaultValue; + } + +} diff --git a/util/di/src/main/java/groowt/util/di/ExtensionContainer.java b/util/di/src/main/java/groowt/util/di/ExtensionContainer.java new file mode 100644 index 0000000..75bd6eb --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/ExtensionContainer.java @@ -0,0 +1,10 @@ +package groowt.util.di; + +import java.util.Collection; + +public interface ExtensionContainer { + void addExtension(RegistryExtension extension); + E getExtension(Class extensionType); + Collection getExtensions(Class extensionType); + void removeExtension(RegistryExtension extension); +} diff --git a/util/di/src/main/java/groowt/util/di/KeyBinder.java b/util/di/src/main/java/groowt/util/di/KeyBinder.java new file mode 100644 index 0000000..ab12600 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/KeyBinder.java @@ -0,0 +1,16 @@ +package groowt.util.di; + +import org.jetbrains.annotations.Nullable; + +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public interface KeyBinder { + Class getKeyClass(); + , T> void bind(KeyHolder keyHolder, Consumer> configure); + , T> @Nullable Binding getBinding(KeyHolder keyHolder); + , T> void removeBinding(KeyHolder keyHolder); + , T> void removeBindingIf(KeyHolder keyHolder, Predicate> filter); + void clearAllBindings(); +} diff --git a/util/di/src/main/java/groowt/util/di/KeyHolder.java b/util/di/src/main/java/groowt/util/di/KeyHolder.java new file mode 100644 index 0000000..953b645 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/KeyHolder.java @@ -0,0 +1,7 @@ +package groowt.util.di; + +public interface KeyHolder, K, T> { + Class binderType(); + Class type(); + K key(); +} diff --git a/util/di/src/main/java/groowt/util/di/LazySingletonBinding.java b/util/di/src/main/java/groowt/util/di/LazySingletonBinding.java new file mode 100644 index 0000000..d392a48 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/LazySingletonBinding.java @@ -0,0 +1,5 @@ +package groowt.util.di; + +import java.util.function.Supplier; + +public record LazySingletonBinding(Supplier singletonSupplier) implements Binding {} diff --git a/util/di/src/main/java/groowt/util/di/NamedRegistryExtension.java b/util/di/src/main/java/groowt/util/di/NamedRegistryExtension.java new file mode 100644 index 0000000..86be96f --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/NamedRegistryExtension.java @@ -0,0 +1,9 @@ +package groowt.util.di; + +public interface NamedRegistryExtension extends RegistryExtension, KeyBinder, QualifierHandlerContainer { + + static KeyHolder named(String name, Class type) { + return new SimpleKeyHolder<>(NamedRegistryExtension.class, type, name); + } + +} diff --git a/util/di/src/main/java/groowt/util/di/ObjectFactory.java b/util/di/src/main/java/groowt/util/di/ObjectFactory.java new file mode 100644 index 0000000..db0ee81 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/ObjectFactory.java @@ -0,0 +1,61 @@ +package groowt.util.di; + +import org.jetbrains.annotations.Contract; + +import java.util.function.Function; + +/** + * An {@link ObjectFactory} is an object that can construct objects of given types. + */ +@FunctionalInterface +public interface ObjectFactory { + + /** + * Create a new instance of the given {@code instanceType} with the given constructor args. + * + * @apiNote An implementation may provide a subclass of the given instance type, + * or it may directly instantiate the given type, if it is a class + * and it can determine the correct constructor from the given arguments. + * See individual implementation documentation for exact behavior. + * + * @implSpec It is up to individual implementations of {@link ObjectFactory} to determine how to + * select the appropriate constructor for the given type. The returned + * instance must be new and in a valid state. + * + * @param instanceType the {@link Class} of the desired type + * @param constructorArgs any arguments to pass to the constructor(s) of the class. + * @return the new instance + * @param the desired type + */ + @Contract("_, _-> new") + T createInstance(Class instanceType, Object... constructorArgs); + + /** + * Very similar to {@link #createInstance(Class, Object...)}, but catches any {@link RuntimeException} + * thrown by {@link #createInstance} and subsequently passes it to the given {@link Function}, returning + * instead the return value of the {@link Function}. + * + * @param instanceType the desired type of the created instance + * @param onException a {@link Function} to handle when an exception occurs and return a value nonetheless + * @param constructorArgs arguments to pass to the constructor + * @return the created instance + * @param the desired type + * + * @throws RuntimeException if the given {@link Function} itself throws a RuntimeException + * + * @see #createInstance(Class, Object...) + */ + @Contract("_, _, _ -> new") + default T createInstanceCatching( + Class instanceType, + Function onException, + Object... constructorArgs + ) { + try { + return this.createInstance(instanceType, constructorArgs); + } catch (RuntimeException runtimeException) { + return onException.apply(runtimeException); + } + } + +} diff --git a/util/di/src/main/java/groowt/util/di/ObjectFactoryUtil.java b/util/di/src/main/java/groowt/util/di/ObjectFactoryUtil.java new file mode 100644 index 0000000..9e0a10f --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/ObjectFactoryUtil.java @@ -0,0 +1,23 @@ +package groowt.util.di; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class ObjectFactoryUtil { + + public static Class[] toTypes(Object... objects) { + final Class[] types = new Class[objects.length]; + for (int i = 0; i < objects.length; i++) { + final Object o = objects[i]; + if (o != null) { + types[i] = o.getClass(); + } else { + types[i] = Object.class; + } + } + return types; + } + + private ObjectFactoryUtil() {} + +} diff --git a/util/di/src/main/java/groowt/util/di/ProviderBinding.java b/util/di/src/main/java/groowt/util/di/ProviderBinding.java new file mode 100644 index 0000000..8ab5979 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/ProviderBinding.java @@ -0,0 +1,5 @@ +package groowt.util.di; + +import jakarta.inject.Provider; + +public record ProviderBinding(Class to, Provider provider) implements Binding {} diff --git a/util/di/src/main/java/groowt/util/di/QualifierHandler.java b/util/di/src/main/java/groowt/util/di/QualifierHandler.java new file mode 100644 index 0000000..3b04737 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/QualifierHandler.java @@ -0,0 +1,10 @@ +package groowt.util.di; + +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; + +@FunctionalInterface +public interface QualifierHandler { + @Nullable Binding handle(A annotation, Class dependencyClass); +} diff --git a/util/di/src/main/java/groowt/util/di/QualifierHandlerContainer.java b/util/di/src/main/java/groowt/util/di/QualifierHandlerContainer.java new file mode 100644 index 0000000..2fd98bf --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/QualifierHandlerContainer.java @@ -0,0 +1,20 @@ +package groowt.util.di; + +import jakarta.inject.Qualifier; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; + +public interface QualifierHandlerContainer { + + static void checkIsValidQualifier(Class annotationClass) { + if (!annotationClass.isAnnotationPresent(Qualifier.class)) { + throw new IllegalArgumentException( + "The given qualifier annotation + " + annotationClass + " is itself not annotated with @Qualifier" + ); + } + } + + @Nullable QualifierHandler getQualifierHandler(Class qualifierType); + +} diff --git a/util/di/src/main/java/groowt/util/di/Registry.java b/util/di/src/main/java/groowt/util/di/Registry.java new file mode 100644 index 0000000..e7c1862 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/Registry.java @@ -0,0 +1,20 @@ +package groowt.util.di; + +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +public interface Registry extends ExtensionContainer, QualifierHandlerContainer, ScopeHandlerContainer { + void bind(Class key, Consumer> configure); + @Nullable Binding getBinding(Class key); + void removeBinding(Class key); + void removeBindingIf(Class key, Predicate> filter); + + void bind(KeyHolder keyHolder, Consumer> configure); + @Nullable Binding getBinding(KeyHolder keyHolder); + void removeBinding(KeyHolder keyHolder); + void removeBindingIf(KeyHolder keyHolder, Predicate> filter); + void clearAllBindings(); + +} diff --git a/util/di/src/main/java/groowt/util/di/RegistryExtension.java b/util/di/src/main/java/groowt/util/di/RegistryExtension.java new file mode 100644 index 0000000..5010d43 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/RegistryExtension.java @@ -0,0 +1,3 @@ +package groowt.util.di; + +public interface RegistryExtension {} diff --git a/util/di/src/main/java/groowt/util/di/RegistryObjectFactory.java b/util/di/src/main/java/groowt/util/di/RegistryObjectFactory.java new file mode 100644 index 0000000..a0d236c --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/RegistryObjectFactory.java @@ -0,0 +1,99 @@ +package groowt.util.di; + +import groowt.util.di.filters.FilterHandler; +import groowt.util.di.filters.IterableFilterHandler; +import jakarta.inject.Provider; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.util.function.Consumer; + +/** + * A {@link RegistryObjectFactory} is an {@link ObjectFactory} that offers the ability + * to provide desired objects based on an instance of + * {@link Registry} to determine how to provide those objects. + */ +public interface RegistryObjectFactory extends ObjectFactory { + + interface Builder { + void configureRegistry(Consumer configure); + void addFilterHandler(FilterHandler handler); + void addIterableFilterHandler(IterableFilterHandler handler); + T build(); + } + + void configureRegistry(Consumer use); + + @Nullable ScopeHandler findScopeHandler(Class scopeType); + + @Nullable QualifierHandler findQualifierHandler(Class qualifierType); + + /** + * Get an object with the desired type. How it is retrieved/created + * depends upon the {@link Binding} present in this {@link RegistryObjectFactory}'s held + * instances of {@link Registry}. The type of the {@link Binding} determines + * how the object is fetched: + * + *

+ * + * @implNote If {@code constructorArgs} are provided + * and the {@link Binding} for the desired type is not a + * {@link ClassBinding}, the implementation should + * either throw an exception or log a warning at the least. + * + * @param clazz the {@link Class} of the desired type + * @param constructorArgs As in {@link #createInstance(Class, Object...)}, + * the arguments which will be used to create the desired object + * if the {@link Binding} is a {@link ClassBinding}. + * @return an object of the desired type + * @param the desired type + * + * @throws RuntimeException if there is no registered {@link Binding} or there is a problem + * fetching or constructing the object. + */ + T get(Class clazz, Object... constructorArgs); + + /** + * Similarly to {@link #get(Class, Object...)}, fetches an object + * of the desired type, but does not throw if there is no registered {@link Binding} + * in any of the held instances of {@link Registry}, + * and instead returns the given {@code defaultValue}. + * + * @param clazz the {@link Class} of the desired type + * @param defaultValue the defaultValue to return + * @param constructorArgs see {@link #get(Class, Object...)} + * @return an object of the desired type + * @param the desired type + * + * @throws RuntimeException if there is a registered {@link Binding} and there is a problem + * fetching or constructing the object. + * + * @see #get(Class, Object...) + */ + T getOrDefault(Class clazz, T defaultValue, Object... constructorArgs); + + /** + * Similar to {@link #getOrDefault(Class, Object, Object...)}, except that + * it returns null by default if there is no registered {@link Binding}. + * + * @param clazz the {@link Class} of the desired type + * @param constructorArgs see {@link RegistryObjectFactory#get(Class, Object...)} + * @return an object of the desired type + * @param the desired type + * + * @see RegistryObjectFactory#get(Class, Object...) + * @see RegistryObjectFactory#getOrDefault(Class, Object, Object...) + * + * @throws RuntimeException if there is a registered {@code Binding} and there + * is a problem fetching or constructing the object. + */ + default @Nullable T getOrNull(Class clazz, Object... constructorArgs) { + return this.getOrDefault(clazz, null, constructorArgs); + } + +} diff --git a/util/di/src/main/java/groowt/util/di/RegistryObjectFactoryUtil.java b/util/di/src/main/java/groowt/util/di/RegistryObjectFactoryUtil.java new file mode 100644 index 0000000..d32b351 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/RegistryObjectFactoryUtil.java @@ -0,0 +1,69 @@ +package groowt.util.di; + +import groowt.util.di.filters.Filter; +import groowt.util.di.filters.IterableFilter; +import jakarta.inject.Qualifier; +import jakarta.inject.Scope; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +@ApiStatus.Internal +public final class RegistryObjectFactoryUtil { + + private RegistryObjectFactoryUtil() {} + + public static List getQualifierAnnotations(Annotation[] annotations) { + return Arrays.stream(annotations) + .filter(a -> a.annotationType().isAnnotationPresent(Qualifier.class)) + .toList(); + } + + public static List getFilterAnnotations(Annotation[] annotations) { + return Arrays.stream(annotations) + .filter(a -> a.annotationType().isAnnotationPresent(Filter.class)) + .toList(); + } + + public static List getIterableFilterAnnotations(Annotation[] annotations) { + return Arrays.stream(annotations) + .filter(a -> a.annotationType().isAnnotationPresent(IterableFilter.class)) + .toList(); + } + + public static Optional orElseSupply(T first, Supplier onNullFirst) { + return first != null ? Optional.of(first) : Optional.ofNullable(onNullFirst.get()); + } + + public static void checkIsValidFilter(Class annotationClass) { + if (!annotationClass.isAnnotationPresent(Filter.class)) { + throw new IllegalArgumentException( + "The given filter annotation " + annotationClass.getName() + " is itself not annotated with @Filter" + ); + } + } + + public static void checkIsValidIterableFilter(Class annotationClass) { + if (!annotationClass.isAnnotationPresent(IterableFilter.class)) { + throw new IllegalArgumentException( + "The given iterable filter annotation " + annotationClass.getName() + " is itself not annotated with @IterableFilter" + ); + } + } + + public static @Nullable Annotation getScopeAnnotation(Class clazz) { + final List scopeAnnotations = Arrays.stream(clazz.getAnnotations()) + .filter(annotation -> annotation.annotationType().isAnnotationPresent(Scope.class)) + .toList(); + if (scopeAnnotations.size() > 1) { + throw new RuntimeException(clazz.getName() + " has too many annotations that are themselves annotated with @Scope"); + } + return scopeAnnotations.size() == 1 ? scopeAnnotations.getFirst() : null; + } + +} diff --git a/util/di/src/main/java/groowt/util/di/ScopeHandler.java b/util/di/src/main/java/groowt/util/di/ScopeHandler.java new file mode 100644 index 0000000..31f3bc8 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/ScopeHandler.java @@ -0,0 +1,8 @@ +package groowt.util.di; + +import java.lang.annotation.Annotation; + +public interface ScopeHandler { + Binding onScopedDependencyRequest(A annotation, Class dependencyClass, RegistryObjectFactory objectFactory); + void reset(); +} diff --git a/util/di/src/main/java/groowt/util/di/ScopeHandlerContainer.java b/util/di/src/main/java/groowt/util/di/ScopeHandlerContainer.java new file mode 100644 index 0000000..b08d3aa --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/ScopeHandlerContainer.java @@ -0,0 +1,20 @@ +package groowt.util.di; + +import jakarta.inject.Scope; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; + +public interface ScopeHandlerContainer { + + static void checkIsValidScope(Class scope) { + if (!scope.isAnnotationPresent(Scope.class)) { + throw new IllegalArgumentException( + "The given scope annotation " + scope + " is itself not annotated with @Scope" + ); + } + } + + @Nullable ScopeHandler getScopeHandler(Class scopeType); + +} diff --git a/util/di/src/main/java/groowt/util/di/SimpleBindingConfigurator.java b/util/di/src/main/java/groowt/util/di/SimpleBindingConfigurator.java new file mode 100644 index 0000000..4a682b1 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/SimpleBindingConfigurator.java @@ -0,0 +1,43 @@ +package groowt.util.di; + +import jakarta.inject.Provider; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +public class SimpleBindingConfigurator implements BindingConfigurator { + + private final Class from; + private @Nullable Binding binding; + + public SimpleBindingConfigurator(Class from) { + this.from = from; + } + + public final Binding getBinding() { + return this.binding != null + ? this.binding + : new ClassBinding<>(this.from, this.from); // return SelfBinding in case we never called anything + } + + @Override + public void to(Class target) { + this.binding = new ClassBinding<>(this.from, target); + } + + @Override + public void toProvider(Provider provider) { + this.binding = new ProviderBinding<>(this.from, provider); + } + + @Override + public void toSingleton(T target) { + this.binding = new SingletonBinding<>(target); + } + + @Override + public void toLazySingleton(Supplier singletonSupplier) { + this.binding = new LazySingletonBinding<>(singletonSupplier); + } + +} diff --git a/util/di/src/main/java/groowt/util/di/SimpleKeyHolder.java b/util/di/src/main/java/groowt/util/di/SimpleKeyHolder.java new file mode 100644 index 0000000..47a1745 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/SimpleKeyHolder.java @@ -0,0 +1,4 @@ +package groowt.util.di; + +public record SimpleKeyHolder, K, T>(Class binderType, Class type, K key) + implements KeyHolder {} diff --git a/util/di/src/main/java/groowt/util/di/SingletonBinding.java b/util/di/src/main/java/groowt/util/di/SingletonBinding.java new file mode 100644 index 0000000..67e04c6 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/SingletonBinding.java @@ -0,0 +1,3 @@ +package groowt.util.di; + +public record SingletonBinding(T to) implements Binding {} diff --git a/util/di/src/main/java/groowt/util/di/SingletonRegistryExtension.java b/util/di/src/main/java/groowt/util/di/SingletonRegistryExtension.java new file mode 100644 index 0000000..0e6e70e --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/SingletonRegistryExtension.java @@ -0,0 +1,22 @@ +package groowt.util.di; + +import jakarta.inject.Singleton; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; + +public class SingletonRegistryExtension implements RegistryExtension, ScopeHandlerContainer { + + private final SingletonScopeHandler handler; + + public SingletonRegistryExtension(Registry owner) { + this.handler = new SingletonScopeHandler(owner); + } + + @SuppressWarnings("unchecked") + @Override + public @Nullable ScopeHandler getScopeHandler(Class scopeType) { + return Singleton.class.isAssignableFrom(scopeType) ? (ScopeHandler) this.handler : null; + } + +} diff --git a/util/di/src/main/java/groowt/util/di/SingletonScopeHandler.java b/util/di/src/main/java/groowt/util/di/SingletonScopeHandler.java new file mode 100644 index 0000000..0be9ab6 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/SingletonScopeHandler.java @@ -0,0 +1,35 @@ +package groowt.util.di; + +import jakarta.inject.Singleton; + +import static groowt.util.di.BindingUtil.toSingleton; + +public final class SingletonScopeHandler implements ScopeHandler { + + private final Registry owner; + + public SingletonScopeHandler(Registry owner) { + this.owner = owner; + } + + @Override + public Binding onScopedDependencyRequest( + Singleton annotation, + Class dependencyClass, + RegistryObjectFactory objectFactory + ) { + final Binding potentialBinding = this.owner.getBinding(dependencyClass); + if (potentialBinding != null) { + return potentialBinding; + } else { + this.owner.bind(dependencyClass, toSingleton(objectFactory.createInstance(dependencyClass))); + return this.owner.getBinding(dependencyClass); + } + } + + @Override + public void reset() { + throw new UnsupportedOperationException("Cannot reset the Singleton scope!"); + } + +} diff --git a/util/di/src/main/java/groowt/util/di/annotation/Given.java b/util/di/src/main/java/groowt/util/di/annotation/Given.java new file mode 100644 index 0000000..ce94d25 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/annotation/Given.java @@ -0,0 +1,10 @@ +package groowt.util.di.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.PARAMETER) +public @interface Given {} diff --git a/util/di/src/main/java/groowt/util/di/filters/Filter.java b/util/di/src/main/java/groowt/util/di/filters/Filter.java new file mode 100644 index 0000000..3f36642 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/filters/Filter.java @@ -0,0 +1,10 @@ +package groowt.util.di.filters; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.ANNOTATION_TYPE }) +public @interface Filter {} diff --git a/util/di/src/main/java/groowt/util/di/filters/FilterHandler.java b/util/di/src/main/java/groowt/util/di/filters/FilterHandler.java new file mode 100644 index 0000000..8581259 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/filters/FilterHandler.java @@ -0,0 +1,39 @@ +package groowt.util.di.filters; + +import java.lang.annotation.Annotation; +import java.util.Objects; +import java.util.function.BiPredicate; + +public interface FilterHandler { + + boolean check(A annotation, T arg); + Class getAnnotationClass(); + Class getArgumentClass(); + + default FilterHandler and(BiPredicate and) { + Objects.requireNonNull(and); + return new SimpleFilterHandler<>( + (a, t) -> this.check(a, t) && and.test(a, t), + this.getAnnotationClass(), + this.getArgumentClass() + ); + } + + default FilterHandler or(BiPredicate or) { + Objects.requireNonNull(or); + return new SimpleFilterHandler<>( + (a, t) -> this.check(a, t) || or.test(a, t), + this.getAnnotationClass(), + this.getArgumentClass() + ); + } + + default FilterHandler negate() { + return new SimpleFilterHandler<>( + (a, t) -> !this.check(a, t), + this.getAnnotationClass(), + this.getArgumentClass() + ); + } + +} diff --git a/util/di/src/main/java/groowt/util/di/filters/FilterHandlers.java b/util/di/src/main/java/groowt/util/di/filters/FilterHandlers.java new file mode 100644 index 0000000..08e908e --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/filters/FilterHandlers.java @@ -0,0 +1,35 @@ +package groowt.util.di.filters; + +import java.lang.annotation.*; +import java.util.function.BiPredicate; + +import static groowt.util.di.filters.FilterUtil.isAssignableToAnyOf; + +public final class FilterHandlers { + + private FilterHandlers() {} + + public static FilterHandler of( + Class annotationClass, + Class argClass, + BiPredicate predicate + ) { + return new SimpleFilterHandler<>(predicate, annotationClass, argClass); + } + + @Filter + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface AllowTypes { + Class[] value(); + } + + public static FilterHandler getAllowsTypesFilterHandler(Class targetType) { + return of( + AllowTypes.class, + targetType, + (annotation, target) -> isAssignableToAnyOf(target.getClass(), annotation.value()) + ); + } + +} diff --git a/util/di/src/main/java/groowt/util/di/filters/FilterUtil.java b/util/di/src/main/java/groowt/util/di/filters/FilterUtil.java new file mode 100644 index 0000000..7471625 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/filters/FilterUtil.java @@ -0,0 +1,16 @@ +package groowt.util.di.filters; + +public final class FilterUtil { + + private FilterUtil() {} + + public static boolean isAssignableToAnyOf(Class subject, Class[] tests) { + for (final var test : tests) { + if (test.isAssignableFrom(subject)) { + return true; + } + } + return false; + } + +} diff --git a/util/di/src/main/java/groowt/util/di/filters/IterableFilter.java b/util/di/src/main/java/groowt/util/di/filters/IterableFilter.java new file mode 100644 index 0000000..63a80d1 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/filters/IterableFilter.java @@ -0,0 +1,10 @@ +package groowt.util.di.filters; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface IterableFilter {} diff --git a/util/di/src/main/java/groowt/util/di/filters/IterableFilterHandler.java b/util/di/src/main/java/groowt/util/di/filters/IterableFilterHandler.java new file mode 100644 index 0000000..26005e1 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/filters/IterableFilterHandler.java @@ -0,0 +1,8 @@ +package groowt.util.di.filters; + +import java.lang.annotation.Annotation; + +public interface IterableFilterHandler { + boolean check(A annotation, Iterable iterable); + Class getAnnotationClass(); +} diff --git a/util/di/src/main/java/groowt/util/di/filters/IterableFilterHandlers.java b/util/di/src/main/java/groowt/util/di/filters/IterableFilterHandlers.java new file mode 100644 index 0000000..78cde67 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/filters/IterableFilterHandlers.java @@ -0,0 +1,33 @@ +package groowt.util.di.filters; + +import java.lang.annotation.*; +import java.util.function.BiPredicate; + +import static groowt.util.di.filters.FilterUtil.isAssignableToAnyOf; + +public final class IterableFilterHandlers { + + private IterableFilterHandlers() {} + + public static IterableFilterHandler of( + Class filterType, + BiPredicate elementPredicate + ) { + return new SimpleIterableFilterHandler<>(filterType, elementPredicate); + } + + @IterableFilter + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface IterableElementTypes { + Class[] value(); + } + + public static IterableFilterHandler getIterableElementTypesFilterHandler() { + return of( + IterableElementTypes.class, + (annotation, element) -> isAssignableToAnyOf(element.getClass(), annotation.value()) + ); + } + +} diff --git a/util/di/src/main/java/groowt/util/di/filters/SimpleFilterHandler.java b/util/di/src/main/java/groowt/util/di/filters/SimpleFilterHandler.java new file mode 100644 index 0000000..338c504 --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/filters/SimpleFilterHandler.java @@ -0,0 +1,33 @@ +package groowt.util.di.filters; + +import java.lang.annotation.Annotation; +import java.util.function.BiPredicate; + +final class SimpleFilterHandler implements FilterHandler { + + private final BiPredicate predicate; + private final Class annotationClass; + private final Class argClass; + + public SimpleFilterHandler(BiPredicate predicate, Class annotationClass, Class argClass) { + this.predicate = predicate; + this.annotationClass = annotationClass; + this.argClass = argClass; + } + + @Override + public boolean check(A annotation, T arg) { + return this.predicate.test(annotation, arg); + } + + @Override + public Class getAnnotationClass() { + return this.annotationClass; + } + + @Override + public Class getArgumentClass() { + return this.argClass; + } + +} diff --git a/util/di/src/main/java/groowt/util/di/filters/SimpleIterableFilterHandler.java b/util/di/src/main/java/groowt/util/di/filters/SimpleIterableFilterHandler.java new file mode 100644 index 0000000..3236cce --- /dev/null +++ b/util/di/src/main/java/groowt/util/di/filters/SimpleIterableFilterHandler.java @@ -0,0 +1,32 @@ +package groowt.util.di.filters; + +import java.lang.annotation.Annotation; +import java.util.Objects; +import java.util.function.BiPredicate; + +final class SimpleIterableFilterHandler implements IterableFilterHandler { + + private final Class annotationClass; + private final BiPredicate elementPredicate; + + public SimpleIterableFilterHandler(Class annotationClass, BiPredicate elementPredicate) { + this.annotationClass = annotationClass; + this.elementPredicate = elementPredicate; + } + + @Override + public boolean check(A annotation, Iterable iterable) { + for (final var e : Objects.requireNonNull(iterable)) { + if (!this.elementPredicate.test(annotation, e)) { + return false; + } + } + return true; + } + + @Override + public Class getAnnotationClass() { + return this.annotationClass; + } + +} diff --git a/util/di/src/test/java/groowt/util/di/DefaultRegistryObjectFactoryTests.java b/util/di/src/test/java/groowt/util/di/DefaultRegistryObjectFactoryTests.java new file mode 100644 index 0000000..aed45cb --- /dev/null +++ b/util/di/src/test/java/groowt/util/di/DefaultRegistryObjectFactoryTests.java @@ -0,0 +1,170 @@ +package groowt.util.di; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.junit.jupiter.api.Test; + +import static groowt.util.di.BindingUtil.*; +import static groowt.util.di.NamedRegistryExtension.named; +import static org.junit.jupiter.api.Assertions.*; + +public class DefaultRegistryObjectFactoryTests { + + public interface Greeter { + String greet(); + } + + public static final class DefaultGreeter implements Greeter { + + @Override + public String greet() { + return "Hello, World!"; + } + + } + + public static final class GivenArgGreeter implements Greeter { + + private final String greeting; + + @Inject + public GivenArgGreeter(String greeting) { + this.greeting = greeting; + } + + @Override + public String greet() { + return this.greeting; + } + + } + + public static final class InjectedArgGreeter implements Greeter { + + private final String greeting; + + @Inject + public InjectedArgGreeter(String greeting) { + this.greeting = greeting; + } + + @Override + public String greet() { + return this.greeting; + } + + } + + public static final class InjectedNamedArgGreeter implements Greeter { + + private final String greeting; + + @Inject + public InjectedNamedArgGreeter(@Named("greeting") String greeting) { + this.greeting = greeting; + } + + @Override + public String greet() { + return this.greeting; + } + + } + + public static final class InjectedNamedSetterGreeter implements Greeter { + + private String greeting; + + @Inject + public void setGreeting(@Named("greeting") String greeting) { + this.greeting = greeting; + } + + @Override + public String greet() { + return this.greeting; + } + + } + + @Test + public void classSmokeScreen() { + final var b = DefaultRegistryObjectFactory.Builder.withDefaults(); + b.configureRegistry(registry -> { + registry.bind(Greeter.class, bc -> bc.to(DefaultGreeter.class)); + }); + final RegistryObjectFactory container = b.build(); + final Greeter greeter = container.get(Greeter.class); + assertEquals("Hello, World!", greeter.greet()); + } + + @Test + public void singletonSmokeScreen() { + final var b = DefaultRegistryObjectFactory.Builder.withDefaults(); + b.configureRegistry(registry -> { + registry.bind(Greeter.class, toSingleton(new DefaultGreeter())); + }); + final RegistryObjectFactory container = b.build(); + final Greeter greeter = container.get(Greeter.class); + assertEquals("Hello, World!", greeter.greet()); + } + + @Test + public void providerSmokeScreen() { + final var b = DefaultRegistryObjectFactory.Builder.withDefaults(); + b.configureRegistry(registry -> { + registry.bind(Greeter.class, toProvider(DefaultGreeter::new)); + }); + final RegistryObjectFactory container = b.build(); + final Greeter greeter = container.get(Greeter.class); + assertEquals("Hello, World!", greeter.greet()); + } + + @Test + public void givenArgSmokeScreen() { + final var b = DefaultRegistryObjectFactory.Builder.withDefaults(); + b.configureRegistry(registry -> { + registry.bind(Greeter.class, bc -> bc.to(GivenArgGreeter.class)); + }); + final RegistryObjectFactory container = b.build(); + final Greeter greeter = container.get(Greeter.class, "Hello, World!"); + assertEquals("Hello, World!", greeter.greet()); + } + + @Test + public void injectedArg() { + final var b = DefaultRegistryObjectFactory.Builder.withDefaults(); + b.configureRegistry(registry -> { + registry.bind(Greeter.class, bc -> bc.to(InjectedArgGreeter.class)); + registry.bind(String.class, toSingleton("Hello, World!")); + }); + final RegistryObjectFactory container = b.build(); + final Greeter greeter = container.get(Greeter.class); + assertEquals("Hello, World!", greeter.greet()); + } + + @Test + public void injectedNamedArg() { + final var b = DefaultRegistryObjectFactory.Builder.withDefaults(); + b.configureRegistry(registry -> { + registry.bind(Greeter.class, bc -> bc.to(InjectedNamedArgGreeter.class)); + registry.bind(named("greeting", String.class), toSingleton("Hello, World!")); + }); + final RegistryObjectFactory container = b.build(); + final Greeter greeter = container.get(Greeter.class); + assertEquals("Hello, World!", greeter.greet()); + } + + @Test + public void injectedSetter() { + final var b = DefaultRegistryObjectFactory.Builder.withDefaults(); + b.configureRegistry(r -> { + r.bind(Greeter.class, toClass(InjectedNamedSetterGreeter.class)); + r.bind(named("greeting", String.class), toSingleton("Hello, World!")); + }); + final RegistryObjectFactory f = b.build(); + final Greeter greeter = f.get(Greeter.class); + assertEquals("Hello, World!", greeter.greet()); + } + +} diff --git a/util/di/src/test/resources/log4j2.xml b/util/di/src/test/resources/log4j2.xml new file mode 100644 index 0000000..beab8e9 --- /dev/null +++ b/util/di/src/test/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/util/extensible/build.gradle b/util/extensible/build.gradle new file mode 100644 index 0000000..d6d4388 --- /dev/null +++ b/util/extensible/build.gradle @@ -0,0 +1,8 @@ +plugins { + id 'GroowtConventions' + id 'java-library' +} + +dependencies { + compileOnlyApi libs.jetbrains.anotations +} diff --git a/util/extensible/src/main/java/groowt/util/extensible/AbstractExtensionContainer.java b/util/extensible/src/main/java/groowt/util/extensible/AbstractExtensionContainer.java new file mode 100644 index 0000000..14ed5a6 --- /dev/null +++ b/util/extensible/src/main/java/groowt/util/extensible/AbstractExtensionContainer.java @@ -0,0 +1,68 @@ +package groowt.util.extensible; + +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.function.Consumer; + +public abstract class AbstractExtensionContainer implements ExtensionContainer { + + private final F extensionFactory; + private final Collection extensions = new ArrayList<>(); + + public AbstractExtensionContainer(F extensionFactory) { + this.extensionFactory = extensionFactory; + } + + /** + * @return A copy of the registered extensions. + */ + protected Collection getRegisteredExtensions() { + return new ArrayList<>(this.extensions); + } + + protected void registerExtension(E extension) { + this.extensions.add(extension); + } + + @Override + public @Nullable T findExtension(Class extensionClass) { + return this.extensions.stream() + .filter(extensionClass::isInstance) + .findFirst() + .map(extensionClass::cast) + .orElse(null); + } + + /** + * @implNote While this {@link AbstractExtensionContainer} calls + * {@link #getExtension}, which will throw if there is no registered + * extension, this method may be overridden to not use {@link #getExtension} + * and instead implement custom handling logic to avoid throwing, etc. + */ + @Override + public void configureExtension(Class extensionClass, Consumer configure) { + configure.accept(this.getExtension(extensionClass)); + } + + @Override + public T getExtension(Class extensionClass) { + return this.extensions.stream() + .filter(extensionClass::isInstance) + .findFirst() + .map(extensionClass::cast) + .orElseThrow(() -> new IllegalArgumentException("There is no registered extension for " + extensionClass.getName())); + } + + @Override + public boolean hasExtension(Class extensionClass) { + return this.extensions.stream().anyMatch(extensionClass::isInstance); + } + + @Override + public F getExtensionFactory() { + return this.extensionFactory; + } + +} diff --git a/util/extensible/src/main/java/groowt/util/extensible/Extensible.java b/util/extensible/src/main/java/groowt/util/extensible/Extensible.java new file mode 100644 index 0000000..53eea32 --- /dev/null +++ b/util/extensible/src/main/java/groowt/util/extensible/Extensible.java @@ -0,0 +1,11 @@ +package groowt.util.extensible; + +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +// TODO: groovy methods to handle getting extensions via property accessors +public interface Extensible> extends ExtensionAware { + T createExtension(Class extensionClass, Object... constructorArgs); + C getExtensionContainer(); +} diff --git a/util/extensible/src/main/java/groowt/util/extensible/ExtensionAware.java b/util/extensible/src/main/java/groowt/util/extensible/ExtensionAware.java new file mode 100644 index 0000000..3a41106 --- /dev/null +++ b/util/extensible/src/main/java/groowt/util/extensible/ExtensionAware.java @@ -0,0 +1,12 @@ +package groowt.util.extensible; + +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +public interface ExtensionAware { + @Nullable T findExtension(Class extensionClass); + void configureExtension(Class extensionClass, Consumer configure); + T getExtension(Class extensionClass); + boolean hasExtension(Class extensionClass); +} diff --git a/util/extensible/src/main/java/groowt/util/extensible/ExtensionContainer.java b/util/extensible/src/main/java/groowt/util/extensible/ExtensionContainer.java new file mode 100644 index 0000000..1d85527 --- /dev/null +++ b/util/extensible/src/main/java/groowt/util/extensible/ExtensionContainer.java @@ -0,0 +1,5 @@ +package groowt.util.extensible; + +public interface ExtensionContainer extends ExtensionAware { + F getExtensionFactory(); +} diff --git a/view-components/build.gradle b/view-components/build.gradle new file mode 100644 index 0000000..454bc74 --- /dev/null +++ b/view-components/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'java' + id 'java-library' + id 'groovy' +} + +repositories { + mavenCentral() +} + +dependencies { + api libs.groovy, libs.groovy.templates, libs.jetbrains.anotations, project(':views') + implementation libs.slf4j.api + + testImplementation libs.junit.jupiter.api + testRuntimeOnly libs.log4j.slf4jBinding, libs.log4j.core +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +test { + testLogging.showStandardStreams = true +} + +testing { + suites { + test { + useJUnitJupiter() + } + } +} diff --git a/view-components/spec.md b/view-components/spec.md new file mode 100644 index 0000000..dae7fcd --- /dev/null +++ b/view-components/spec.md @@ -0,0 +1,349 @@ +# View Components Spec + +The View Components work roughly as follows. + +First, one defines a component by at a minimum implementing the `ViewComponent` interface. Additionally, for ease of use, one can extend one of the built-in helpers, such as `AbstractViewComponent` or `GStringTemplateViewComponent`. For example, here might be a web-style component: +```groovy +class MyComponent extends GStringTemplateViewComponent { + String greeting + + MyComponent(Map attr) { + super(new File('someTemplate.gst')) // TODO: figure out what args this actually takes + greeting = atr.greeting + } +} +``` + +Its associated template could be as simple as: +```text +Hello. Here is a friendly greeting: ${greeting}. +``` + +However, it could be something more complex such as a JSON document: +```json +{ + "greeting": "<%= greeting %>" +} +``` + +Extending this last example, say our `MyComponent` class had this method: + +```groovy +def getGreetingInQuotes() { + '"' + greeting + '"' +} +``` + +Then we could simply do in our JSON document: +``` +{ + "greeting": $greetingInQuotes // or <%= greetingInQuotes %>, whichever you prefer +} +``` + +And so on. + +Now we have to find a way to invoke our component. We could of course do so programmatically, like: + +```groovy +def myComponent = new MyComponent(greeting: 'hello from everywhere!') +def rendered = myComponent.render() +assert rendered == 'Hello. Here is a friendly greeting: hello from everywhere!' +``` + +However, this is basically the same as using `GStringTemplateView` (from the `Views` module). For the real power of the View Components, let's see how we can call them from other templates and components. + +## Using Web Components + +Continuing with the `MyComponent` from above, let's create our basic template which will include a rendering of the component. It's as simple as this, say in a file called `myComponentPage.gst`: + +```text + +``` + +To render this template, we need to do the following: + +```groovy +import MyComponent + +def pageViewComponent = new PageViewComponent(new File('myComponentPage.gst')).configure { + context { + registry { + rootScope { + addWithMapArg(MyComponent) + } + } + } +} +def rendered = pageViewComponent.render() +assert rendered == 'Hello. Here is a friendly greeting: Hello, World!' +``` + +Internally, the `PageComponent` will compile its template to something like the following Groovy script: + +```groovy +import static com.jessebrault.groowt.view.component.runtime.Helpers.* + +def getScript() { + return { Writer __writer0 -> + resolveOrThrow(context, 'MyComponent')([greeting: 'Hello, World!']).renderTo(__writer0) + } +} +``` + +The script above has the single method `getScript()` which returns a single closure. The `PageComponent` class will call this method, receive the closure, set itself as the closure's delegate, and call it with a `Writer`. + +## More Advanced Web Component Example + +Here is quite a broad sketch of how a set of HTML form components might work. + +ViewComponent Groovy classes: + +```groovy +// This trait should be included in the HTML lib +trait HTMLComponent { + String orElseEmpty(boolean cond, Closure lazyOnTrue, Closure format) { + cond ? format(lazyOnTrue()) : '' + } + + String orElseEmpty(boolean cond, String onTrue) { + cond ? onTrue : '' + } + + String orElseEmpty(boolean cond, Closure onTrue) { + cond ? onTrue() : '' + } + + String inQuotes(Object value) { + /"$value"/ + } + + String attr(String name, Object value) { + /$name="$value"/ + } + + String attr(Closure cl) { + new RenderAttrClosure(this, cl)() // RenderAttrClosure would be in the lib + } + + String joinAttr(Map attr) { + orElseEmpty(!attr.isEmpty) { + attr.collect(this.&attr).join(' ') + } + } + + String tag(String tagName, Map attr, Object inner) { + "<$tagName ${joinAttr(attr)}>$inner" + } +} + +// Our enhanced form class +class FormWithModel extends GStringTemplateViewComponent implements HTMLComponent { + final String id + final Object model + final String action + final boolean wrap + final Map customAttr + + private final Closure children + + FormWithModel(Map attr, Closure children) { + super(new File('formWithModel.gst')) + this.id = id + this.model = attr.model + this.action = attr.action + this.wrap = attr.wrap != null ? attr.wrap : false + this.children = children + } + + @Override + protected Closure getChildren() { + this.children + } + + @Override + protected ComponentRegistry.Scope getChildrenScope() { + new SimpleScope().tap { + addWithContextAndMapArgs(Input) + } + } + + boolean hasModelProperty(String name) { + this.model.metaClass.properties.find { it.name == name } != null + } + + Object getModelProperty(String name) { + this.model.getProperty(name) + } +} + +class Input extends GStringTemplateViewComponent implements HTMLComponent { + + private static String getDefaultType(Object model, String name) { + def value = model.getProperty(name) + return switch (value) { + case String -> 'text' + default -> throw new UnsupportedOperationException('String model properties not supported yet') + } + } + + final String name + final String type + final boolean wrap + final boolean putLabel + final String label + + private final Map customAttr + + Input(ComponentContext context, Map attr) { + super(new File('input.gst')) + this.context = context + this.name = requireNonNull(attr.name) + this.type = attr.type ? attr.type : getDefaultType(this.getForm().model, this.name) + this.wrap = attr.wrap != null ? attr.wrap : this.getForm().wrap + this.putLabel = attr.putLabel != null ? attr.putLabel : true + this.label = attr.label != null ? attr.label : '' + this.customAttr = attr.findAll { keyHolder, value -> value != null && !(keyHolder in ['name', 'wrap']) } + } + + private FormWithModel getForm() { + def form = this.context.findNearestAncestorByClass(FormWithModel) + if (form == null) { + throw new ComponentException("An Input can only be used inside a FormWithModel") + } else { + return form + } + } + + boolean hasValue() { + this.form.hasModelProperty(this.name) + } + + Object getValue() { + this.form.getModelProperty(this.name) + } +} + +// Should be in the HTML lib +class SurroundIf extends GStringTemplateViewComponent implements HTMLComponent { + + static Closure tag(String name, Map attr) { + return { SurroundIf self -> + Tuple.of("<$name ${self.joinAttr(attr)}>", "") + } + } + + private static final String template = ''' + + <%= tag[0] %> + <%= renderChildren() %> + <%= tag[1] %> + +''' + + final boolean condition + final Tuple tag + + private final Closure children + + SurroundIf(Map attr, Closure children) { + super(template.trim()) + this.condition = attr.condition + switch (attr.tag) { + case null -> { this.tag = Tuple.of('', '') } + case Closure -> { this.tag = attr.tag.call(this) } + case String -> { this.tag = Tuple.of("<$attr.tag>", "") } + default -> { throw new IllegalArgumentException() } + } + this.children = children + } + + @Override + protected Closure getChildren() { + this.children + } +} +``` + +And our template files: + +```text +// formWithModel.gst +
+ ${ renderChildren() } +
+``` + +```text +// input.gst + +${ renderChildren() }}> + $label} /> + + + + + + + + + + +``` + +Our basic model: + +```groovy +class MessageModel { + String from + String to + String message +} +``` + +Now here is our target page: + +```text +// target.gst + + + + + +``` + +Now let's render it: + +```groovy +def message = new MessageModel(from: 'Jesse', to: 'Jeanna', message: 'Hello, World!') +def root = new HTMLRootViewComponent(new File('target.gst')).configure { + context { + registry { + rootScope { + addWithMapAndChildrenArgs(SurroundIf, FormWithModel) + } + } + } +} +root.message = message +def rendered = root.render() +assert rendered == ''' +
+
+ + +
+
+ +
+ + +
+'''.trim() // may not be slightly correct with indentation, but close enough +``` + + + + diff --git a/view-components/src/main/java/groowt/view/component/AbstractComponentFactory.java b/view-components/src/main/java/groowt/view/component/AbstractComponentFactory.java new file mode 100644 index 0000000..2733d4b --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/AbstractComponentFactory.java @@ -0,0 +1,81 @@ +package groowt.view.component; + +import groovy.lang.GroovyObjectSupport; +import groovy.lang.MetaMethod; +import groovy.lang.MissingMethodException; + +import java.util.*; + +public abstract class AbstractComponentFactory extends GroovyObjectSupport + implements ComponentFactory { + + private static final String DO_CREATE = "doCreate"; + private static final Class[] EMPTY_CLASSES = new Class[0]; + + private static Object[] flatten(Object... args) { + if (args.length == 0) { + return args; + } else { + final List result = new ArrayList<>(args.length); + for (final var arg : args) { + if (arg instanceof Object[] arr) { + result.addAll(Arrays.asList(arr)); + } else { + result.add(arg); + } + } + return result.toArray(Object[]::new); + } + } + + private static Class[] asTypes(Object[] args) { + if (args.length == 0) { + return EMPTY_CLASSES; + } + final Class[] result = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + result[i] = args[i].getClass(); + } + return result; + } + + private final Map[], MetaMethod> cache = new HashMap<>(); + + private MetaMethod findDoCreateMethod(Object[] allArgs) { + return this.cache.computeIfAbsent(asTypes(allArgs), types -> + this.getMetaClass().getMetaMethod(DO_CREATE, types) + ); + } + + @SuppressWarnings("unchecked") + private T findAndDoCreate(ComponentContext componentContext, Object[] args) { + final Object[] contextsAndArgs = flatten(componentContext, args); + final MetaMethod contextsAndArgsMethod = this.findDoCreateMethod(contextsAndArgs); + if (contextsAndArgsMethod != null) { + return (T) contextsAndArgsMethod.invoke(this, contextsAndArgs); + } + + final Object[] contextOnly = new Object[] { componentContext }; + final MetaMethod contextOnlyMethod = this.findDoCreateMethod(contextOnly); + if (contextOnlyMethod != null) { + return (T) contextOnlyMethod.invoke(this, contextOnly); + } + + final MetaMethod argsOnlyMethod = this.findDoCreateMethod(args); + if (argsOnlyMethod != null) { + return (T) argsOnlyMethod.invoke(this, args); + } + + throw new MissingMethodException( + DO_CREATE, + this.getClass(), + args + ); + } + + @Override + public T create(ComponentContext componentContext, Object... args) { + return this.findAndDoCreate(componentContext, args); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/AbstractViewComponent.java b/view-components/src/main/java/groowt/view/component/AbstractViewComponent.java new file mode 100644 index 0000000..6f32170 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/AbstractViewComponent.java @@ -0,0 +1,51 @@ +package groowt.view.component; + +import groovy.lang.Closure; + +import java.io.IOException; +import java.io.Writer; +import java.util.Objects; + +public abstract class AbstractViewComponent implements ViewComponent { + + private ComponentTemplate template; + + public AbstractViewComponent() {} + + public AbstractViewComponent(ComponentTemplate template) { + this.template = Objects.requireNonNull(template); + } + + public AbstractViewComponent(Class templateClass) { + try { + this.template = templateClass.getConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + protected ComponentTemplate getTemplate() { + return Objects.requireNonNull(template); + } + + protected void setTemplate(ComponentTemplate template) { + this.template = Objects.requireNonNull(template); + } + + protected void beforeRender() {} + + protected void afterRender() { + this.getContext().afterComponent(this); + } + + @Override + public void renderTo(Writer out) throws IOException { + final Closure closure = this.template.getRenderer(); + closure.setDelegate(this); + closure.setResolveStrategy(Closure.DELEGATE_FIRST); + this.beforeRender(); + closure.call(this.getContext(), out); + this.afterRender(); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/CachingComponentTemplateCompiler.java b/view-components/src/main/java/groowt/view/component/CachingComponentTemplateCompiler.java new file mode 100644 index 0000000..9c03d55 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/CachingComponentTemplateCompiler.java @@ -0,0 +1,27 @@ +package groowt.view.component; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +public abstract class CachingComponentTemplateCompiler implements ComponentTemplateCompiler { + + private final Map, ComponentTemplate> cache = new HashMap<>(); + + protected final void putInCache(Class forClass, ComponentTemplate template) { + this.cache.put(forClass, template); + } + + protected final ComponentTemplate getFromCache(Class forClass) { + return Objects.requireNonNull(this.cache.get(forClass)); + } + + protected final ComponentTemplate getFromCacheOrElse( + Class forClass, + Supplier onEmpty + ) { + return this.cache.computeIfAbsent(forClass, ignored -> onEmpty.get()); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/ComponentContext.java b/view-components/src/main/java/groowt/view/component/ComponentContext.java new file mode 100644 index 0000000..d997eba --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/ComponentContext.java @@ -0,0 +1,52 @@ +package groowt.view.component; + +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 { + + @ApiStatus.Internal + ComponentFactory resolve(String component); + + @ApiStatus.Internal + ViewComponent create(ComponentFactory factory, Object... args); + + @ApiStatus.Internal + void afterComponent(ViewComponent component); + + Deque getScopeStack(); + + void pushScope(ComponentScope scope); + void pushDefaultScope(); + void popScope(); + + default ComponentScope getCurrentScope() { + return Objects.requireNonNull(this.getScopeStack().peek(), "There is no current scope."); + } + + Deque getComponentStack(); + + @Nullable ViewComponent getParent(); + @Nullable T getParent(Class parentClass); + + @Nullable ViewComponent findNearestAncestor(Predicate matching); + + default @Nullable T findNearestAncestor( + 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)); + } + + List getAllAncestors(); + +} diff --git a/view-components/src/main/java/groowt/view/component/ComponentCreateException.java b/view-components/src/main/java/groowt/view/component/ComponentCreateException.java new file mode 100644 index 0000000..3e0db81 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/ComponentCreateException.java @@ -0,0 +1,22 @@ +package groowt.view.component; + +public class ComponentCreateException extends RuntimeException { + + private final ComponentTemplate template; + private final int line; + private final int column; + + public ComponentCreateException(ComponentTemplate template, int line, int column, Throwable cause) { + super(cause); + this.template = template; + this.line = line; + this.column = column; + } + + @Override + public String getMessage() { + return "Exception while rendering " + this.template.getClass() + + " at line " + this.line + ", column " + this.column + "."; + } + +} diff --git a/view-components/src/main/java/groowt/view/component/ComponentFactory.java b/view-components/src/main/java/groowt/view/component/ComponentFactory.java new file mode 100644 index 0000000..eecf080 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/ComponentFactory.java @@ -0,0 +1,20 @@ +package groowt.view.component; + +import groovy.lang.Closure; +import groovy.lang.GroovyObject; + +import java.util.function.Supplier; + +public interface ComponentFactory extends GroovyObject { + + static ComponentFactory of(Closure closure) { + return new DelegatingComponentFactory<>((context, args) -> closure.call(context, args)); + } + + static ComponentFactory of(Supplier supplier) { + return new DelegatingComponentFactory<>((ignored0, ignored1) -> supplier.get()); + } + + T create(ComponentContext componentContext, Object... args); + +} diff --git a/view-components/src/main/java/groowt/view/component/ComponentRenderException.java b/view-components/src/main/java/groowt/view/component/ComponentRenderException.java new file mode 100644 index 0000000..3810ab7 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/ComponentRenderException.java @@ -0,0 +1,43 @@ +package groowt.view.component; + +public class ComponentRenderException extends RuntimeException { + + private static String formatMessage(int line, int column) { + return "Exception while rendering at line " + line + ", column " + column + "."; + } + + private static String formatMessage(ViewComponent viewComponent) { + return "Exception while rendering " + viewComponent; + } + + private static String formatMessage(ViewComponent viewComponent, int line, int column) { + return "Exception while rendering " + viewComponent + " at line " + line + ", column " + column + "."; + } + + public ComponentRenderException() {} + + public ComponentRenderException(String message) { + super(message); + } + + public ComponentRenderException(String message, Throwable cause) { + super(message, cause); + } + + public ComponentRenderException(Throwable cause) { + super(cause); + } + + public ComponentRenderException(int line, int column, Throwable cause) { + super(formatMessage(line, column), cause); + } + + public ComponentRenderException(ViewComponent component, Throwable cause) { + super(formatMessage(component), cause); + } + + public ComponentRenderException(ViewComponent component, int line, int column, Throwable cause) { + super(formatMessage(component, line, column), cause); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/ComponentScope.java b/view-components/src/main/java/groowt/view/component/ComponentScope.java new file mode 100644 index 0000000..e6b3428 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/ComponentScope.java @@ -0,0 +1,41 @@ +package groowt.view.component; + +public interface ComponentScope { + + void add(String name, ComponentFactory factory); + boolean contains(String name); + void remove(String name); + ComponentFactory get(String name); + + default ComponentFactory factoryMissing(String typeName) { + throw new NoFactoryMissingException(this.getClass().getName() + " does not support factoryMissing()"); + } + + default void add(Class clazz, ComponentFactory factory) { + this.add(clazz.getName(), factory); + } + + default boolean contains(Class clazz) { + return this.contains(clazz.getName()); + } + + @SuppressWarnings("unchecked") + default ComponentFactory get(Class clazz) { + return (ComponentFactory) this.get(clazz.getName()); + } + + @SuppressWarnings("unchecked") + default ComponentFactory getAs(String name, Class viewComponentType) { + return (ComponentFactory) this.get(name); + } + + default void remove(Class clazz) { + this.remove(clazz.getName()); + } + + @SuppressWarnings("unchecked") + default ComponentFactory factoryMissing(Class clazz) { + return (ComponentFactory) this.factoryMissing(clazz.getName()); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/ComponentTemplate.java b/view-components/src/main/java/groowt/view/component/ComponentTemplate.java new file mode 100644 index 0000000..d572d75 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/ComponentTemplate.java @@ -0,0 +1,7 @@ +package groowt.view.component; + +import groovy.lang.Closure; + +public interface ComponentTemplate { + Closure getRenderer(); +} diff --git a/view-components/src/main/java/groowt/view/component/ComponentTemplateCompiler.java b/view-components/src/main/java/groowt/view/component/ComponentTemplateCompiler.java new file mode 100644 index 0000000..3f1ee74 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/ComponentTemplateCompiler.java @@ -0,0 +1,16 @@ +package groowt.view.component; + +import java.io.File; +import java.io.InputStream; +import java.io.Reader; +import java.net.URI; +import java.net.URL; + +public interface ComponentTemplateCompiler { + ComponentTemplate compile(Class forClass, File templateFile); + ComponentTemplate compile(Class forClass, String template); + ComponentTemplate compile(Class forClass, URI templateURI); + ComponentTemplate compile(Class forClass, URL templateURL); + ComponentTemplate compile(Class forClass, InputStream inputStream); + ComponentTemplate compile(Class forClass, Reader reader); +} diff --git a/view-components/src/main/java/groowt/view/component/ComponentTemplateCreateException.java b/view-components/src/main/java/groowt/view/component/ComponentTemplateCreateException.java new file mode 100644 index 0000000..4226662 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/ComponentTemplateCreateException.java @@ -0,0 +1,49 @@ +package groowt.view.component; + +import java.io.Reader; + +public class ComponentTemplateCreateException extends RuntimeException { + + private final Class forClass; + private final Object templateSource; + + public ComponentTemplateCreateException( + String message, + Class forClass, + Object templateSource + ) { + super(message); + this.forClass = forClass; + this.templateSource = templateSource; + } + + public ComponentTemplateCreateException( + String message, + Throwable cause, + Class forClass, + Object templateSource + ) { + super(message, cause); + this.forClass = forClass; + this.templateSource = templateSource; + } + + public ComponentTemplateCreateException( + Throwable cause, + Class forClass, + Object templateSource + ) { + super(cause); + 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/DefaultComponentContext.java b/view-components/src/main/java/groowt/view/component/DefaultComponentContext.java new file mode 100644 index 0000000..7b9401d --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/DefaultComponentContext.java @@ -0,0 +1,144 @@ +package groowt.view.component; + +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Predicate; + +public class DefaultComponentContext implements ComponentContext { + + private final Deque scopeStack = new LinkedList<>(); + private final Deque componentStack = new LinkedList<>(); + + @Override + public ComponentFactory 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 scope.get(component); + } + } + + final var missingStack = new LinkedList<>(this.scopeStack); + NoFactoryMissingException first = null; + while (!missingStack.isEmpty()) { + final ComponentScope scope = getStack.pop(); + try { + return 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(ComponentFactory factory, Object... args) { + final ViewComponent component = factory.create(this, args); + this.componentStack.push(component); + return component; + } + + @Override + public void afterComponent(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() { + return new LinkedList<>(this.scopeStack); + } + + @Override + public void pushScope(ComponentScope scope) { + this.scopeStack.push(scope); + } + + protected ComponentScope getNewDefaultScope() { + return new DefaultComponentScope(); + } + + @Override + public void pushDefaultScope() { + this.pushScope(this.getNewDefaultScope()); + } + + @Override + public void popScope() { + this.scopeStack.pop(); + } + + @Override + public Deque getComponentStack() { + return new LinkedList<>(this.componentStack); + } + + @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; + } + return null; + } + + @Override + public @Nullable T getParent(Class parentClass) { + return parentClass.cast(this.getParent()); + } + + @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); + if (matching.test(ancestor)) { + result = 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(); + } + } + +} diff --git a/view-components/src/main/java/groowt/view/component/DefaultComponentScope.java b/view-components/src/main/java/groowt/view/component/DefaultComponentScope.java new file mode 100644 index 0000000..36eea68 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/DefaultComponentScope.java @@ -0,0 +1,30 @@ +package groowt.view.component; + +import java.util.HashMap; +import java.util.Map; + +public class DefaultComponentScope implements ComponentScope { + + private final Map> factories = new HashMap<>(); + + @Override + public void add(String name, ComponentFactory factory) { + this.factories.put(name, factory); + } + + @Override + public boolean contains(String name) { + return this.factories.containsKey(name); + } + + @Override + public void remove(String name) { + this.factories.remove(name); + } + + @Override + public ComponentFactory get(String name) { + return this.factories.get(name); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/DelegatingComponentFactory.java b/view-components/src/main/java/groowt/view/component/DelegatingComponentFactory.java new file mode 100644 index 0000000..b0633a4 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/DelegatingComponentFactory.java @@ -0,0 +1,20 @@ +package groowt.view.component; + +final class DelegatingComponentFactory extends AbstractComponentFactory { + + @FunctionalInterface + interface ComponentFactoryDelegate { + T doCreate(ComponentContext context, Object... args); + } + + private final ComponentFactoryDelegate function; + + public DelegatingComponentFactory(ComponentFactoryDelegate function) { + this.function = function; + } + + public T doCreate(ComponentContext componentContext, Object... args) { + return this.function.doCreate(componentContext, args); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/MissingClassTypeException.java b/view-components/src/main/java/groowt/view/component/MissingClassTypeException.java new file mode 100644 index 0000000..9204aa8 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/MissingClassTypeException.java @@ -0,0 +1,17 @@ +package groowt.view.component; + +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/MissingComponentException.java b/view-components/src/main/java/groowt/view/component/MissingComponentException.java new file mode 100644 index 0000000..527567a --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/MissingComponentException.java @@ -0,0 +1,23 @@ +package groowt.view.component; + +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 "Missing " + this.getMissingKeyName() + " on line " + this.line + ", column " + this.col + "."; + } + +} diff --git a/view-components/src/main/java/groowt/view/component/MissingFragmentTypeException.java b/view-components/src/main/java/groowt/view/component/MissingFragmentTypeException.java new file mode 100644 index 0000000..afbdb15 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/MissingFragmentTypeException.java @@ -0,0 +1,14 @@ +package groowt.view.component; + +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/MissingStringTypeException.java b/view-components/src/main/java/groowt/view/component/MissingStringTypeException.java new file mode 100644 index 0000000..b729804 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/MissingStringTypeException.java @@ -0,0 +1,17 @@ +package groowt.view.component; + +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/NoFactoryMissingException.java b/view-components/src/main/java/groowt/view/component/NoFactoryMissingException.java new file mode 100644 index 0000000..8fcf1cc --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/NoFactoryMissingException.java @@ -0,0 +1,19 @@ +package groowt.view.component; + +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/ViewComponent.java b/view-components/src/main/java/groowt/view/component/ViewComponent.java new file mode 100644 index 0000000..0ed85a7 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/ViewComponent.java @@ -0,0 +1,14 @@ +package groowt.view.component; + +import groowt.view.View; + +public interface ViewComponent extends View { + + default String getTypeName() { + return this.getClass().getName(); + } + + void setContext(ComponentContext context); + ComponentContext getContext(); + +} diff --git a/views/build.gradle b/views/build.gradle new file mode 100644 index 0000000..a472e26 --- /dev/null +++ b/views/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'java' + id 'java-library' + id 'groovy' +} + +repositories { + mavenCentral() +} + +dependencies { + api libs.groovy, libs.groovy.templates + implementation libs.slf4j.api + + testImplementation libs.junit.jupiter.api + testRuntimeOnly libs.log4j.slf4jBinding, libs.log4j.core +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +test { + testLogging.showStandardStreams = true +} + +testing { + suites { + test { + useJUnitJupiter() + } + } +} diff --git a/views/src/main/groovy/groowt/view/AbstractView.java b/views/src/main/groovy/groowt/view/AbstractView.java new file mode 100644 index 0000000..d95621e --- /dev/null +++ b/views/src/main/groovy/groowt/view/AbstractView.java @@ -0,0 +1,34 @@ +package groowt.view; + +import groovy.lang.Closure; +import groovy.lang.GroovyObjectSupport; +import groovy.lang.MetaProperty; +import groovy.lang.Writable; +import org.codehaus.groovy.runtime.typehandling.GroovyCastException; + +import java.util.Map; +import java.util.stream.Collectors; + +/** + * TODO: create a Map implementation that can access the View for keys/values, or do a metaclass thing + * TODO: get rid of this and just move the asType stuff to GStringTemplateView + */ +public abstract class AbstractView extends GroovyObjectSupport implements View { + + @SuppressWarnings("unchecked") + public T asType(Class clazz) { + if (clazz.isAssignableFrom(this.getClass())) { + return (T) this; + } else if (clazz.equals(Writable.class)) { + return (T) this.asWritable(); + } else if (clazz.equals(Closure.class)) { + return (T) this.asClosure(); + } else if (clazz.equals(Map.class)) { + return (T) this.getMetaClass().getProperties().stream() + .collect(Collectors.toMap(MetaProperty::getName, metaProperty -> metaProperty.getProperty(this))); + } else { + throw new GroovyCastException(this, clazz); + } + } + +} diff --git a/views/src/main/groovy/groowt/view/GStringTemplateView.java b/views/src/main/groovy/groowt/view/GStringTemplateView.java new file mode 100644 index 0000000..0b39bd5 --- /dev/null +++ b/views/src/main/groovy/groowt/view/GStringTemplateView.java @@ -0,0 +1,196 @@ +package groowt.view; + +import groovy.lang.Closure; +import groovy.lang.Writable; +import groovy.text.GStringTemplateEngine; +import groovy.text.Template; + +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +/** + * Delegates to self. + */ +public class GStringTemplateView extends AbstractView { + + private final GStringTemplateEngine engine; + private final Object src; + private final Charset charset; + private final Template template; + + private final ThreadLocal currentWriter = new ThreadLocal<>(); + private final ThreadLocal> yieldClosure = new ThreadLocal<>(); + + public GStringTemplateView(Map args) { + this.engine = (GStringTemplateEngine) args.getOrDefault("engine", new GStringTemplateEngine()); + if (!args.containsKey("src")) { + throw new IllegalArgumentException("args.src must not be null"); + } + this.src = args.get("src"); + this.charset = switch (args.get("charset")) { + case null -> null; + case Charset c -> c; + default -> throw new IllegalArgumentException("args.charset, if not null, must be an instance of Charset"); + }; + try { + switch (this.src) { + case String s -> this.template = this.engine.createTemplate(s); + case File f -> { + if (this.charset != null) { + this.template = this.engine.createTemplate(f, this.charset); + } else { + this.template = this.engine.createTemplate(f); + } + } + case Reader r -> this.template = this.engine.createTemplate(r); + case URL url -> { + if (this.charset != null) { + this.template = this.engine.createTemplate(url, this.charset); + } else { + this.template = this.engine.createTemplate(url); + } + } + default -> throw new IllegalArgumentException("args.src must be a String, File, Reader, or URL."); + } + } catch (IOException | ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + @Override + public void renderTo(Writer writer) throws IOException { + this.currentWriter.set(writer); + final Closure closure = (Closure) this.template.make(); + closure.setDelegate(this); + closure.setResolveStrategy(Closure.DELEGATE_FIRST); + final Writable writable = (Writable) closure; + writable.writeTo(writer); + this.currentWriter.remove(); + } + + public String render(Closure yieldClosure) { + this.yieldClosure.set(yieldClosure); + final String result = this.render(); + this.yieldClosure.remove(); + return result; + } + + public void renderTo(Writer writer, Closure yieldClosure) throws IOException { + this.yieldClosure.set(yieldClosure); + this.renderTo(writer); + this.yieldClosure.remove(); + } + + public CharSequence yield(Object... args) { + final Closure yieldClosure = this.yieldClosure.get(); + if (yieldClosure == null) { + throw new IllegalStateException("Cannot yield in a GStringTemplateView without passing a Closure to render() or renderTo()."); + } + final Class[] paramTypes = yieldClosure.getParameterTypes(); + + // if Writer is first param, pass that + if (paramTypes.length > 0) { + final Class firstParamType = paramTypes[0]; + if (Writer.class.isAssignableFrom(firstParamType)) { + yieldClosure.call(this.currentWriter.get(), args); + return ""; + } + } + + // else just give whatever was passed to yield + final Object returned = yieldClosure.call(args); + if (returned instanceof CharSequence cs) { + return cs; + } else { + throw new RuntimeException("The yield Closure must return an instance of CharSequence or a subtype thereof; given: " + returned + " of type " + returned.getClass()); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public String partial(Object... args) { + if (args.length == 0) { + throw new IllegalArgumentException("Must provide at least 1 argument. Signature: partial(String | File | Reader | URL | Map, Map | Closure | [Map, Closure])"); + } + + final Object arg0 = args[0]; + final Object arg1 = args.length >= 2 ? args[1] : null; + final Object arg2 = args.length >= 3 ? args[2] : null; + + final Map createArgs = new HashMap<>(); + + switch (arg0) { + case String s -> createArgs.put("src", new File(s)); + case File f -> createArgs.put("src", f); + case Reader r -> createArgs.put("src", r); + case URL url -> createArgs.put("src", url); + case Map m -> createArgs.putAll(m); + default -> throw new IllegalArgumentException("First argument must be any of String, File, Reader, URL, or Map."); + } + + if (!createArgs.containsKey("engine")) { + createArgs.put("engine", this.engine); + } + if (!createArgs.containsKey("charset") && this.charset != null) { + createArgs.put("charset", this.charset); + } + if (!createArgs.containsKey("parent")) { + createArgs.put("parent", this); + } + + switch (arg1) { + case null -> { + final View partial = new StandardGStringTemplateView(createArgs); + try { + partial.renderTo(this.currentWriter.get()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + case Map m -> { + createArgs.put("locals", m); + final GStringTemplateView view = new StandardGStringTemplateView(createArgs); + final Closure cl = switch (arg2) { + case null -> null; + case Closure closure -> closure; + default -> throw new IllegalArgumentException("Third argument, if not null, must be a Closure."); + }; + try { + if (cl != null) { + view.renderTo(this.currentWriter.get(), cl); + } else { + view.renderTo(this.currentWriter.get()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + case Closure cl -> { + final GStringTemplateView view = new StandardGStringTemplateView(createArgs); + try { + view.renderTo(this.currentWriter.get(), cl); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + default -> throw new IllegalArgumentException("Second argument, if not null, must be a Map or a Closure."); + } + + return ""; + } + + public URL templateResource(String name) { + return this.getClass().getResource(name); + } + + @Override + public String toString() { + return String.format("GStringTemplateView(src: %s)", this.src); + } + +} diff --git a/views/src/main/groovy/groowt/view/StandardGStringTemplateView.java b/views/src/main/groovy/groowt/view/StandardGStringTemplateView.java new file mode 100644 index 0000000..7d7ee68 --- /dev/null +++ b/views/src/main/groovy/groowt/view/StandardGStringTemplateView.java @@ -0,0 +1,46 @@ +package groowt.view; + +import groovy.lang.GroovySystem; +import groovy.lang.MetaClass; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; + +import java.util.Collections; +import java.util.Map; + +public class StandardGStringTemplateView extends GStringTemplateView { + + static { + final MetaClass mc = new StandardGStringTemplateViewMetaClass(); + mc.initialize(); + GroovySystem.getMetaClassRegistry().setMetaClass(StandardGStringTemplateView.class, mc); + } + + private final Map locals; + private final View parent; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public StandardGStringTemplateView(Map args) { + super(args); + final Object localsArg = args.get("locals"); + this.locals = localsArg instanceof Map map + ? Collections.unmodifiableMap(map) + : localsArg != null + ? Collections.unmodifiableMap(DefaultGroovyMethods.asType(localsArg, Map.class)) + : Collections.emptyMap(); + this.parent = args.containsKey("parent") ? DefaultGroovyMethods.asType(args.get("parent"), View.class) : null; + } + + public Map getLocals() { + return locals; + } + + public View getParent() { + return parent; + } + + @Override + public String toString() { + return "StandardGStringTemplateView(super: " + super.toString() + ")"; + } + +} diff --git a/views/src/main/groovy/groowt/view/StandardGStringTemplateViewMetaClass.java b/views/src/main/groovy/groowt/view/StandardGStringTemplateViewMetaClass.java new file mode 100644 index 0000000..fedf35e --- /dev/null +++ b/views/src/main/groovy/groowt/view/StandardGStringTemplateViewMetaClass.java @@ -0,0 +1,178 @@ +package groowt.view; + +import groovy.lang.*; +import org.codehaus.groovy.runtime.metaclass.MethodSelectionException; +import org.codehaus.groovy.util.FastArray; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class StandardGStringTemplateViewMetaClass extends ExpandoMetaClass { + + private static final Logger logger = LoggerFactory.getLogger(StandardGStringTemplateViewMetaClass.class); + + private static StandardGStringTemplateView asView(Object object) { + return object instanceof StandardGStringTemplateView view ? view : null; + } + + private static Object[] asArgsArray(Object object) { + return object instanceof Object[] objects ? objects : new Object[] { object }; + } + + private static MetaMethod findMetaMethod(MetaClass metaClass, String methodName, Object[] argsArray) { + final List metaMethods = metaClass.getMetaMethods().stream().filter(metaMethod -> + metaMethod.getName().equals(methodName) && metaMethod.isValidMethod(argsArray) + ).toList(); + if (metaMethods.size() > 1) { + @SuppressWarnings("rawtypes") + final Class[] argTypes = Arrays.stream(argsArray).map(Object::getClass).toArray(Class[]::new); + throw new MethodSelectionException(methodName, new FastArray(metaMethods), argTypes); + } else if (metaMethods.size() == 1) { + return metaMethods.getFirst(); + } else { + return null; + } + } + + private static void warnWrongType(Object object) { + logger.warn( + "StandardGStringTemplateViewMetaClass should only be used as a MetaClass of StandardGStringTemplateViewMetaClass or a subclass thereof; given " + + object + " of type " + object.getClass() + ); + } + + private static MetaClass findMetaClass(Object object) { + return switch (object) { + case GroovyObject groovyObject -> groovyObject.getMetaClass(); + default -> GroovySystem.getMetaClassRegistry().getMetaClass(object.getClass()); + }; + } + + public StandardGStringTemplateViewMetaClass() { + super(StandardGStringTemplateView.class, true, true); + } + + @Override + public Object invokeMethod(Object object, String methodName, Object arguments) { + final StandardGStringTemplateView view = asView(object); + if (view == null) { + logger.warn("StandardGStringTemplateViewMetaClass should only be used as a MetaClass of StandardGStringTemplateViewMetaClass."); + return super.invokeMethod(object, methodName, arguments); + } + + // local closure + final Map locals = view.getLocals(); + if (locals.containsKey(methodName)) { + final Object local = locals.get(methodName); + if (local instanceof @SuppressWarnings("rawtypes") Closure closure) { + return closure.call(arguments); + } + } + + // for both self and parent + final Object[] argsArray = asArgsArray(arguments); + + // self + final MetaMethod selfMethod = findMetaMethod(this, methodName, argsArray); + if (selfMethod != null) { + return selfMethod.invoke(view, argsArray); + } + + // parent hierarchy + View parent = view.getParent(); + while (parent != null) { + final var parentMetaMethod = findMetaMethod(findMetaClass(parent), methodName, argsArray); + if (parentMetaMethod != null) { + return parentMetaMethod.invoke(parent, argsArray); + } else if (parent instanceof StandardGStringTemplateView) { + parent = ((StandardGStringTemplateView) parent).getParent(); + } else { + parent = null; + } + } + + return super.invokeMethod(object, methodName, arguments); + } + + @Override + public Object getProperty(Object object, String name) { + final StandardGStringTemplateView view = asView(object); + if (view == null) { + warnWrongType(object); + return super.getProperty(object, name); + } + + // local + final Map locals = view.getLocals(); + if (locals != null && locals.containsKey(name)) { + return view.getLocals().get(name); + } + + // self + final var metaProperty = this.getMetaProperty(name); + if (metaProperty != null) { + return metaProperty.getProperty(object); + } + + // parent hierarchy + View parent = view.getParent(); + while (parent != null) { + final var parentMetaProperty = findMetaClass(parent).getMetaProperty(name); + if (parentMetaProperty != null) { + return parentMetaProperty.getProperty(parent); + } else if (parent instanceof StandardGStringTemplateView) { + parent = ((StandardGStringTemplateView) parent).getParent(); + } else { + parent = null; + } + } + + // all else fails, try super metaClass + return super.getProperty(object, name); + } + + @Override + public void setProperty(Object object, String name, Object value) { + final StandardGStringTemplateView view = asView(object); + if (view == null) { + warnWrongType(object); + super.setProperty(object, name, value); + return; + } + + // local + final Map locals = view.getLocals(); + if (locals != null && locals.containsKey(name)) { + locals.put(name, value); + return; + } + + // self + final var selfMetaProperty = this.getMetaProperty(name); + if (selfMetaProperty != null) { + selfMetaProperty.setProperty(view, value); + return; + } + + // parent hierarchy + View parent = view.getParent(); + while (parent != null) { + final var parentMetaProperty = findMetaClass(parent).getMetaProperty(name); + if (parentMetaProperty != null) { + parentMetaProperty.setProperty(parent, value); + return; + } else if (parent instanceof StandardGStringTemplateView) { + parent = ((StandardGStringTemplateView) parent).getParent(); + } else { + parent = null; + } + } + + // all else fails, try super metaClass + super.setProperty(object, name, value); + } + +} diff --git a/views/src/main/groovy/groowt/view/TemplateView.java b/views/src/main/groovy/groowt/view/TemplateView.java new file mode 100644 index 0000000..3ccc0d3 --- /dev/null +++ b/views/src/main/groovy/groowt/view/TemplateView.java @@ -0,0 +1,27 @@ +package groowt.view; + +import groovy.lang.Writable; +import groovy.text.Template; + +import java.io.IOException; +import java.io.Writer; +import java.util.Map; + +public class TemplateView extends AbstractView { + + private final Template template; + + public TemplateView(Template template) { + this.template = template; + } + + @Override + public void renderTo(Writer writer) throws IOException { + this.getWritableFrom(this.template).writeTo(writer); + } + + protected Writable getWritableFrom(Template template) { + return template.make(this.asType(Map.class)); + } + +} diff --git a/views/src/main/groovy/groowt/view/View.java b/views/src/main/groovy/groowt/view/View.java new file mode 100644 index 0000000..2b19815 --- /dev/null +++ b/views/src/main/groovy/groowt/view/View.java @@ -0,0 +1,57 @@ +package groowt.view; + +import groovy.lang.Closure; +import groovy.lang.Writable; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; + +public interface View { + + /** + * TODO: consider making this a (package private?) separate class, perhaps with support for GStringTemplateViews, etc.? + */ + final class ClosureView extends Closure { + + private final View view; + + public ClosureView(View view) { + super(view, view); + this.view = view; + } + + public void doCall(Writer writer) throws IOException { + this.view.renderTo(writer); + } + + public String doCall() { + return this.view.render(); + } + + } + + void renderTo(Writer writer) throws IOException; + + default String render() { + final Writer w = new StringWriter(); + try { + this.renderTo(w); + return w.toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + default Writable asWritable() { + return writer -> { + this.renderTo(writer); + return writer; + }; + } + + default Closure asClosure() { + return new ClosureView(this); + } + +} diff --git a/views/src/test/groovy/groowt/view/GStringTemplateViewTests.java b/views/src/test/groovy/groowt/view/GStringTemplateViewTests.java new file mode 100644 index 0000000..dfab154 --- /dev/null +++ b/views/src/test/groovy/groowt/view/GStringTemplateViewTests.java @@ -0,0 +1,100 @@ +package groowt.view; + +import groovy.lang.Closure; +import groovy.text.GStringTemplateEngine; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class GStringTemplateViewTests { + + private static final class GreetingView extends GStringTemplateView { + + private final String greeting = "Hello, world!"; + + public GreetingView(GStringTemplateEngine engine) { + super(Map.of( + "engine", engine, + "src", "<%= greeting %>" + )); + } + + public String getGreeting() { + return this.greeting; + } + + } + + private final GStringTemplateEngine engine = new GStringTemplateEngine(); + + @Test + public void helloWorld() { + final var view = new GreetingView(this.engine); + assertEquals("Hello, world!", view.render()); + } + + @Test + public void coerceToClosureCallNoArgs() { + final var view = new GreetingView(this.engine); + final var cl = view.asClosure(); + assertEquals("Hello, world!", cl.call()); + } + + @Test + public void coerceToClosureCallWithWriter() { + final var view = new GreetingView(this.engine); + final var cl = view.asClosure(); + final var w = new StringWriter(); + cl.call(w); + assertEquals("Hello, world!", w.toString()); + } + + @Test + public void coerceToWritable() throws IOException { + final var view = new GreetingView(this.engine); + final var writable = view.asWritable(); + final var w = new StringWriter(); + writable.writeTo(w); + assertEquals("Hello, world!", w.toString()); + } + + @SuppressWarnings("unchecked") + @Test + public void coerceToMap() { + final var view = new GreetingView(this.engine); + final Map map = view.asType(Map.class); + assertTrue(map.containsKey("greeting")); + assertEquals("Hello, world!", map.get("greeting")); + } + + @Test + public void yieldingViewSimple() { + final var greetingView = new GreetingView(this.engine); + final Closure yieldClosure = new Closure(this) { + + public String doCall() { + return greetingView.render(); + } + + }; + final var view = new GStringTemplateView(Map.of( + "engine", this.engine, + "src", "Yielded: <%= yield() %>" + )); + assertEquals("Yielded: Hello, world!", view.render(yieldClosure)); + } + + @Test + public void simplePartialWithLocal() { + final var view = new GStringTemplateView(Map.of( + "engine", this.engine, + "src", "<%= partial templateResource('simplePartial.gst'), [greeting: 'Hello, World!'] %>" + )); + assertEquals("Hello from partial. Greeting: Hello, World!", view.render()); + } + +} diff --git a/views/src/test/groovy/groowt/view/StandardGStringTemplateViewTests.groovy b/views/src/test/groovy/groowt/view/StandardGStringTemplateViewTests.groovy new file mode 100644 index 0000000..385bb90 --- /dev/null +++ b/views/src/test/groovy/groowt/view/StandardGStringTemplateViewTests.groovy @@ -0,0 +1,44 @@ +package groowt.view + +import org.junit.jupiter.api.Test + +import static org.junit.jupiter.api.Assertions.assertEquals + +class StandardGStringTemplateViewTests { + + @Test + void smokeScreen() { + def view = new StandardGStringTemplateView(src: '<%= "hello" %>') + assertEquals('hello', view.render()) + } + + @Test + void fetchesLocal() { + def view = new StandardGStringTemplateView(src: '<%= greeting %>', locals: [greeting: 'hello']) + assertEquals('hello', view.render()) + } + + private static final class GreetingView extends StandardGStringTemplateView { + + final String greeting = 'hello' + + GreetingView() { + super(src: '<%= greeting %>') + } + + } + + @Test + void fetchesFromSelf() { + def view = new GreetingView() + assertEquals('hello', view.render()) + } + + @Test + void fetchesFromParent() { + def parent = new GreetingView() + def view = new StandardGStringTemplateView(src: '<%= greeting %>', parent: parent) + assertEquals('hello', view.render()) + } + +} diff --git a/views/src/test/resources/groowt/view/simplePartial.gst b/views/src/test/resources/groowt/view/simplePartial.gst new file mode 100644 index 0000000..492b5fd --- /dev/null +++ b/views/src/test/resources/groowt/view/simplePartial.gst @@ -0,0 +1 @@ +Hello from partial. Greeting: <%= greeting %> \ No newline at end of file diff --git a/web-views/.gitignore b/web-views/.gitignore new file mode 100644 index 0000000..ba077a4 --- /dev/null +++ b/web-views/.gitignore @@ -0,0 +1 @@ +bin diff --git a/web-views/build.gradle b/web-views/build.gradle new file mode 100644 index 0000000..ea317ee --- /dev/null +++ b/web-views/build.gradle @@ -0,0 +1,196 @@ +import groovy.transform.NullCheck +import groowt.gradle.antlr.GroowtAntlrTask + +plugins { + id 'java' + id 'java-library' + id 'groovy' + id 'antlr' + id 'GroowtAntlrPlugin' + id 'org.jetbrains.kotlin.jvm' + id 'java-test-fixtures' +} + +repositories { + mavenCentral() +} + +configurations { + groovyConsole + toolsImplementation { + extendsFrom(apiElements, runtimeElements) + } +} + +sourceSets { + tools { + java { + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output + } + } +} + +dependencies { + api( + libs.groovy, + libs.groovy.templates, + libs.antlr.runtime, + project(':view-components'), + project(':views') + ) + compileOnlyApi libs.jetbrains.anotations + implementation( + libs.slf4j.api, + libs.kotlin.stdlib, + libs.log4j.core, + libs.jansi, + libs.asm, + project(':di'), + project(':extensible') + ) + antlr libs.antlr + runtimeOnly libs.log4j.slf4jBinding + + def testLibs = [ + libs.junit.jupiter.api, libs.mockito.core, libs.mockito.junit + ] + + testLibs.each { + testApi it + testFixturesApi it + } + + groovyConsole libs.groovy.console + toolsApi libs.picocli + toolsImplementation libs.groovy.console +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +ext { + antlrPackageName = 'groowt.view.web.antlr' + toolsPackageName = 'groowt.view.web.tools' +} + +groowtAntlr { + packageName = project.ext.get('antlrPackageName') + visitor = true +} + +// Must be temporarily afterEvaluate unless we switch the GroowtAntlr plugin +// to eagerly create the tasks upon plugin application +afterEvaluate { + tasks.named('generateWebViewComponentsLexerBase', GroowtAntlrTask) { task -> + doLast { + def pattern = ~/public class WebViewComponentsLexerBase(.*)/ + def lexerSource = new File(task.outputDirectory, 'WebViewComponentsLexerBase.java') + def outLines = lexerSource.readLines().collect { + def matcher = pattern.matcher(it) + if (matcher.matches()) { + return 'public abstract class WebViewComponentsLexerBase' + matcher.group(1) + } else { + return it + } + } + lexerSource.write(outLines.join('\n')) + } + } +} + +tasks.register('groovyConsole', JavaExec) { + group = 'groovy' + classpath += sourceSets.main.runtimeClasspath + configurations.groovyConsole + mainClass = 'groovy.console.ui.Console' +} + +tasks.register('toolsJar', Jar) { + group 'tools' + archiveBaseName = 'web-tools' + exclude { FileTreeElement element -> + element.file in sourceSets.main.output + } + from sourceSets.tools.output + from sourceSets.tools.runtimeClasspath.filter(File.&exists).collect { it.isDirectory() ? it : zipTree(it) } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + dependsOn tasks.named('jar') +} + +@NullCheck +class ToolSpec { + + final String name + final String mainClass + final String mainClassPackage + + ToolSpec(String name, String mainClass, String mainClassPackage) { + this.name = name + this.mainClass = mainClass + this.mainClassPackage = mainClassPackage + } + + String getFullMainClass() { + this.mainClassPackage + '.' + this.mainClass + } + +} + +def toolSpec = { String name, String mainClass -> + new ToolSpec(name, mainClass, project.ext.get('toolsPackageName') as String) +} + +final List toolSpecs = [ + toolSpec('parseTreeFileMaker', 'ParseTreeFileMakerCli'), + toolSpec('astFileMaker', 'AstFileMakerCli'), + toolSpec('convertToGroovy', 'ConvertToGroovy'), + toolSpec('lexer', 'LexerTool'), + toolSpec('parser', 'ParserTool') +] + +toolSpecs.each { spec -> + tasks.register("create${spec.name.capitalize()}StartScripts", CreateStartScripts) { + group = 'tools' + outputDir = file('bin') + applicationName = spec.name + mainClass = spec.fullMainClass + unixStartScriptGenerator.template = resources.text.fromFile('src/tools/binTemplate.gst') + dependsOn tasks.named('toolsJar') + } +} + +tasks.register('createToolsStartScripts') { + group 'tools' + dependsOn tasks.matching { Task task -> + task != it + && task.name.startsWith('create') + && task.name.endsWith('StartScripts') + && task.group == 'tools' + } +} + +tasks.register('cleanBin', Delete) { + group = 'tools' + delete file('bin') +} + +// Hacky +tasks.named('compileKotlin').configure { + dependsOn 'generateLexerFragments', 'generateWebViewComponentsLexerBase', 'generateWebViewComponentsParser' +} + +test { + jvmArgs '-XX:+EnableDynamicAgentLoading' // for mockito/bytebuddy + testLogging.showStandardStreams = true +} + +testing { + suites { + test { + useJUnitJupiter() + } + } +} diff --git a/web-views/sketching.gst b/web-views/sketching.gst new file mode 100644 index 0000000..0a37f78 --- /dev/null +++ b/web-views/sketching.gst @@ -0,0 +1,20 @@ +--- +import some.Thing // a comment + +def greeting = 'Hello, World!' +--- + + + + +

${greeting}

+ + +

It's true! :)

+
+ +

It's false... :(

+
+
+ + diff --git a/web-views/sketching/.gitignore b/web-views/sketching/.gitignore new file mode 100644 index 0000000..c1f99c7 --- /dev/null +++ b/web-views/sketching/.gitignore @@ -0,0 +1,2 @@ +*.groovy +classes diff --git a/web-views/sketching/helloTarget.wvc b/web-views/sketching/helloTarget.wvc new file mode 100644 index 0000000..a9c4d42 --- /dev/null +++ b/web-views/sketching/helloTarget.wvc @@ -0,0 +1 @@ +Hello, $target! diff --git a/web-views/sketching/preambleHelloTarget.wvc b/web-views/sketching/preambleHelloTarget.wvc new file mode 100644 index 0000000..bc9b1fd --- /dev/null +++ b/web-views/sketching/preambleHelloTarget.wvc @@ -0,0 +1,9 @@ +--- +import groowt.view.component.ComponentContext + +class Helper { + String greeting +} +--- +Hello, $target! +What a nice day. diff --git a/web-views/sketching/simpleGreeter.wvc b/web-views/sketching/simpleGreeter.wvc new file mode 100644 index 0000000..fa47c6d --- /dev/null +++ b/web-views/sketching/simpleGreeter.wvc @@ -0,0 +1 @@ + diff --git a/web-views/src/main/antlr/LexerFragments.g4 b/web-views/src/main/antlr/LexerFragments.g4 new file mode 100644 index 0000000..1b4ff9a --- /dev/null +++ b/web-views/src/main/antlr/LexerFragments.g4 @@ -0,0 +1,70 @@ +lexer grammar LexerFragments; + +fragment +NL : [\n\r] ; + +fragment +WS : [ \t] ; + +fragment +NLWS : NL | WS ; + +fragment +TWO_DASH : '--' ; + +fragment +THREE_DASH : '---' ; + +fragment +FS : '/' ; + +fragment +BS : '\\' ; + +fragment +LT : '<' ; + +fragment +GT : '>' ; + +fragment +LP : '(' ; + +fragment +RP : ')' ; + +fragment +DQ : '"' ; + +fragment +SQ : '\'' ; + +fragment +STAR : '*' ; + +fragment +LEFT_CURLY : '{' ; + +fragment +RIGHT_CURLY : '}' ; + +fragment +DOLLAR : '$' ; + +fragment +DOT : '.' ; + +fragment +MINUS : '-' ; + +fragment +QUESTION : '?' ; + +fragment +EQ : '=' ; + +fragment +PERCENT : '%' ; + +fragment +BANG : '!' ; diff --git a/web-views/src/main/antlr/WebViewComponentsLexerBase.g4 b/web-views/src/main/antlr/WebViewComponentsLexerBase.g4 new file mode 100644 index 0000000..13b86be --- /dev/null +++ b/web-views/src/main/antlr/WebViewComponentsLexerBase.g4 @@ -0,0 +1,543 @@ +lexer grammar WebViewComponentsLexerBase; + +import LexerFragments; + +options { + superClass = AbstractWebViewComponentsLexer; +} + +tokens { + PreambleBreak, + GroovyCode, + GStringAttrValueEnd, + JStringAttrValueEnd, + ClosureAttrValueEnd, + DollarScriptletClose +} + +@header { + import java.util.Set; + import static groowt.view.web.antlr.LexerSemanticPredicates.*; +} + +@members { + + public static final Set GroovyTokens = Set.of( + GroovyCodeChars, + LeftParenthesis, + RightParenthesis, + LeftCurly, + RightCurly, + LineCommentStart, + StarCommentStart, + JStringStart, + GStringStart, + TripleJStringStart, + TripleGStringStart, + ParenthesesSlashyStringStart, + DollarSlashyStringStart, + LineCommentText, + LineCommentEnd, + StarCommentChars, + StarCommentEnd, + JStringText, + JStringEnd, + GStringText, + GStringDollarValueStart, + GStringClosureStart, + GStringEnd, + GStringIdentifier, + GStringDot, + GStringPathEnd, + GStringIdentifierStartChar, + GStringIdentifierChar, + TripleJStringContent, + TripleJStringEnd, + TripleGStringDollarValueStart, + TripleGStringClosureStart, + TripleGStringText, + TripleGStringEnd, + ParenthesesSlashyStringText, + ParenthesesSlashyStringDollarValueStart, + ParenthesesSlashyStringClosureStart, + ParenthesesSlashyStringEnd, + DollarSlashyStringText, + DollarSlashyStringDollarValueStart, + DollarSlashyStringClosureStart, + DollarSlashyStringEnd + ); + + public static final Set GStringParts = Set.of( + GStringDollarValueStart, + GStringClosureStart, + GStringIdentifier, + GStringDot, + GStringPathEnd, + GStringIdentifierStartChar, + GStringIdentifierChar, + TripleGStringDollarValueStart, + TripleGStringClosureStart, + ParenthesesSlashyStringDollarValueStart, + ParenthesesSlashyStringClosureStart, + DollarSlashyStringDollarValueStart, + DollarSlashyStringClosureStart + ); + + private void onPreambleClose() { + this.setType(PreambleBreak); + this.exitPreamble(); + this.popMode(); + } + + private void onGStringClosure() { + this.curlies.push(this::popMode); // onPop + this.curlies.increment(); // after, curlies.currentCount == 1 + this.pushMode(GROOVY_CODE); + } + + @Override + protected void enterConstructor() { + super.enterConstructor(); // setup state + this.pushMode(GROOVY_CODE); + } + +} + +// ---------------------------------------- +// DEFAULT_MODE + +PreambleOpen + : THREE_DASH ( NL | WS+ )? { this.canPreamble() }? { this.enterPreamble(); } + -> type(PreambleBreak), pushMode(GROOVY_CODE) + ; + +ComponentOpen + : LT -> pushMode(IN_TAG) + ; + +ClosingComponentOpen + : LT FS -> pushMode(IN_TAG) + ; + +EqualsScriptletOpen + : LT PERCENT EQ -> pushMode(GROOVY_CODE) + ; + +PlainScriptletOpen + : LT PERCENT -> pushMode(GROOVY_CODE) + ; + +DollarScriptletOpen + : DOLLAR LEFT_CURLY { + this.curlies.push(() -> { + this.setType(DollarScriptletClose); + this.popMode(); + }); + this.curlies.increment(); + this.pushMode(GROOVY_CODE); + } + ; + +DollarReferenceStart + : DOLLAR { isIdentifierStartChar(this.getNextChar()) }? -> pushMode(IN_G_STRING_PATH) + ; + +QuestionTag + : LT QUESTION .*? QUESTION GT + ; + +HtmlComment + : LT BANG TWO_DASH .*? TWO_DASH GT + ; + +RawText + : ( ~[-<$] // n.b.: LT cannot be escaped, only via < + | MINUS { !this.isNext("--") }? + | DOLLAR { !this.isNext('{') && !isIdentifierStartChar(this.getNextChar()) }? + | LT BANG { !this.isNext("--") }? + )+ + ; + +// ---------------------------------------- +mode IN_TAG; + +ComponentSelfClose + : FS GT -> popMode + ; + +ComponentClose + : GT -> popMode + ; + +Identifier + : IdentifierStartChar IdentifierChar* + ; + +IdentifierStartChar + : ~[.] { isIdentifierStartChar(this.getCurrentChar()) }? + ; + +IdentifierChar + : ~[.] { isIdentifierChar(this.getCurrentChar()) }? + ; + +ConstructorOpen + : LP { this.enterConstructor(); } + ; + +Dot + : DOT + ; + +Equals + : EQ + ; + +GStringAttrValueStart + : DQ -> pushMode(IN_G_STRING) + ; + +JStringAttrValueStart + : SQ -> pushMode(IN_J_STRING) + ; + +ClosureAttrValueStart + : LEFT_CURLY ComponentNlws? { !this.isNext('<') }? + { + this.curlies.push(() -> { + this.setType(ClosureAttrValueEnd); + this.popMode(); + }); + this.curlies.increment(); + this.pushMode(GROOVY_CODE); + } + ; + +ComponentAttrValueStart + : LEFT_CURLY ComponentNlws? { this.isNext('<') }? + ; + +ComponentAttrValueEnd + : GT RIGHT_CURLY + ; + +ComponentNlws + : NLWS+ + ; + +// ---------------------------------------- +mode GROOVY_CODE; + +PreambleClose + : NL? THREE_DASH ( NL | WS+ )? { this.inPreamble() }? + { + this.onPreambleClose(); + } + ; + +ScriptletClose + : PERCENT GT -> popMode + ; + +GroovyCodeChars + : ( ~[/\n\r\-$%(){}'"] + | FS { !isAnyOf(this.getNextChar(), '/', '*') }? + | NL { !this.inPreamble() || !this.isNext("---") }? + | MINUS { !(this.getCharPositionInLine() == 1 && this.isNext("--")) }? + | DOLLAR { !this.isNext('/') }? + | PERCENT { !this.isNext('>') }? + )+ + ; + +LeftParenthesis + : LP { !this.isNext('/') }? + { + if (this.parentheses.isCounting()) { + this.parentheses.increment(); + } + } + ; + +ConstructorClose + : RP { this.canExitConstructor() }? { this.exitConstructor(); } + ; + +RightParenthesis + : RP { !this.inConstructor() }? + { + if (this.parentheses.isCounting()) { + this.parentheses.decrement(); + } + } + ; + +LeftCurly + : LEFT_CURLY + { + if (this.curlies.isCounting()) { + this.curlies.increment(); + } + } + ; + +RightCurly + : RIGHT_CURLY + { + if (this.curlies.isCounting()) { + if (this.curlies.isLast()) { + this.curlies.pop(); // calls this.pop() in onPop + } else { + this.curlies.decrement(); + } + } + } + ; + +LineCommentStart + : FS FS -> pushMode(IN_LINE_COMMENT) + ; + +StarCommentStart + : FS STAR -> pushMode(IN_STAR_COMMENT) + ; + +JStringStart + : SQ { canFollowJStringOpening(this.getNextCharsAsString(2)) }? -> pushMode(IN_J_STRING) + ; + +GStringStart + : DQ { canFollowGStringOpening(this.getNextCharsAsString(2)) }? -> pushMode(IN_G_STRING) + ; + +TripleJStringStart + : SQ SQ SQ -> pushMode(IN_TRIPLE_J_STRING) + ; + +TripleGStringStart + : DQ DQ DQ -> pushMode(IN_TRIPLE_G_STRING) + ; + +ParenthesesSlashyStringStart + : LP FS { !isAnyOf(this.getNextChar(), '/', '*') }? -> pushMode(IN_PARENTHESES_SLASHY_STRING) + ; + +DollarSlashyStringStart + : DOLLAR FS -> pushMode(IN_DOLLAR_SLASHY_STRING) + ; + +// ---------------------------------------- +mode IN_LINE_COMMENT; + +LineCommentText + : ~[\n\r]+ + ; + +LineCommentEnd + : NL -> popMode + ; + +// ---------------------------------------- +mode IN_STAR_COMMENT; + +StarCommentChars + : ~'*'+ + ; + +StarCommentEnd + : STAR FS -> popMode + ; + +// ---------------------------------------- +mode IN_J_STRING; + +JStringText + : ( ~[\n\r'] | BS SQ )+ + ; + +JStringEnd + : SQ + { + if (this.peekMode(1) == IN_TAG) { + this.setType(JStringAttrValueEnd); + } + this.popMode(); + } + ; + +// ---------------------------------------- +mode IN_G_STRING; + +GStringText + : ( ~[\n\r"$] + | BS DQ + | BS DOLLAR + )+ + ; + +GStringDollarValueStart + : DOLLAR { !this.isNext('{') }? -> pushMode(IN_G_STRING_PATH) + ; + +GStringClosureStart + : DOLLAR LEFT_CURLY { this.onGStringClosure(); } + ; + +GStringEnd + : DQ + { + if (this.peekMode(1) == IN_TAG) { + this.setType(GStringAttrValueEnd); + } + this.popMode(); + } + ; + +// ---------------------------------------- +mode IN_G_STRING_PATH; + +GStringIdentifier + : GStringIdentifierStartChar GStringIdentifierChar* + ; + +GStringDot + : DOT { isGStringIdentifierStartChar(this.getNextChar()) }? + ; + +GStringPathEnd + : ~[."/] { !isGStringIdentifierChar(this.getCurrentChar()) }? + { + final var current = this.getCurrentChar(); + final GStringPathEndSpec endSpec = switch (this.peekMode(1)) { + case IN_G_STRING -> { + if (current == '"') { + yield new StringClosingEndSpec(GStringEnd, 2); + } else { + yield new StringContinueEndSpec(); + } + } + case IN_TRIPLE_G_STRING -> { + if (current == '"' && this.isNext("\"\"")) { + yield new StringClosingEndSpec(TripleGStringEnd, 2); + } else { + yield new StringContinueEndSpec(); + } + } + case IN_PARENTHESES_SLASHY_STRING -> { + if (current == '/' && this.isNext(')')) { + yield new StringClosingEndSpec(ParenthesesSlashyStringEnd, 2); + } else { + yield new StringContinueEndSpec(); + } + } + case IN_DOLLAR_SLASHY_STRING -> { + if (current == '/' && this.isNext('$')) { + yield new StringClosingEndSpec(DollarSlashyStringEnd, 2); + } else { + yield new StringContinueEndSpec(); + } + } + case IN_TAG -> { + if (current == '"') { + yield new StringClosingEndSpec(GStringAttrValueEnd, 2); + } else { + yield new StringContinueEndSpec(); + } + } + case DEFAULT_MODE -> new StringContinueEndSpec(); + default -> throw new IllegalStateException("not a valid gString context: " + this.getModeName(this.peekMode(1))); + }; + switch (endSpec) { + case StringContinueEndSpec ignored -> { + this.popMode(); + this.rollbackOne(); + } + case StringClosingEndSpec closingEndSpec -> { + this.setType(closingEndSpec.type()); + for (int i = 0; i < closingEndSpec.popCount(); i++) { + this.popMode(); + } + } + } + } + ; + +GStringIdentifierStartChar + : ~'.' { isGStringIdentifierStartChar(this.getCurrentChar()) }? + ; + +GStringIdentifierChar + : ~'.' { isGStringIdentifierChar(this.getCurrentChar()) }? + ; + +// ---------------------------------------- +mode IN_TRIPLE_J_STRING; + +// TODO: check for unescaped SQ, I think groovy allows them +TripleJStringContent + : ( ~['] | BS SQ )+ + ; + +TripleJStringEnd + : SQ SQ SQ -> popMode + ; + +// ---------------------------------------- +mode IN_TRIPLE_G_STRING; + +// TODO: check for unescaped DQ, I think groovy allows them +TripleGStringDollarValueStart + : DOLLAR { !this.isNext('{') }? -> pushMode(IN_G_STRING_PATH) + ; + +TripleGStringClosureStart + : DOLLAR LEFT_CURLY { this.onGStringClosure(); } + ; + +TripleGStringText + : ( ~["$] | BS DQ | BS DOLLAR )+ + ; + +TripleGStringEnd + : DQ DQ DQ -> popMode + ; + +// ---------------------------------------- +mode IN_PARENTHESES_SLASHY_STRING; + +ParenthesesSlashyStringText + : ( ~'/' | BS FS )+ + ; + +ParenthesesSlashyStringDollarValueStart + : DOLLAR { !this.isNext('{') }? -> pushMode(IN_G_STRING_PATH) + ; +ParenthesesSlashyStringClosureStart + : DOLLAR LEFT_CURLY { this.onGStringClosure(); } + ; + +ParenthesesSlashyStringEnd + : FS RP -> popMode + ; + +// ---------------------------------------- +mode IN_DOLLAR_SLASHY_STRING; + +DollarSlashyStringText + : ( ~[$/] + | DOLLAR DOLLAR + | DOLLAR FS + | FS DOLLAR DOLLAR + )+ + ; + +DollarSlashyStringDollarValueStart + : DOLLAR { !isAnyOf(this.getNextChar(), '/', '$', '{') }? -> pushMode(IN_G_STRING_PATH) + ; + +DollarSlashyStringClosureStart + : DOLLAR LEFT_CURLY { this.onGStringClosure(); } + ; + +DollarSlashyStringEnd + : FS DOLLAR { !this.isNext('$') }? -> popMode + ; diff --git a/web-views/src/main/antlr/WebViewComponentsParser.g4 b/web-views/src/main/antlr/WebViewComponentsParser.g4 new file mode 100644 index 0000000..69c7539 --- /dev/null +++ b/web-views/src/main/antlr/WebViewComponentsParser.g4 @@ -0,0 +1,123 @@ +parser grammar WebViewComponentsParser; + +options { + tokenVocab = WebViewComponentsLexerBase; +} + +compilationUnit + : preamble? body? EOF + ; + +preamble + : PreambleBreak ( GroovyCode? PreambleBreak )? + ; + +body + : bodyText? ( component bodyText? )+ | bodyText + ; + +bodyText + : gStringBodyText | jStringBodyText + ; + +gStringBodyText + : jStringBodyText? ( gStringBodyTextGroovyElement jStringBodyText? )+ + ; + +jStringBodyText + : ( QuestionTag | HtmlComment | RawText )+ + ; + +gStringBodyTextGroovyElement + : plainScriptlet | equalsScriptlet | dollarScriptlet | dollarReference + ; + +component + : selfClosingComponent | componentWithChildren | fragmentComponent + ; + +selfClosingComponent + : ComponentOpen componentArgs ComponentSelfClose + ; + +componentWithChildren + : openComponent children=body? closingComponent + ; + +openComponent + : ComponentOpen componentArgs ComponentClose + ; + +closingComponent + : ClosingComponentOpen ComponentNlws? + componentType ComponentNlws? + ComponentClose + ; + +fragmentComponent + : ComponentOpen ComponentClose + body? + ClosingComponentOpen ComponentClose + ; + +componentArgs + : ComponentNlws? componentType + ComponentNlws? componentConstructor? + ComponentNlws? ( attr ComponentNlws? )* + ; + +componentType + : Identifier ( Dot Identifier )* + ; + +componentConstructor + : ConstructorOpen GroovyCode? ConstructorClose + ; + +attr + : keyValueAttr | booleanAttr + ; + +keyValueAttr + : Identifier Equals value + ; + +booleanAttr + : Identifier + ; + +value + : gStringAttrValue | jStringAttrValue | closureAttrValue | componentAttrValue + ; + +gStringAttrValue + : GStringAttrValueStart GroovyCode? GStringAttrValueEnd + ; + +jStringAttrValue + : JStringAttrValueStart GroovyCode? JStringAttrValueEnd + ; + +closureAttrValue + : ClosureAttrValueStart GroovyCode? ClosureAttrValueEnd + ; + +componentAttrValue + : ComponentAttrValueStart component ComponentAttrValueEnd + ; + +equalsScriptlet + : EqualsScriptletOpen GroovyCode? ScriptletClose + ; + +plainScriptlet + : PlainScriptletOpen GroovyCode? ScriptletClose + ; + +dollarScriptlet + : DollarScriptletOpen GroovyCode? DollarScriptletClose + ; + +dollarReference + : DollarReferenceStart GroovyCode + ; diff --git a/web-views/src/main/java/groowt/view/web/ChildRenderException.java b/web-views/src/main/java/groowt/view/web/ChildRenderException.java new file mode 100644 index 0000000..ef0058c --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ChildRenderException.java @@ -0,0 +1,9 @@ +package groowt.view.web; + +public class ChildRenderException extends RuntimeException { + + public ChildRenderException(Throwable cause) { + super(cause); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/DefaultWebComponentTemplateCompiler.java b/web-views/src/main/java/groowt/view/web/DefaultWebComponentTemplateCompiler.java new file mode 100644 index 0000000..c53af22 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/DefaultWebComponentTemplateCompiler.java @@ -0,0 +1,208 @@ +package groowt.view.web; + +import groovy.lang.GroovyClassLoader; +import groowt.util.di.RegistryObjectFactory; +import groowt.view.component.CachingComponentTemplateCompiler; +import groowt.view.component.ComponentTemplate; +import groowt.view.component.ComponentTemplateCreateException; +import groowt.view.component.ViewComponent; +import groowt.view.web.antlr.CompilationUnitParseResult; +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 groowt.view.web.transpile.TranspilerConfiguration; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.Phases; +import org.codehaus.groovy.control.io.AbstractReaderSource; +import org.codehaus.groovy.tools.GroovyClass; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.io.*; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; +import java.util.function.Supplier; + +public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplateCompiler { + + private final CompilerConfiguration configuration; + private final String defaultPackageName; + private final int phase; + + private GroovyClassLoader groovyClassLoader; + + public DefaultWebComponentTemplateCompiler( + CompilerConfiguration configuration, + String defaultPackageName + ) { + this(configuration, defaultPackageName, Phases.CLASS_GENERATION); + } + + @ApiStatus.Internal + public DefaultWebComponentTemplateCompiler( + CompilerConfiguration configuration, + String defaultPackageName, + int phase + ) { + this.configuration = configuration; + this.defaultPackageName = defaultPackageName; + this.phase = phase; + } + + protected GroovyClassLoader getGroovyClassLoader() { + if (this.groovyClassLoader == null) { + this.groovyClassLoader = new GroovyClassLoader(this.getClass().getClassLoader()); + } + return this.groovyClassLoader; + } + + public void setGroovyClassLoader(GroovyClassLoader groovyClassLoader) { + this.groovyClassLoader = Objects.requireNonNull(groovyClassLoader); + } + + public void useOwnClassLoader() { + this.groovyClassLoader = null; + } + + protected ComponentTemplate doCompile(Class forClass, Reader reader) { + return this.doCompile(forClass, reader, null); + } + + protected ComponentTemplate doCompile(Class forClass, Reader reader, @Nullable URI uri) { + final CompilationUnitParseResult parseResult = ParserUtil.parseCompilationUnit(reader); + + // TODO: analysis + + final var tokenList = new TokenList(parseResult.getTokenStream()); + final var astBuilder = new DefaultAstBuilder(new DefaultNodeFactory(tokenList)); + final var cuNode = (CompilationUnitNode) astBuilder.build(parseResult.getCompilationUnitContext()); + + final var groovyCompilationUnit = new CompilationUnit(this.configuration); + final var transpiler = new DefaultGroovyTranspiler( + groovyCompilationUnit, + this.defaultPackageName, + DefaultTranspilerConfiguration::new + ); + + final var ownerComponentName = forClass.getSimpleName(); + 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); + + groovyCompilationUnit.compile(this.phase); + + final var classes = groovyCompilationUnit.getClasses(); + Class templateClass = null; + for (final GroovyClass groovyClass : classes) { + if (groovyClass.getName().equals(fqn)) { + if (templateClass == null) { + templateClass = this.getGroovyClassLoader().defineClass( + groovyClass.getName(), groovyClass.getBytes() + ); + } else { + throw new IllegalStateException("Somehow found two classes with same name."); + } + } else { + this.getGroovyClassLoader().defineClass( + groovyClass.getName(), groovyClass.getBytes() + ); + } + } + + if (templateClass == null) { + throw new IllegalStateException("Did not find templateClass"); + } + + try { + return (ComponentTemplate) templateClass.getConstructor().newInstance(); + } catch (Exception e) { + throw new ComponentTemplateCreateException(e, forClass, reader); + } + } + + @Override + public ComponentTemplate compile(Class forClass, File templateFile) { + return this.getFromCacheOrElse(forClass, () -> { + try { + return this.doCompile(forClass, new FileReader(templateFile)); + } catch (FileNotFoundException e) { + throw new ComponentTemplateCreateException(e, forClass, templateFile); + } + }); + } + + @Override + public ComponentTemplate compile(Class forClass, String template) { + return this.getFromCacheOrElse(forClass, () -> this.doCompile(forClass, new StringReader(template))); + } + + @Override + public ComponentTemplate compile(Class forClass, URI templateURI) { + return this.getFromCacheOrElse(forClass, () -> { + final Path path = Paths.get(templateURI); + try { + return this.doCompile(forClass, Files.newBufferedReader(path), templateURI); + } catch (IOException e) { + throw new ComponentTemplateCreateException(e, forClass, templateURI); + } + }); + } + + @Override + public ComponentTemplate compile(Class forClass, URL templateURL) { + return this.getFromCacheOrElse(forClass, () -> { + try { + return this.doCompile(forClass, new InputStreamReader(templateURL.openStream()), templateURL.toURI()); + } catch (Exception e) { + throw new ComponentTemplateCreateException(e, forClass, templateURL); + } + }); + } + + @Override + public ComponentTemplate compile(Class forClass, InputStream inputStream) { + return this.getFromCacheOrElse(forClass, () -> this.doCompile(forClass, new InputStreamReader(inputStream))); + } + + @Override + public ComponentTemplate compile(Class forClass, Reader reader) { + return this.getFromCacheOrElse(forClass, () -> this.doCompile(forClass, reader)); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/DefaultWebViewComponent.java b/web-views/src/main/java/groowt/view/web/DefaultWebViewComponent.java new file mode 100644 index 0000000..71aae6c --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/DefaultWebViewComponent.java @@ -0,0 +1,113 @@ +package groowt.view.web; + +import groovy.lang.Closure; +import groovy.lang.GroovyClassLoader; +import groowt.util.di.DefaultRegistryObjectFactory; +import groowt.view.component.AbstractViewComponent; +import groowt.view.component.ComponentContext; +import groowt.view.component.ComponentTemplate; +import groowt.view.web.WebViewTemplateComponentSource.*; +import groowt.view.web.runtime.WebViewComponentWriter; +import groowt.view.web.transpile.DefaultTranspilerConfiguration; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.jetbrains.annotations.Nullable; + +import java.io.*; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class DefaultWebViewComponent extends AbstractViewComponent implements WebViewComponent { + + private static ComponentTemplate getComponentTemplate( + Class selfType, + WebViewTemplateComponentSource source, + @Nullable GroovyClassLoader groovyClassLoader + ) { + final var compiler = new DefaultWebComponentTemplateCompiler( + CompilerConfiguration.DEFAULT, + selfType.getPackageName() + ); + + if (groovyClassLoader != null) { + compiler.setGroovyClassLoader(groovyClassLoader); + } + + return switch (source) { + case FileSource(File f) -> compiler.compile(selfType, f); + case InputStreamSource(InputStream inputStream) -> compiler.compile(selfType, inputStream); + case ReaderSource(Reader r) -> compiler.compile(selfType, r); + case StringSource(String s) -> compiler.compile(selfType, s); + case URISource(URI uri) -> compiler.compile(selfType, uri); + case URLSource(URL url) -> compiler.compile(selfType, url); + }; + } + + private ComponentContext context; + private List children; + + public DefaultWebViewComponent() {} + + public DefaultWebViewComponent(ComponentTemplate template) { + super(template); + } + + public DefaultWebViewComponent(Class templateType) { + super(templateType); + } + + public DefaultWebViewComponent(WebViewTemplateComponentSource source) { + this.setTemplate(getComponentTemplate(this.getClass(), source, null)); + } + + public DefaultWebViewComponent(WebViewTemplateComponentSource source, GroovyClassLoader groovyClassLoader) { + this.setTemplate(getComponentTemplate(this.getClass(), source, groovyClassLoader)); + } + + @Override + public void setContext(ComponentContext context) { + this.context = context; + } + + @Override + public ComponentContext getContext() { + return Objects.requireNonNull(this.context); + } + + @Override + public List getChildren() { + return Objects.requireNonNullElseGet(this.children, ArrayList::new); + } + + @Override + public void setChildren(List children) { + this.children = children; + } + + @Override + public void renderChildren() { + for (final var childRenderer : this.getChildren()) { + try { + childRenderer.render(this); + } catch (Exception e) { + throw new ChildRenderException(e); + } finally { + if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) { + this.getContext().afterComponent(childComponentRenderer.getComponent()); + } + } + } + } + + @Override + public void renderTo(Writer out) throws IOException { + final var webWriter = new WebViewComponentWriter(out); + final Closure renderer = this.getTemplate().getRenderer(); + renderer.setDelegate(this); + renderer.setResolveStrategy(Closure.DELEGATE_FIRST); + renderer.call(this.getContext(), webWriter); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/WebViewChildComponentRenderer.java b/web-views/src/main/java/groowt/view/web/WebViewChildComponentRenderer.java new file mode 100644 index 0000000..12fb166 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/WebViewChildComponentRenderer.java @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..cb67db7 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/WebViewChildGStringRenderer.java @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..1e11427 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/WebViewChildJStringRenderer.java @@ -0,0 +1,18 @@ +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 new file mode 100644 index 0000000..c7bbfec --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/WebViewChildRenderer.java @@ -0,0 +1,22 @@ +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 new file mode 100644 index 0000000..0e3692a --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/WebViewComponent.java @@ -0,0 +1,11 @@ +package groowt.view.web; + +import groowt.view.component.ViewComponent; + +import java.util.List; + +public interface WebViewComponent extends ViewComponent { + List getChildren(); + void setChildren(List children); + void renderChildren(); +} diff --git a/web-views/src/main/java/groowt/view/web/WebViewTemplateComponentSource.java b/web-views/src/main/java/groowt/view/web/WebViewTemplateComponentSource.java new file mode 100644 index 0000000..7ca4606 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/WebViewTemplateComponentSource.java @@ -0,0 +1,55 @@ +package groowt.view.web; + +import java.io.File; +import java.io.InputStream; +import java.io.Reader; +import java.net.URI; +import java.net.URL; + +public sealed interface WebViewTemplateComponentSource { + + static WebViewTemplateComponentSource of(String template) { + return new StringSource(template); + } + + static WebViewTemplateComponentSource of(File templateFile) { + return new FileSource(templateFile); + } + + static WebViewTemplateComponentSource of(URI templateURI) { + return new URISource(templateURI); + } + + static WebViewTemplateComponentSource of(URL url) { + return new URLSource(url); + } + + static WebViewTemplateComponentSource of(InputStream templateInputStream) { + return new InputStreamSource(templateInputStream); + } + + static WebViewTemplateComponentSource of(Reader templateReader) { + return new ReaderSource(templateReader); + } + + /** + * @param resourceName An absolute path resource name. + * @return A template source + */ + static WebViewTemplateComponentSource fromResource(String resourceName) { + return of(WebViewTemplateComponentSource.class.getClassLoader().getResource(resourceName)); + } + + record StringSource(String template) implements WebViewTemplateComponentSource {} + + record FileSource(File templateFile) implements WebViewTemplateComponentSource {} + + record URISource(URI templateURI) implements WebViewTemplateComponentSource {} + + record URLSource(URL templateURL) implements WebViewTemplateComponentSource {} + + record InputStreamSource(InputStream templateInputStream) implements WebViewTemplateComponentSource {} + + record ReaderSource(Reader templateReader) implements WebViewTemplateComponentSource {} + +} diff --git a/web-views/src/main/java/groowt/view/web/analysis/AnalysisError.java b/web-views/src/main/java/groowt/view/web/analysis/AnalysisError.java new file mode 100644 index 0000000..9672ab6 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/analysis/AnalysisError.java @@ -0,0 +1,6 @@ +package groowt.view.web.analysis; + +public interface AnalysisError { + T subject(); + String message(); +} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/analysis/Analyzer.java b/web-views/src/main/java/groowt/view/web/analysis/Analyzer.java new file mode 100644 index 0000000..c6710f6 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/analysis/Analyzer.java @@ -0,0 +1,7 @@ +package groowt.view.web.analysis; + +import java.util.List; + +public sealed interface Analyzer permits ParseTreeAnalyzer, AstAnalyzer { + List analyze(T t); +} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/analysis/AstAnalyzer.java b/web-views/src/main/java/groowt/view/web/analysis/AstAnalyzer.java new file mode 100644 index 0000000..b4b44fa --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/analysis/AstAnalyzer.java @@ -0,0 +1,6 @@ +package groowt.view.web.analysis; + +import groowt.view.web.ast.node.Node; + +@FunctionalInterface +public non-sealed interface AstAnalyzer> extends Analyzer {} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/analysis/AstError.java b/web-views/src/main/java/groowt/view/web/analysis/AstError.java new file mode 100644 index 0000000..7f0f748 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/analysis/AstError.java @@ -0,0 +1,5 @@ +package groowt.view.web.analysis; + +import groowt.view.web.ast.node.Node; + +public interface AstError extends AnalysisError {} diff --git a/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeAnalyzer.java b/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeAnalyzer.java new file mode 100644 index 0000000..72ad7e3 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeAnalyzer.java @@ -0,0 +1,16 @@ +package groowt.view.web.analysis; + +import groowt.view.web.antlr.WebViewComponentsParser.ComponentWithChildrenContext; +import org.antlr.v4.runtime.tree.ParseTree; + +import java.util.List; + +public final class MismatchedComponentTypeAnalyzer + implements ParseTreeAnalyzer { + + @Override + public List analyze(ParseTree parseTree) { + return MismatchedComponentTypeErrorAnalyzerKt.check(parseTree); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeError.java b/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeError.java new file mode 100644 index 0000000..4fb2313 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeError.java @@ -0,0 +1,6 @@ +package groowt.view.web.analysis; + +import groowt.view.web.antlr.WebViewComponentsParser.ComponentWithChildrenContext; + +public record MismatchedComponentTypeError(ComponentWithChildrenContext subject, String message) + implements ParseTreeAnalysisError {} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeErrorAnalyzer.kt b/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeErrorAnalyzer.kt new file mode 100644 index 0000000..5984c42 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/analysis/MismatchedComponentTypeErrorAnalyzer.kt @@ -0,0 +1,58 @@ +package groowt.view.web.analysis + +import groowt.view.web.antlr.WebViewComponentsParser.ComponentTypeContext +import groowt.view.web.antlr.WebViewComponentsParser.ComponentWithChildrenContext +import groowt.view.web.util.SourcePosition +import org.antlr.v4.runtime.ParserRuleContext +import org.antlr.v4.runtime.Token +import org.antlr.v4.runtime.tree.ParseTree +import org.antlr.v4.runtime.tree.TerminalNode + +private fun getIdentifiers( + componentTypeContext: ComponentTypeContext +): List = componentTypeContext.Identifier().map(TerminalNode::getSymbol) + +private fun getErrorMessage( + openType: ComponentTypeContext, + closingType: ComponentTypeContext +) = "The component's opening and closing tags' types must match exactly. " + + "Found ${openType.text} at [${SourcePosition.formatStartOfToken(openType.start)}] " + + "and ${closingType.text} at [${SourcePosition.formatStartOfToken(closingType.start)}]." + +private fun test( + openIdentifiers: List, + closingIdentifiers: List +): Boolean { + if (openIdentifiers.size != closingIdentifiers.size) { + return false + } + openIdentifiers.zip(closingIdentifiers).forEach { (openIdentifier, closingIdentifier) -> + if (!openIdentifier.text.equals(closingIdentifier.text)) { + return false + } + } + return true +} + +private fun doCheck(tree: ParseTree, destination: MutableList) { + if (tree is ParserRuleContext) { + for (child in tree.children) { + doCheck(child, destination) + } + if (tree is ComponentWithChildrenContext) { + val openType: ComponentTypeContext = tree.openComponent().componentArgs().componentType() + val closingType: ComponentTypeContext = tree.closingComponent().componentType() + val openTypeIdentifiers = getIdentifiers(openType) + val closingTypeIdentifiers = getIdentifiers(closingType) + if (!test(openTypeIdentifiers, closingTypeIdentifiers)) { + destination.add(MismatchedComponentTypeError(tree, getErrorMessage(openType, closingType))) + } + } + } +} + +fun check(tree: ParseTree): List { + val result: MutableList = ArrayList() + doCheck(tree, result) + return result +} diff --git a/web-views/src/main/java/groowt/view/web/analysis/ParseTreeAnalysisError.java b/web-views/src/main/java/groowt/view/web/analysis/ParseTreeAnalysisError.java new file mode 100644 index 0000000..b99d1eb --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/analysis/ParseTreeAnalysisError.java @@ -0,0 +1,5 @@ +package groowt.view.web.analysis; + +import org.antlr.v4.runtime.tree.ParseTree; + +public interface ParseTreeAnalysisError extends AnalysisError {} diff --git a/web-views/src/main/java/groowt/view/web/analysis/ParseTreeAnalyzer.java b/web-views/src/main/java/groowt/view/web/analysis/ParseTreeAnalyzer.java new file mode 100644 index 0000000..f3518ba --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/analysis/ParseTreeAnalyzer.java @@ -0,0 +1,7 @@ +package groowt.view.web.analysis; + +import org.antlr.v4.runtime.tree.ParseTree; + +@FunctionalInterface +public non-sealed interface ParseTreeAnalyzer> + extends Analyzer {} diff --git a/web-views/src/main/java/groowt/view/web/analysis/classes/ClassLoaderClassLocator.java b/web-views/src/main/java/groowt/view/web/analysis/classes/ClassLoaderClassLocator.java new file mode 100644 index 0000000..33fff03 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/analysis/classes/ClassLoaderClassLocator.java @@ -0,0 +1,129 @@ +package groowt.view.web.analysis.classes; + +import groovy.lang.GroovyClassLoader; +import org.codehaus.groovy.control.CompilationFailedException; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiPredicate; + +public non-sealed class ClassLoaderClassLocator implements ClassLocator { + + private static final Logger logger = LoggerFactory.getLogger(ClassLoaderClassLocator.class); + + protected sealed interface CachedLocatedClass + permits ClazzCachedLocatedClass, FailedGroovyCachedLocatedClass, CustomCachedLocatedClass {} + + protected record ClazzCachedLocatedClass(Class cached) implements CachedLocatedClass {} + + protected record FailedGroovyCachedLocatedClass( + CompilationFailedException exception) implements CachedLocatedClass {} + + protected non-sealed interface CustomCachedLocatedClass extends CachedLocatedClass { + Class get(); + } + + protected final ClassLoader classLoader; + private final Map cache = new HashMap<>(); + + public ClassLoaderClassLocator(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + public ClassLoaderClassLocator() { + this.classLoader = Thread.currentThread().getContextClassLoader(); + } + + protected final void addToCache(String name, CachedLocatedClass cachedLocatedClass) { + this.cache.put(name, cachedLocatedClass); + } + + protected final boolean cacheHas(String name) { + return this.cache.containsKey(name); + } + + protected final void removeFromCacheIf(BiPredicate predicate) { + final List targets = new ArrayList<>(); + this.cache.forEach((name, cached) -> { + if (predicate.test(name, cached)) { + targets.add(name); + } + }); + targets.forEach(this.cache::remove); + } + + protected final void removeFromCacheIf(Class ofType, BiPredicate predicate) { + this.removeFromCacheIf((name, cached) -> { + if (ofType.isAssignableFrom(cached.getClass())) { + return predicate.test(name, ofType.cast(cached)); + } + return false; + }); + } + + protected final void removeFromCacheByType(Class type) { + this.removeFromCacheIf((name, cached) -> type.isAssignableFrom(cached.getClass())); + } + + protected final Map getFromCacheByType(Class type) { + final Map result = new HashMap<>(); + this.cache.forEach((name, cached) -> { + if (type.isAssignableFrom(cached.getClass())) { + result.put(name, type.cast(cached)); + } + }); + return result; + } + + protected final @Nullable Class loadFromCache(String name) { + final var cachedLocated = this.cache.getOrDefault(name, null); + if (cachedLocated == null) { + return null; + } else { + return switch (cachedLocated) { + case ClazzCachedLocatedClass(var cached) -> cached; + case CustomCachedLocatedClass custom -> custom.get(); + case FailedGroovyCachedLocatedClass(var exception) -> + throw new RuntimeException("Cannot load Groovy class because compilation failed.", exception); + }; + } + } + + protected @Nullable Class searchClassLoader(String name) { + if (classLoader instanceof GroovyClassLoader gcl) { + try { + Class clazz = gcl.loadClass(name, true, true, false); + this.addToCache(name, new ClazzCachedLocatedClass(clazz)); + return clazz; + } catch (ClassNotFoundException ignored) { + // Ignored + } catch (CompilationFailedException cfe) { + logger.warn("Could not compile class: {}", name); + this.addToCache(name, new FailedGroovyCachedLocatedClass(cfe)); + // return null because we don't actually have a class + return null; + } + } else { + try { + Class clazz = classLoader.loadClass(name); + this.addToCache(name, new ClazzCachedLocatedClass(clazz)); + return clazz; + } catch (ClassNotFoundException ignored) {} + } return null; + } + + @Override + public boolean hasClassForFQN(String name) { + return this.cacheHas(name) || this.searchClassLoader(name) != null; + } + + public void clearCache() { + this.cache.clear(); + } + +} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/analysis/classes/ClassLocator.java b/web-views/src/main/java/groowt/view/web/analysis/classes/ClassLocator.java new file mode 100644 index 0000000..c69bfc5 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/analysis/classes/ClassLocator.java @@ -0,0 +1,5 @@ +package groowt.view.web.analysis.classes; + +public sealed interface ClassLocator permits ClassLoaderClassLocator, PreambleAwareClassLocator { + boolean hasClassForFQN(String name); +} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/analysis/classes/PreambleAwareClassLocator.java b/web-views/src/main/java/groowt/view/web/analysis/classes/PreambleAwareClassLocator.java new file mode 100644 index 0000000..d818fd7 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/analysis/classes/PreambleAwareClassLocator.java @@ -0,0 +1,90 @@ +package groowt.view.web.analysis.classes; + +import groovy.lang.GroovyClassLoader; +import groowt.view.web.antlr.MergedGroovyCodeToken; +import groowt.view.web.antlr.WebViewComponentsParser.PreambleContext; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.builder.AstStringCompiler; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public non-sealed class PreambleAwareClassLocator extends ClassLoaderClassLocator implements ClassLocator { + + private final List currentClassNodes = new ArrayList<>(); + private GroovyClassLoader currentGroovyClassLoader; + + public PreambleAwareClassLocator(ClassLoader classLoader) { + super(classLoader); + } + + protected class ClassNodeCachedLocatedClass implements CustomCachedLocatedClass { + private final ClassNode classNode; + private Class lazyLoadedClass; + + public ClassNodeCachedLocatedClass(ClassNode classNode) { + this.classNode = classNode; + } + + @Override + public Class get() { + if (this.lazyLoadedClass == null) { + try { + final File tmp = File.createTempFile("preambleContextAwareClassLocator", "_" + System.currentTimeMillis()); + this.lazyLoadedClass = currentGroovyClassLoader.defineClass(this.classNode, null, tmp.getAbsolutePath()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return this.lazyLoadedClass; + } + } + + public void setCurrentPreamble(PreambleContext preambleContext) { + this.currentGroovyClassLoader = new GroovyClassLoader(this.classLoader); + this.currentClassNodes.clear(); + final MergedGroovyCodeToken groovyCodeToken = (MergedGroovyCodeToken) preambleContext.GroovyCode().getSymbol(); + final String groovyCode = groovyCodeToken.getText(); + final List astNodes = new AstStringCompiler().compile(groovyCode); + astNodes.forEach(groovyASTNode -> { + if (groovyASTNode instanceof ClassNode classNode) { + this.currentClassNodes.add(classNode); + } + }); + } + + private @Nullable ClassNode searchPreambleSimpleName(String simpleName) { + for (final ClassNode classNode : this.currentClassNodes) { + if (classNode.getNameWithoutPackage().equals(simpleName)) { + this.addToCache(classNode.getName(), new ClassNodeCachedLocatedClass(classNode)); + return classNode; + } + } + return null; + } + + @Override + public boolean hasClassForFQN(String name) { + return super.hasClassForFQN(name) || this.searchPreambleSimpleName(name) != null; + } + + private boolean hasSimpleNameInCache(String simpleName) { + final Collection allCached = this.getFromCacheByType(ClassNodeCachedLocatedClass.class).values(); + for (final ClassNodeCachedLocatedClass cached : allCached) { + if (cached.classNode.getNameWithoutPackage().equals(simpleName)) { + return true; + } + } + return false; + } + + public boolean hasClassForSimpleName(String simpleName) { + return this.hasSimpleNameInCache(simpleName) || this.searchPreambleSimpleName(simpleName) != null; + } + +} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/antlr/AbstractWebViewComponentsLexer.java b/web-views/src/main/java/groowt/view/web/antlr/AbstractWebViewComponentsLexer.java new file mode 100644 index 0000000..ef7a012 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/antlr/AbstractWebViewComponentsLexer.java @@ -0,0 +1,276 @@ +package groowt.view.web.antlr; + +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.Lexer; +import org.antlr.v4.runtime.atn.ATN; +import org.antlr.v4.runtime.atn.LexerATNSimulator; +import org.antlr.v4.runtime.atn.PredictionContextCache; +import org.antlr.v4.runtime.dfa.DFA; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.Deque; +import java.util.LinkedList; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static groowt.view.web.antlr.TokenUtil.escapeChars; + +public abstract class AbstractWebViewComponentsLexer extends Lexer { + + protected static final class PositionAdjustingLexerATNSimulator extends LexerATNSimulator { + + public PositionAdjustingLexerATNSimulator( + Lexer recognizer, + ATN atn, + DFA[] decisionToDFA, + PredictionContextCache sharedContextCache + ) { + super(recognizer, atn, decisionToDFA, sharedContextCache); + } + + public void resetAcceptPosition(CharStream input, int index, int line, int charPositionInLine) { + input.seek(index); + this.line = line; + this.charPositionInLine = charPositionInLine; + this.consume(input); + } + + } + + protected sealed interface GStringPathEndSpec permits StringContinueEndSpec, StringClosingEndSpec {} + + protected record StringContinueEndSpec() implements GStringPathEndSpec {} + + protected record StringClosingEndSpec(int type, int popCount) implements GStringPathEndSpec {} + + protected final PairCounter curlies = new SimplePairCounter(this); + protected final PairCounter parentheses = new SimplePairCounter(this); + + private final Logger logger; + + private boolean canPreamble = true; + private boolean inPreamble; + private boolean inConstructor; + + public AbstractWebViewComponentsLexer(CharStream input) { + super(input); + this.logger = LoggerFactory.getLogger(this.getClass()); + } + + @Override + public void reset() { + this.curlies.clear(); + this.parentheses.clear(); + this.canPreamble = true; + this.inPreamble = false; + this.inConstructor = false; + super.reset(); + } + + protected String getModeName(int m) { + return this.getModeNames()[m]; + } + + @Override + public void pushMode(int m) { + if (logger.isDebugEnabled()) { + final var old = this._mode; + super.pushMode(m); + final var delta = this._mode; + logger.debug("pushMode: target {} to {}", this.getModeName(old), this.getModeName(delta)); + } else { + super.pushMode(m); + } + } + + @Override + public int popMode() { + if (logger.isDebugEnabled()) { + final var popped = this._mode; + final var delta = super.popMode(); + logger.debug("popMode: to {} target {}", this.getModeName(popped), this.getModeName(delta)); + return popped; + } else { + return super.popMode(); + } + } + + protected int peekMode() { + return this._mode; + } + + protected int peekMode(int index) { + if (index == 0) { + return this._mode; + } else { + final Deque tempStack = new LinkedList<>(); + Integer result = null; + for (int i = 0; i < index; i++) { + final var cur = this._modeStack.pop(); + tempStack.push(cur); + if (i + 1 == index) { + result = cur; + } + } + if (result == null) { + throw new IllegalStateException("did not find result in peek mode"); + } + for (int i = 0; i < tempStack.size(); i++) { + this._modeStack.push(tempStack.pop()); + } + return result; + } + } + + protected boolean canPreamble() { + return this.canPreamble; + } + + protected void enterPreamble() { + this.inPreamble = true; + this.canPreamble = false; + } + + protected boolean inPreamble() { + return this.inPreamble; + } + + protected void exitPreamble() { + this.inPreamble = false; + } + + protected void enterConstructor() { + this.parentheses.push(this::popMode); + this.parentheses.increment(); + this.inConstructor = true; + } + + protected boolean inConstructor() { + return this.inConstructor; + } + + protected boolean canExitConstructor() { + return this.inConstructor && this.parentheses.getStackSize() == 1 && this.parentheses.isLast(); + } + + protected void exitConstructor() { + this.parentheses.pop(); + this.inConstructor = false; + } + + protected String getNextCharsAsString(int numberOfChars) { + final var b = new StringBuilder(); + for (int i = 1; i <= numberOfChars; i++) { + b.append((char) this._input.LA(i)); + } + return b.toString(); + } + + protected int getCurrentChar() { + return this._input.LA(-1); + } + + protected int getNextChar() { + return this._input.LA(1); + } + + protected boolean isNext(char test) { + return this._input.LA(1) == test; + } + + protected boolean isNext(String test) { + return this.getNextCharsAsString(test.length()).equals(test); + } + + @Override + public final LexerATNSimulator getInterpreter() { + return this._interp; + } + + protected abstract PositionAdjustingLexerATNSimulator getPositionAdjustingInterpreter(); + + protected void rollbackOne() { + this.getPositionAdjustingInterpreter().resetAcceptPosition( + this._input, + this._tokenStartCharIndex - 1, + this._tokenStartLine, + this._tokenStartCharIndex - 1 + ); + } + + // For debugging purposes + protected record CurrentInfo(String currentText, int line, int col, int index, int mode, int[] modeStack) {} + + // For debugging purposes + protected final Function dumpCurrentInfo = currentInfo -> { + return new StringBuilder("CurrentInfo(text: ") + .append(escapeChars(currentInfo.currentText)) + .append(", line: ") + .append(currentInfo.line) + .append(", col: ") + .append(currentInfo.col) + .append(", index: ") + .append(currentInfo.index) + .append(", mode: ") + .append(this.getModeName(currentInfo.mode)) + .append(", modeStack: ") + .append(Arrays.stream(currentInfo.modeStack) + .mapToObj(this::getModeName) + .collect(Collectors.joining(", ", "[", "]")) + ) + .append(")") + .toString(); + }; + + // For debugging purposes + private CurrentInfo getCurrentInfo() { + final LexerATNSimulator interpreter = this.getInterpreter(); + return new CurrentInfo( + this.getText(), + interpreter.getLine(), + interpreter.getCharPositionInLine(), + this.getInputStream().index(), + this._mode, + this._modeStack.toArray() + ); + } + + // For debugging purposes + protected void debugHook(String name) { + logger.debug("hooked: {}", name); + } + + // For debugging purposes + protected void debugHook(String name, Function msgFunction) { + if (logger.isDebugEnabled() && msgFunction != null) { + logger.debug("hooked: {}; msg: {}", name, msgFunction.apply(this.getCurrentInfo())); + } else { + logger.debug("hooked: {}", name); + } + } + + // For debugging purposes + protected T debugHookReturning(String name, T returnValue) { + this.debugHook(name); + return returnValue; + } + + // For debugging purposes + protected T debugHookReturning(String name, Function msgFunction, T returnValue) { + this.debugHook(name, msgFunction); + return returnValue; + } + + // For debugging purposes + protected boolean debugHookSemPred(String name) { + return this.debugHookReturning(name, true); + } + + // For debugging purposes + protected boolean debugHookSemPred(String name, Function msgFunction) { + return this.debugHookReturning(name, msgFunction, true); + } + +} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/antlr/AntlrUtil.java b/web-views/src/main/java/groowt/view/web/antlr/AntlrUtil.java new file mode 100644 index 0000000..26d9fb8 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/antlr/AntlrUtil.java @@ -0,0 +1,76 @@ +package groowt.view.web.antlr; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.ErrorNode; +import org.antlr.v4.runtime.tree.Tree; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public final class AntlrUtil { + + private AntlrUtil() {} + + public static final class ParseErrorCollector { + + private final List nodesWithRecognitionException = new ArrayList<>(); + private final List errorNodes = new ArrayList<>(); + + public void addNodeWithRecognitionException(ParserRuleContext parserRuleContext) { + if (parserRuleContext.exception == null) { + throw new IllegalArgumentException(); + } + this.nodesWithRecognitionException.add(parserRuleContext); + } + + public void addErrorNode(ErrorNode errorNode) { + this.errorNodes.add(errorNode); + } + + public boolean isEmpty() { + return this.nodesWithRecognitionException.isEmpty() && this.errorNodes.isEmpty(); + } + + public int getErrorCount() { + return this.nodesWithRecognitionException.size() + this.errorNodes.size(); + } + + public Collection getAll() { + final Collection all = new ArrayList<>(this.nodesWithRecognitionException); + all.addAll(this.errorNodes); + return all; + } + + public List getNodesWithRecognitionException() { + return this.nodesWithRecognitionException; + } + + public List getErrorNodes() { + return this.errorNodes; + } + + } + + private static void findErrorNodes(Tree tree, ParseErrorCollector parseErrorCollector) { + switch (tree) { + case ParserRuleContext parserRuleContext -> { + if (parserRuleContext.exception != null) { + parseErrorCollector.addNodeWithRecognitionException(parserRuleContext); + } + parserRuleContext.children.forEach(child -> { + findErrorNodes(child, parseErrorCollector); + }); + } + case ErrorNode errorNode -> parseErrorCollector.addErrorNode(errorNode); + default -> {} // ignore + } + } + + public static ParseErrorCollector findErrorNodes(ParserRuleContext ruleContext) { + final var ec = new ParseErrorCollector(); + findErrorNodes(ruleContext, ec); + return ec; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/antlr/GroovyTokenSourceIterable.kt b/web-views/src/main/java/groowt/view/web/antlr/GroovyTokenSourceIterable.kt new file mode 100644 index 0000000..f84a011 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/antlr/GroovyTokenSourceIterable.kt @@ -0,0 +1,35 @@ +package groowt.view.web.antlr + +import org.antlr.v4.runtime.Token +import org.antlr.v4.runtime.TokenSource + +internal sealed interface NextToken { + val token: Token +} + +internal data class GroovyNextToken(override val token: Token) : NextToken + +internal data class NonGroovyNextToken(override val token: Token) : NextToken + +internal class GroovyTokenSourceIterable(private val tokenSource: TokenSource) : Iterable { + override fun iterator() = GroovyTokenSourceIterator(this.tokenSource) +} + +internal class GroovyTokenSourceIterator(private val tokenSource: TokenSource) : Iterator { + + private var done: Boolean = false + + override fun hasNext() = !done + + override fun next(): NextToken { + if (this.done) throw IllegalStateException("Cannot next() when hasNext() == false") + val next = this.tokenSource.nextToken() + if (isGroovyTokenType(next)) { + return GroovyNextToken(next) + } else { + this.done = true + return NonGroovyNextToken(next) + } + } + +} diff --git a/web-views/src/main/java/groowt/view/web/antlr/LexerSemanticPredicates.kt b/web-views/src/main/java/groowt/view/web/antlr/LexerSemanticPredicates.kt new file mode 100644 index 0000000..aeaa277 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/antlr/LexerSemanticPredicates.kt @@ -0,0 +1,83 @@ +@file:JvmName("LexerSemanticPredicates") +package groowt.view.web.antlr + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.lang.invoke.MethodHandles + +private val logger: Logger = LoggerFactory.getLogger( + MethodHandles.lookup().lookupClass() +) + +private fun escapeParams(params: Array): Array { + val escapedParams: Array = Array(params.size) { null } + params.forEachIndexed { index, param -> + escapedParams[index] = when (param) { + is Char -> escapeChar(param) + is CharArray -> escapeChars(param) + is String -> escapeChars(param) + else -> param + } + } + return escapedParams +} + +private fun logDebug(msg: String, vararg params: Any?) { + if (logger.isDebugEnabled) { + logger.debug(msg, *escapeParams(params)) + } +} + +fun isAnyOf(subject: Char, vararg tests: Char): Boolean { + var result = false + for (test in tests) { + if (subject == test) { + result = true + break + } + } + logDebug ("subject: {}, tests: {}, result: {}", subject, tests, result) + return result +} + +fun isAnyOf(subject: Int, vararg tests: Char): Boolean = isAnyOf(subject.toChar(), *tests) + +/** + * Contract, starting with current character (read as regex): + * - `'[^'] -> true` + * - `''[^'] -> true` + * - `''' -> false` + */ +fun canFollowJStringOpening(nextTwo: String): Boolean { + val result = nextTwo[0] != '\'' || nextTwo[1] != '\'' + logDebug ("nextTwo: {}, result: {}", nextTwo, result) + return result +} + +/** + * Contract, starting with current character (read as regex): + * - `"[^"] -> true` + * - `""[^"] -> true` + * - `""" -> false}` + */ +fun canFollowGStringOpening(nextTwo: String): Boolean { + val result = nextTwo[0] != '"' || nextTwo[1] != '"' + logDebug ("nextTwo: {}, result: {}", nextTwo, result) + return result +} + +fun isIdentifierStartChar(c: Char): Boolean = Character.isJavaIdentifierStart(c) + +fun isIdentifierStartChar(subject: Int) = isIdentifierStartChar(subject.toChar()) + +fun isIdentifierChar(c: Char): Boolean = Character.isJavaIdentifierPart(c) + +fun isIdentifierChar(subject: Int) = isIdentifierChar(subject.toChar()) + +fun isGStringIdentifierStartChar(c: Char): Boolean = c != '$' && isIdentifierStartChar(c) + +fun isGStringIdentifierStartChar(subject: Int) = isGStringIdentifierChar(subject.toChar()) + +fun isGStringIdentifierChar(c: Char): Boolean = c != '$' && isIdentifierChar(c) + +fun isGStringIdentifierChar(subject: Int) = isGStringIdentifierChar(subject.toChar()) diff --git a/web-views/src/main/java/groowt/view/web/antlr/LexerUtil.kt b/web-views/src/main/java/groowt/view/web/antlr/LexerUtil.kt new file mode 100644 index 0000000..8da23a6 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/antlr/LexerUtil.kt @@ -0,0 +1,7 @@ +@file:JvmName("LexerUtil") +package groowt.view.web.antlr + +import org.antlr.v4.runtime.CharStream +import org.antlr.v4.runtime.Token + +fun runLexerAllTokens(input: CharStream): List = WebViewComponentsLexer(input).allTokens diff --git a/web-views/src/main/java/groowt/view/web/antlr/MergedGroovyCodeToken.kt b/web-views/src/main/java/groowt/view/web/antlr/MergedGroovyCodeToken.kt new file mode 100644 index 0000000..92dd803 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/antlr/MergedGroovyCodeToken.kt @@ -0,0 +1,48 @@ +package groowt.view.web.antlr + +import groowt.view.web.antlr.WebViewComponentsLexer.DEFAULT_TOKEN_CHANNEL +import groowt.view.web.antlr.WebViewComponentsLexer.GroovyCode +import org.antlr.v4.runtime.CharStream +import org.antlr.v4.runtime.Token +import org.antlr.v4.runtime.TokenSource + +open class MergedGroovyCodeToken( + private val originals: List, private + val index: Int, private + val tokenSource: TokenSource, private + val inputStream: CharStream +) : Token { + + private val myText: String by lazy { this.originals.joinToString("", transform = Token::getText) } + + private val myLine: Int by lazy { this.originals.first().line } + + private val myCharPositionInLine: Int by lazy { this.originals.first().charPositionInLine } + + private val myStartIndex: Int by lazy { this.originals.first().startIndex } + + private val myStopIndex: Int by lazy { this.originals.last().stopIndex } + + fun getOriginals() = this.originals + + override fun getText() = this.myText + + override fun getType() = GroovyCode + + override fun getLine() = this.myLine + + override fun getCharPositionInLine() = this.myCharPositionInLine + + override fun getChannel() = DEFAULT_TOKEN_CHANNEL + + override fun getTokenIndex() = this.index + + override fun getStartIndex() = this.myStartIndex + + override fun getStopIndex() = this.myStopIndex + + override fun getTokenSource() = this.tokenSource + + override fun getInputStream() = this.inputStream + +} diff --git a/web-views/src/main/java/groowt/view/web/antlr/PairCounter.java b/web-views/src/main/java/groowt/view/web/antlr/PairCounter.java new file mode 100644 index 0000000..fbf7be0 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/antlr/PairCounter.java @@ -0,0 +1,21 @@ +package groowt.view.web.antlr; + +public interface PairCounter { + + @FunctionalInterface + interface OnPop { + void after(); + } + + void push(); + void push(OnPop onPop); + void pop(); + void increment(); + void decrement(); + boolean isCounting(); + boolean isLast(); + int getCurrentCount(); + int getStackSize(); + void clear(); + +} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/antlr/ParserUtil.kt b/web-views/src/main/java/groowt/view/web/antlr/ParserUtil.kt new file mode 100644 index 0000000..3ff54ed --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/antlr/ParserUtil.kt @@ -0,0 +1,167 @@ +@file:JvmName("ParserUtil") +package groowt.view.web.antlr + +import groowt.view.web.antlr.WebViewComponentsParser.CompilationUnitContext +import org.antlr.v4.runtime.* +import org.antlr.v4.runtime.tree.ErrorNode +import org.antlr.v4.runtime.tree.ParseTree +import org.antlr.v4.runtime.tree.TerminalNode +import org.antlr.v4.runtime.tree.Tree +import org.fusesource.jansi.Ansi.ansi +import java.io.File +import java.io.Reader +import java.util.function.BiConsumer +import java.util.function.Consumer +import java.util.function.Function + +data class CompilationUnitParseResult( + val lexer: WebViewComponentsLexer, + val tokenStream: WebViewComponentsTokenStream, + val parser: WebViewComponentsParser, + val compilationUnitContext: CompilationUnitContext +) + +fun parseCompilationUnit(file: File): CompilationUnitParseResult = + parseCompilationUnit(CharStreams.fromFileName(file.toString())) + +fun parseCompilationUnit(source: String): CompilationUnitParseResult = + parseCompilationUnit(CharStreams.fromString(source)) + +fun parseCompilationUnit(reader: Reader): CompilationUnitParseResult = + parseCompilationUnit(CharStreams.fromReader(reader)) + +fun parseCompilationUnit(charStream: CharStream): CompilationUnitParseResult { + val lexer = WebViewComponentsLexer(charStream) + val tokenStream = WebViewComponentsTokenStream(lexer) + val parser = WebViewComponentsParser(tokenStream) + val cu = parser.compilationUnit() + return CompilationUnitParseResult(lexer, tokenStream, parser, cu) +} + +fun parseCompilationUnit(tokenStream: TokenStream): CompilationUnitContext = + parse(tokenStream, WebViewComponentsParser::compilationUnit) + +fun parseCompilationUnit( + tokenStream: TokenStream, + onResult: BiConsumer +) = parse(tokenStream, WebViewComponentsParser::compilationUnit, onResult) + +fun parse(tokenStream: TokenStream, resultFunction: Function): C = + resultFunction.apply(WebViewComponentsParser(tokenStream)) + +fun parse( + tokenStream: TokenStream, + resultFunction: Function, + onResult: BiConsumer +) { + val parser = WebViewComponentsParser(tokenStream) + val c = resultFunction.apply(parser) + onResult.accept(c, parser) +} + +private fun formatForError(parser: Parser, tree: ParseTree): String { + val name: String = when (tree) { + is ParserRuleContext -> parser.ruleNames[tree.ruleIndex] + is TerminalNode -> parser.vocabulary.getDisplayName(tree.symbol.type) + else -> throw IllegalArgumentException("Unable to determine name for ParseTree: $tree") + } + val (line, col) = when (tree) { + is ParserRuleContext -> Pair(tree.start.line, tree.start.charPositionInLine) + is TerminalNode -> Pair(tree.symbol.line, tree.symbol.charPositionInLine) + else -> throw IllegalArgumentException("Unable to determine line/col for ParseTree: $tree") + } + return "$name($line,$col)" +} + +fun formatTree(parser: Parser, tree: Tree, colors: Boolean, consumer: Consumer): Unit = + consumer.accept(formatTree(parser, tree, colors).toString()) + +fun formatTree(parser: Parser, tree: Tree, colors: Boolean = true): String = + doFormatTree(parser, tree, colors, 0, " ", StringBuilder()).toString() + +fun formatTree(parser: Parser, tree: Tree, colors: Boolean, indentTimes: Int): String = + doFormatTree(parser, tree, colors, indentTimes, " ", StringBuilder()).toString() + +fun formatTree(parser: Parser, tree: Tree, colors: Boolean, indentTimes: Int, indentText: String): String = + doFormatTree(parser, tree, colors, indentTimes, indentText, StringBuilder()).toString() + +fun formatTree( + parser: Parser, + tree: Tree, + colors: Boolean, + indentTimes: Int, + indentText: String, + sb: StringBuilder +): String = doFormatTree(parser, tree, colors, indentTimes, indentText, sb).toString() + +private fun formatBasicInfo(parser: Parser, tree: Tree, sb: StringBuilder) { + when (tree) { + is ParserRuleContext -> { + sb.append(parser.ruleNames[tree.ruleIndex]) + .append( + "[${tree.start.line},${tree.start.charPositionInLine + 1}.." + + "${tree.stop.line},${tree.stop.charPositionInLine + 1}]" + ) + } + + is TerminalNode -> { + sb.append(parser.vocabulary.getDisplayName(tree.symbol.type)) + .append("[${tree.symbol.line},${tree.symbol.charPositionInLine + 1}]") + } + } +} + +private fun doFormatTree( + parser: Parser, + tree: Tree, + colors: Boolean, + indentTimes: Int, + indentText: String, + sb: StringBuilder +): StringBuilder { + sb.repeat(indentText, indentTimes) + if (tree is RuleContext) { + var e: RecognitionException? = null + if (tree is ParserRuleContext && tree.exception != null) { + e = tree.exception + if (colors) { + sb.append(ansi().fgRed()) + } + } + formatBasicInfo (parser, tree, sb) + if (e != null) { + sb.append(": Exception: ${e.javaClass.simpleName}(${escapeChars(tree.text)})") + if (colors) { + sb.append(ansi().reset()) + } + } + sb . append ("\n") + var i = 0 + while (i < tree.childCount) { + doFormatTree(parser, tree.getChild(i), colors, indentTimes + 1, indentText, sb) + i++ + } + } else if (tree is TerminalNode) { + if (colors && tree is ErrorNode) { + sb.append(ansi().fgRed()) + } + formatBasicInfo (parser, tree, sb) + sb.append("(${escapeChars(tree.text)})") + if (colors) { + sb.append( + ansi().reset() + ) + } + sb.append ("\n") + } else { + if (colors) { + sb.append(ansi().fgYellow()) + } + sb.append ("Unknown node: ${tree.toStringTree()}") + if (colors) { + sb.append(ansi().reset()) + } + sb.append ("\n") + } + return sb +} diff --git a/web-views/src/main/java/groowt/view/web/antlr/SimplePairCounter.kt b/web-views/src/main/java/groowt/view/web/antlr/SimplePairCounter.kt new file mode 100644 index 0000000..e63496d --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/antlr/SimplePairCounter.kt @@ -0,0 +1,83 @@ +package groowt.view.web.antlr + +import org.antlr.v4.runtime.Lexer +import org.antlr.v4.runtime.misc.IntegerStack +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.* + +class SimplePairCounter(private val lexer: Lexer) : PairCounter { + + companion object { + private val logger: Logger = LoggerFactory.getLogger(SimplePairCounter::class.java) + } + + private class StackEntry(val modes: IntegerStack, val onPop: PairCounter.OnPop?) { + private var count: Int = 0 + fun get() = this.count + fun increment(): Int = ++this.count + fun decrement(): Int = --this.count + } + + private val stack: Deque = LinkedList() + + private fun currentEntry(): StackEntry { + if (this.stack.isEmpty()) { + throw IllegalStateException("Cannot currentEntry() when stack is empty!") + } + return this.stack.peek() + } + + private fun getLexerModes() = IntegerStack(this.lexer._modeStack).also { it.push(this.lexer._mode) } + + override fun push() { + this.stack.push(StackEntry(this.getLexerModes(), null)) + } + + override fun push(onPop: PairCounter.OnPop?) { + this.stack.push(StackEntry(this.getLexerModes(), onPop)) + } + + override fun pop() { + val entry = this.stack.pop() + if (entry.onPop != null) { + entry.onPop.after() + } + if (logger.isWarnEnabled) { + val currentModes = this.getLexerModes() + if (entry.modes != currentModes) { + logger.warn( + "popped counter entry's modes differ target current modes; " + + "old modes: {}, new modes: {}; " + + "did you pop the counter target a different mode?", + entry.modes, + currentModes + ) + } + } + } + + override fun increment() { + this.currentEntry().increment() + } + + override fun decrement() { + val newCount = this.currentEntry().decrement() + if (newCount < 1) { + throw IllegalStateException("Should never decrement below 1!") + } + } + + override fun isCounting() = this.stack.isNotEmpty() + + override fun isLast() = this.currentCount == 1 + + override fun getCurrentCount() = this.currentEntry().get() + + override fun getStackSize() = this.stack.size + + override fun clear() { + this.stack.clear() + } + +} diff --git a/web-views/src/main/java/groowt/view/web/antlr/TokenList.java b/web-views/src/main/java/groowt/view/web/antlr/TokenList.java new file mode 100644 index 0000000..0fb52fe --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/antlr/TokenList.java @@ -0,0 +1,126 @@ +package groowt.view.web.antlr; + +import groowt.view.web.util.RangeIterator; +import groowt.view.web.util.TokenRange; +import org.antlr.v4.runtime.Token; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +public final class TokenList extends ArrayList { + + public TokenList(Collection c) { + super(c); + } + + public TokenList(WebViewComponentsTokenStream tokenStream) { + super(tokenStream.getAllTokens()); + } + + private @Nullable Token getNullable(int index) { + if (index < this.size()) { + return this.get(index); + } else { + return null; + } + } + + public List getRange(TokenRange range) { + final List result = new ArrayList<>(); + final RangeIterator iter = range.rangeIterator(this::getNullable); + while (iter.hasNext()) { + result.add(iter.next()); + } + return result; + } + + public MergedGroovyCodeToken getGroovyToken(int index) { + return (MergedGroovyCodeToken) this.get(index); + } + + @Override + public boolean add(Token token) { + throw new UnsupportedOperationException("TokenList is immutable."); + } + + @Override + public void add(int index, Token element) { + throw new UnsupportedOperationException("TokenList is immutable."); + } + + @Override + public void addFirst(Token element) { + throw new UnsupportedOperationException("TokenList is immutable."); + } + + @Override + public void addLast(Token element) { + throw new UnsupportedOperationException("TokenList is immutable."); + } + + @Override + public Token remove(int index) { + throw new UnsupportedOperationException("TokenList is immutable."); + } + + @Override + public Token removeFirst() { + throw new UnsupportedOperationException("TokenList is immutable."); + } + + @Override + public Token removeLast() { + throw new UnsupportedOperationException("TokenList is immutable."); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("TokenList is immutable."); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException("TokenList is immutable."); + } + + @Override + public boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException("TokenList is immutable."); + } + + @Override + protected void removeRange(int fromIndex, int toIndex) { + throw new UnsupportedOperationException("TokenList is immutable."); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException("TokenList is immutable."); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException("TokenList is immutable."); + } + + @Override + public boolean removeIf(Predicate predicate) { + throw new UnsupportedOperationException("TokenList is immutable."); + } + + @Override + public void replaceAll(UnaryOperator operator) { + throw new UnsupportedOperationException("TokenList is immutable."); + } + + @Override + public void sort(Comparator comparator) { + throw new UnsupportedOperationException("TokenList is immutable."); + } + +} 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 new file mode 100644 index 0000000..538d3bf --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/antlr/TokenUtil.kt @@ -0,0 +1,103 @@ +@file:JvmName("TokenUtil") +package groowt.view.web.antlr + +import groowt.view.web.antlr.WebViewComponentsLexer.GStringParts +import groowt.view.web.antlr.WebViewComponentsLexer.GroovyTokens +import org.antlr.v4.runtime.Token + +fun isGroovyTokenType(token: Token) = isGroovyTokenType(token.type) + +fun isGroovyTokenType(type: Int): Boolean = type in GroovyTokens + +fun isGStringPart (token: Token) = isGStringPart(token.type) + +fun isGStringPart(type: Int): Boolean = type in GStringParts + +fun getTokenName (token: Token) = getTokenName(token.type) + +fun getTokenName(type: Int): String = WebViewComponentsLexer.VOCABULARY.getDisplayName(type) + +fun formatToken(token: Token): String = formatToken(token, ::formatTokenText) + +@FunctionalInterface +fun interface TokenTextFormatter { + fun format(text: String): String +} + +fun formatToken(token: Token, textFormatter: TokenTextFormatter): String = + "${getTokenName(token)}[${token.line},${token.charPositionInLine}](${textFormatter.format(token.text)})" + +fun shortFormatToken(token: Token): String = + "${getTokenName(token)}[${token.line},${token.charPositionInLine},${token.text.length}]" + +fun formatTokenText(text: String): String = excerptTokenParts(escapeTokenPartsToList(text)) + +fun excerptTokenParts( + parts: List, + startLength: Int = 30, + endLength: Int = 7, + separator: String = "..." +): String { + val length = getTokenTextPartsLength(parts) + if (length > 40) { + val start = joinTokenPartsUntil(parts, startLength) + val end = joinTokenPartsUntilFromEnd(parts, endLength) + return start + separator + end + } else { + return joinTokenParts(parts) + } +} + +private fun joinTokenParts(parts: List): String = + parts.fold("") { acc, s -> acc + s } + +private fun getTokenTextPartsLength(parts: List): Int = + parts.fold(0) { acc, s -> acc + s.length } + +private fun joinTokenPartsUntil(parts: List, limit: Int): String { + val b = StringBuilder() + val iter = parts.iterator() + while (b.length < limit && iter.hasNext()) { + val next = iter.next() + val remaining = limit - b.length + if (next.length <= remaining) { + b.append(next) + } else { + b.append(next.substring(0, remaining)) + } + } + return b.toString() +} + +private fun joinTokenPartsUntilFromEnd(parts: List, limit: Int): String { + val reversedTemp: MutableList = ArrayList() + val iter = parts.reversed().iterator() + var length = 0 + while (length < limit && iter.hasNext()) { + val next = iter.next() + val remaining = limit - length + if (next.length <= remaining) { + reversedTemp.addLast(next.reversed()) + length += next.length + } else { + val excerpt = next.reversed() + .substring(0, remaining) + reversedTemp.addLast(excerpt) + length += excerpt.length + } + } + return reversedTemp.reversed().fold("") { acc, reversedPart -> acc + reversedPart.reversed() } +} + +fun escapeChars(cs: CharArray): Array = Array(cs.size) { escapeChar(cs[it]) } + +fun escapeChars(s: String): String = escapeTokenPartsToList(s).joinToString("") + +fun escapeChar(c: Char): String = when (c) { + '\n' -> "\\n" + '\r' -> "\\r" + '\t' -> "\\t" + else -> c.toString() +} + +private fun escapeTokenPartsToList(text: String): List = text.map(::escapeChar) diff --git a/web-views/src/main/java/groowt/view/web/antlr/WebViewComponentsLexer.java b/web-views/src/main/java/groowt/view/web/antlr/WebViewComponentsLexer.java new file mode 100644 index 0000000..3a177f6 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/antlr/WebViewComponentsLexer.java @@ -0,0 +1,17 @@ +package groowt.view.web.antlr; + +import org.antlr.v4.runtime.CharStream; + +public class WebViewComponentsLexer extends WebViewComponentsLexerBase { + + public WebViewComponentsLexer(CharStream input) { + super(input); + this._interp = new PositionAdjustingLexerATNSimulator(this, _ATN, _decisionToDFA, _sharedContextCache); + } + + @Override + protected PositionAdjustingLexerATNSimulator getPositionAdjustingInterpreter() { + return (PositionAdjustingLexerATNSimulator) this.getInterpreter(); + } + +} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/antlr/WebViewComponentsTokenStream.kt b/web-views/src/main/java/groowt/view/web/antlr/WebViewComponentsTokenStream.kt new file mode 100644 index 0000000..17ec407 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/antlr/WebViewComponentsTokenStream.kt @@ -0,0 +1,204 @@ +package groowt.view.web.antlr + +import org.antlr.v4.runtime.* +import org.antlr.v4.runtime.misc.Interval + +private operator fun Interval.component1(): Int = this.a + +private operator fun Interval.component2(): Int = this.b + +class WebViewComponentsTokenStream (private val tokenSource: TokenSource) : TokenStream { + + private val tokens: MutableList = ArrayList() + + private var initialized = false + private var currentIndex = 0 + + private fun atEOF(): Boolean { + if (this.tokens.isEmpty()) { + throw IllegalStateException("Must initialize first!") + } else if (this.currentIndex >= this.tokens.size) { + throw IllegalStateException( + "this.currentIndex is greater than or equal to this.tokens.size: " + + "currentIndex: ${this.currentIndex}, tokens.size: ${this.tokens.size}" + ) + } + return this.tokens[this.currentIndex].type == Token.EOF + } + + private fun hasEOF() = this.tokens.isNotEmpty() && this.tokens.last().type == Token.EOF + + private fun initialize() { + if (!this.initialized) { + this.currentIndex = 0 + val syncResult = this.sync(this.currentIndex) + if (!syncResult) { + throw IllegalStateException("Could not sync during initializing!") + } + this.initialized = true + } + } + + private fun sync(index: Int): Boolean { + val needed = index - tokens.size + 1 // how many do we need? + if (needed > 0) { + val fetchedCount = this.fetch(needed) + return fetchedCount >= needed + } + return true + } + + private fun syncNextAndIncrementIndex() { + if (this.sync(this.currentIndex + 1)) { + this.currentIndex++ + } + } + + private fun fetch(count: Int): Int { + if (hasEOF()) { + return 0 + } + var fetched = 0 + while (fetched < count) { + val (groovyTokens, followingToken) = this.fetchGroovyTokens() + if (groovyTokens.isNotEmpty()) { + fetched++ + this.tokens.add( + MergedGroovyCodeToken(groovyTokens, this.tokens.size, this.tokenSource, this.tokenSource.inputStream) + ) + } + if (followingToken != null) { + fetched++ + if (followingToken is WritableToken) { + followingToken.tokenIndex = this.tokens.size + } + this.tokens.add(followingToken) + if (followingToken.type == Token.EOF) { + break + } + } + } + return fetched + } + + private fun fetchGroovyTokens(): Pair, Token?> { + val groovyTokens: MutableList = ArrayList() + for (next in GroovyTokenSourceIterable(this.tokenSource)) { + when (next) { + is GroovyNextToken -> groovyTokens.add(next.token) + is NonGroovyNextToken -> return Pair(groovyTokens, next.token) + } + } + return Pair(groovyTokens, null) + } + + override fun consume() { + if (this.initialized && (this.atEOF() || this.LA(1) == Token.EOF)) { + throw IllegalStateException("Cannot consume when at EOF!") + } + this.syncNextAndIncrementIndex() + } + + override fun LA(i: Int): Int = when (i) { + 0 -> throw IllegalArgumentException("Cannot LA to index 0.") + else -> this.LT(i)!!.type + } + + override fun mark() = 0 + + override fun release(marker: Int) = Unit // Do nothing + + override fun index() = this.currentIndex + + override fun seek(index: Int) { + this.initialize() + this.currentIndex = index + } + + override fun size(): Int { + throw UnsupportedOperationException("Cannot get size() on this stream!") + } + + override fun getSourceName(): String = this.tokenSource.sourceName + + @Suppress("FunctionName") + private fun LB(target: Int): Token? { + val delta = this.currentIndex - target + return when { + delta < 0 -> null + else -> this.tokens.get(delta) + } + } + + override fun LT(target: Int): Token? { + this.initialize() + return when { + target == 0 -> null + target < 0 -> this.LB(target * -1) + else -> { + val index = this.currentIndex + target - 1 + this.sync(index) + if (index >= tokens.size) { + return this.tokens.last() + } else { + return this.tokens[index] + } + } + } + } + + override fun get(index: Int): Token { + if (index < 0 || index >= tokens.size) { + throw IndexOutOfBoundsException("Token index $index is out of bounds 0..${this.tokens.size}!") + } + return this.tokens[index] + } + + override fun getTokenSource() = this.tokenSource + + override fun getText(interval: Interval): String { + val (start, stop) = interval + if (start < 0 || stop < 0) { + return "" + } + this.sync(stop) + val target = if (stop >= this.tokens.size) this.tokens.size - 1 else stop + val b = StringBuilder() + for (i in start..target) { + val token = this.tokens[i] + if (token.type != Token.EOF) { + b.append(token.text) + } + } + return b.toString() + } + + override fun getText(): String = this.getText(Interval.of(0, this.size() - 1)) + + override fun getText(ctx: RuleContext): String = this.getText(ctx.sourceInterval) + + override fun getText(start: Token?, stop: Token?): String { + return if (start != null && stop != null) { + this.getText(Interval.of(start.tokenIndex, stop.tokenIndex)) + } else { + "" + } + } + + fun getAllTokens(): List { + this.fill() + return this.tokens + } + + private fun fill() { + val oldIndex = this.currentIndex + this.initialize() + if (!this.hasEOF()) { + while (!this.atEOF()) { + this.syncNextAndIncrementIndex() + } + } + this.seek(oldIndex) + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/AstBuilder.java b/web-views/src/main/java/groowt/view/web/ast/AstBuilder.java new file mode 100644 index 0000000..427879d --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/AstBuilder.java @@ -0,0 +1,16 @@ +package groowt.view.web.ast; + +import groowt.view.web.antlr.WebViewComponentsParser.CompilationUnitContext; +import groowt.view.web.ast.node.CompilationUnitNode; +import groowt.view.web.ast.node.Node; +import org.antlr.v4.runtime.ParserRuleContext; + +public interface AstBuilder { + + Node build(ParserRuleContext ruleContext); + + default CompilationUnitNode buildCompilationUnit(CompilationUnitContext compilationUnitContext) { + return (CompilationUnitNode) this.build(compilationUnitContext); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/DefaultAstBuilder.java b/web-views/src/main/java/groowt/view/web/ast/DefaultAstBuilder.java new file mode 100644 index 0000000..f25e880 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/DefaultAstBuilder.java @@ -0,0 +1,19 @@ +package groowt.view.web.ast; + +import groowt.view.web.ast.node.Node; +import org.antlr.v4.runtime.ParserRuleContext; + +public class DefaultAstBuilder implements AstBuilder { + + private final NodeFactory nodeFactory; + + public DefaultAstBuilder(NodeFactory nodeFactory) { + this.nodeFactory = nodeFactory; + } + + @Override + public Node build(ParserRuleContext ruleContext) { + return ruleContext.accept(new DefaultAstBuilderVisitor(this.nodeFactory)); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/DefaultAstBuilderVisitor.java b/web-views/src/main/java/groowt/view/web/ast/DefaultAstBuilderVisitor.java new file mode 100644 index 0000000..c0ba4e0 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/DefaultAstBuilderVisitor.java @@ -0,0 +1,392 @@ +package groowt.view.web.ast; + +import groowt.view.web.antlr.MergedGroovyCodeToken; +import groowt.view.web.antlr.TokenUtil; +import groowt.view.web.antlr.WebViewComponentsParser; +import groowt.view.web.antlr.WebViewComponentsParserBaseVisitor; +import groowt.view.web.ast.node.*; +import groowt.view.web.util.TokenRange; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.ErrorNode; +import org.antlr.v4.runtime.tree.RuleNode; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +public class DefaultAstBuilderVisitor extends WebViewComponentsParserBaseVisitor { + + protected static boolean isNotBlankNotEmpty(@NotNull String subject) { + return !subject.isEmpty() && !subject.isBlank(); + } + + private final NodeFactory nodeFactory; + + public DefaultAstBuilderVisitor(NodeFactory nodeFactory) { + this.nodeFactory = nodeFactory; + } + + @Override + public Node visitChildren(RuleNode node) { + throw new UnsupportedOperationException(); + } + + @Override + protected Node defaultResult() { + throw new UnsupportedOperationException(); + } + + @Override + protected Node aggregateResult(Node aggregate, Node nextResult) { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean shouldVisitNextChild(RuleNode node, Node currentResult) { + throw new UnsupportedOperationException(); + } + + protected @Nullable Node getSingle(@Nullable ParserRuleContext ctx) { + return ctx != null ? ctx.accept(this) : null; + } + + protected @Nullable R getSingleAs(@Nullable ParserRuleContext ctx, @NotNull Class type) { + final Node result = this.getSingle(ctx); + return result != null ? type.cast(result) : null; + } + + protected @NotNull Node getSingleNonNull(@NotNull ParserRuleContext ctx) { + return Objects.requireNonNull(Objects.requireNonNull(ctx).accept(this)); + } + + protected @NotNull R getSingleAsNonNull(@NotNull ParserRuleContext ctx, @NotNull Class type) { + return type.cast(this.getSingleNonNull(ctx)); + } + + protected Node getSingleChild(ParserRuleContext ctx) { + return ctx.getChild(0).accept(this); + } + + protected R getSingleChildAs(ParserRuleContext ctx, Class type) { + return type.cast(this.getSingleChild(ctx)); + } + + protected TokenRange getTokenRange(ParserRuleContext ctx) { + return TokenRange.of(ctx.start, ctx.stop); + } + + protected TerminalNode getSingleChildTerminalNode(WebViewComponentsParser.JStringBodyTextContext ctx) { + return ctx.getChild(TerminalNode.class, 0); + } + + @Override + public Node visitCompilationUnit(WebViewComponentsParser.CompilationUnitContext ctx) { + final PreambleNode preamble = this.getSingleAs(ctx.preamble(), PreambleNode.class); + final BodyNode body = this.getSingleAs(ctx.body(), BodyNode.class); + return this.nodeFactory.compilationUnitNode(this.getTokenRange(ctx), preamble, body); + } + + @Override + public @Nullable Node visitPreamble(WebViewComponentsParser.PreambleContext ctx) { + final @Nullable TerminalNode groovyCode = ctx.GroovyCode(); + if (groovyCode != null && isNotBlankNotEmpty(groovyCode.getText())) { + return this.nodeFactory.preambleNode(this.getTokenRange(ctx), groovyCode.getSymbol().getTokenIndex()); + } else { + return null; + } + } + + @Override + public @Nullable Node visitBody(WebViewComponentsParser.BodyContext ctx) { + final List children = new ArrayList<>(); + for (final var child : ctx.children) { + final BodyChildNode bodyChildNode = (BodyChildNode) child.accept(this); + if (bodyChildNode != null) { + children.add(bodyChildNode); + } + } + if (children.isEmpty()) { + return null; + } else { + return this.nodeFactory.bodyNode(this.getTokenRange(ctx), children); + } + } + + @Override + public Node visitBodyText(WebViewComponentsParser.BodyTextContext ctx) { + return this.getSingleChild(ctx); + } + + @Override + public Node visitGStringBodyText(WebViewComponentsParser.GStringBodyTextContext ctx) { + final List children = new ArrayList<>(); + for (final var child : ctx.children) { + final @Nullable Node childResult = child.accept(this); + if (childResult != null) { + children.add(childResult); + } + } + return this.nodeFactory.gStringBodyTextNode(this.getTokenRange(ctx), children); + } + + @Override + public @Nullable Node visitJStringBodyText(WebViewComponentsParser.JStringBodyTextContext ctx) { + final String text = ctx.getText(); + if (isNotBlankNotEmpty(text)) { + return this.nodeFactory.jStringBodyTextNode( + this.getTokenRange(ctx), + text + ); + } else { + return null; + } + } + + @Override + public Node visitGStringBodyTextGroovyElement(WebViewComponentsParser.GStringBodyTextGroovyElementContext ctx) { + return this.getSingleChild(ctx); + } + + @Override + public Node visitComponent(WebViewComponentsParser.ComponentContext ctx) { + return this.getSingleChild(ctx); + } + + @Override + public Node visitSelfClosingComponent(WebViewComponentsParser.SelfClosingComponentContext ctx) { + return this.nodeFactory.typedComponentNode( + this.getTokenRange(ctx), + this.getSingleAs(ctx.componentArgs(), ComponentArgsNode.class), + null + ); + } + + @Override + public Node visitComponentWithChildren(WebViewComponentsParser.ComponentWithChildrenContext ctx) { + return this.nodeFactory.typedComponentNode( + this.getTokenRange(ctx), + this.getSingleAs(ctx.openComponent().componentArgs(), ComponentArgsNode.class), + this.getSingleAs(ctx.body(), BodyNode.class) + ); + } + + @Override + public Node visitOpenComponent(WebViewComponentsParser.OpenComponentContext ctx) { + throw new UnsupportedOperationException(); + } + + @Override + public Node visitClosingComponent(WebViewComponentsParser.ClosingComponentContext ctx) { + throw new UnsupportedOperationException(); + } + + @Override + public Node visitFragmentComponent(WebViewComponentsParser.FragmentComponentContext ctx) { + return this.nodeFactory.fragmentComponentNode( + this.getTokenRange(ctx), + this.getSingleAs(ctx.body(), BodyNode.class) + ); + } + + @Override + public Node visitComponentArgs(WebViewComponentsParser.ComponentArgsContext ctx) { + final ComponentTypeNode typeNode = this.getSingleAs(ctx.componentType(), ComponentTypeNode.class); + final @Nullable ComponentConstructorNode constructorNode = this.getSingleAs( + ctx.componentConstructor(), + ComponentConstructorNode.class + ); + final List attrNodes = new ArrayList<>(); + for (final var attrCtx : ctx.attr()) { + attrNodes.add(this.getSingleAsNonNull(attrCtx, AttrNode.class)); + } + return this.nodeFactory.componentArgsNode(this.getTokenRange(ctx), typeNode, constructorNode, attrNodes); + } + + private static final Pattern lowercaseLetterPattern = Pattern.compile("\\p{Ll}"); + + protected boolean startsWithLowercaseLetter(String subject) { + if (subject.isEmpty()) { + throw new IllegalArgumentException( + "Cannot test for starting lowercase letter when the subject length is 0; given: " + subject); + } + return lowercaseLetterPattern.matcher(subject.substring(0, 1)).matches(); + } + + @Override + public Node visitComponentType(WebViewComponentsParser.ComponentTypeContext ctx) { + final var identifiers = ctx.Identifier(); + if (identifiers.size() == 1) { + final TerminalNode first = identifiers.getFirst(); + if (startsWithLowercaseLetter(first.getText())) { + return this.nodeFactory.stringComponentTypeNode( + this.getTokenRange(ctx), first.getSymbol().getTokenIndex() + ); + } + } + return this.nodeFactory.classComponentTypeNode(this.getTokenRange(ctx)); + } + + @Override + public @Nullable Node visitComponentConstructor(WebViewComponentsParser.ComponentConstructorContext ctx) { + final TerminalNode groovyCode = ctx.GroovyCode(); + if (groovyCode != null) { + final String rawCode = groovyCode.getText(); + if (isNotBlankNotEmpty(rawCode)) { + return this.nodeFactory.componentConstructorNode( + this.getTokenRange(ctx), + ctx.GroovyCode().getSymbol().getTokenIndex() + ); + } + } + return null; + } + + @Override + public Node visitAttr(WebViewComponentsParser.AttrContext ctx) { + return this.getSingleChild(ctx); + } + + @Override + public Node visitKeyValueAttr(WebViewComponentsParser.KeyValueAttrContext ctx) { + final TerminalNode identifier = ctx.Identifier(); + final KeyNode keyNode = this.nodeFactory.keyNode(TokenRange.of( + identifier.getSymbol(), + ctx.Equals().getSymbol() + ), identifier.getSymbol().getTokenIndex()); + final ValueNode valueNode = (ValueNode) ctx.value().accept(this); + return this.nodeFactory.keyValueAttrNode(this.getTokenRange(ctx), keyNode, valueNode); + } + + @Override + public Node visitBooleanAttr(WebViewComponentsParser.BooleanAttrContext ctx) { + final Token identifierToken = ctx.Identifier().getSymbol(); + final KeyNode keyNode = this.nodeFactory.keyNode( + TokenRange.of(identifierToken), + identifierToken.getTokenIndex() + ); + return this.nodeFactory.booleanValueAttrNode(this.getTokenRange(ctx), keyNode); + } + + @Override + public Node visitValue(WebViewComponentsParser.ValueContext ctx) { + return this.getSingleChild(ctx); + } + + protected static boolean canBeGString(List originalGroovyTokens) { + return originalGroovyTokens.stream().anyMatch(TokenUtil::isGStringPart); + } + + @Override + public Node visitGStringAttrValue(WebViewComponentsParser.GStringAttrValueContext ctx) { + final TerminalNode groovyCode = ctx.GroovyCode(); + final TokenRange ctxTokenRange = this.getTokenRange(ctx); + if (groovyCode != null) { + final MergedGroovyCodeToken groovyCodeToken = (MergedGroovyCodeToken) groovyCode.getSymbol(); + if (canBeGString(groovyCodeToken.getOriginals())) { + // TODO: we need to set the appropriate type: slashy, dollar slashy, etc. + return this.nodeFactory.gStringValueNode(ctxTokenRange, groovyCodeToken.getTokenIndex()); + } else { + return this.nodeFactory.jStringValueNode(ctxTokenRange, groovyCode.getText()); + } + } else { + return this.nodeFactory.jStringValueNode(ctxTokenRange, ""); + } + } + + @Override + public Node visitJStringAttrValue(WebViewComponentsParser.JStringAttrValueContext ctx) { + final TerminalNode groovyCode = ctx.GroovyCode(); + final TokenRange ctxTokenRange = this.getTokenRange(ctx); + if (groovyCode != null) { + return this.nodeFactory.jStringValueNode(ctxTokenRange, groovyCode.getText()); + } else { + return this.nodeFactory.jStringValueNode(ctxTokenRange, ""); + } + } + + @Override + public Node visitClosureAttrValue(WebViewComponentsParser.ClosureAttrValueContext ctx) { + final TerminalNode groovyCode = ctx.GroovyCode(); + final TokenRange ctxTokenRange = this.getTokenRange(ctx); + if (groovyCode != null) { + final String rawCode = groovyCode.getText(); + if (!(rawCode.isEmpty() || rawCode.isBlank())) { + return this.nodeFactory.closureValueNode(ctxTokenRange, groovyCode.getSymbol().getTokenIndex()); + } + } + return this.nodeFactory.emptyClosureValueNode(ctxTokenRange); + } + + @Override + public Node visitComponentAttrValue(WebViewComponentsParser.ComponentAttrValueContext ctx) { + return this.nodeFactory.componentValueNode( + this.getTokenRange(ctx), + (ComponentNode) ctx.component().accept(this) + ); + } + + @Override + public @Nullable Node visitEqualsScriptlet(WebViewComponentsParser.EqualsScriptletContext ctx) { + final TerminalNode groovyCode = ctx.GroovyCode(); + if (groovyCode != null) { + return this.nodeFactory.dollarScriptletNode( + this.getTokenRange(ctx), + ctx.GroovyCode().getSymbol().getTokenIndex() + ); + } else { + return null; + } + } + + @Override + public @Nullable Node visitPlainScriptlet(WebViewComponentsParser.PlainScriptletContext ctx) { + final TerminalNode groovyCode = ctx.GroovyCode(); + if (groovyCode != null) { + return this.nodeFactory.plainScriptletNode(this.getTokenRange(ctx), groovyCode.getSymbol().getTokenIndex()); + } else { + return null; + } + } + + @Override + public @Nullable Node visitDollarScriptlet(WebViewComponentsParser.DollarScriptletContext ctx) { + final TerminalNode groovyCode = ctx.GroovyCode(); + if (groovyCode != null) { + return this.nodeFactory.dollarScriptletNode( + this.getTokenRange(ctx), + groovyCode.getSymbol().getTokenIndex() + ); + } else { + return null; + } + } + + @Override + public @Nullable Node visitDollarReference(WebViewComponentsParser.DollarReferenceContext ctx) { + final TerminalNode groovyCode = ctx.GroovyCode(); + if (groovyCode != null) { + return this.nodeFactory.dollarReferenceNode( + this.getTokenRange(ctx), + groovyCode.getSymbol().getTokenIndex() + ); + } else { + return null; + } + } + + @Override + public Node visitTerminal(TerminalNode node) { + throw new UnsupportedOperationException(); + } + + @Override + public Node visitErrorNode(ErrorNode node) { + throw new IllegalStateException("Found an ErrorNode: " + node); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/DefaultNodeFactory.java b/web-views/src/main/java/groowt/view/web/ast/DefaultNodeFactory.java new file mode 100644 index 0000000..58cdb11 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/DefaultNodeFactory.java @@ -0,0 +1,218 @@ +package groowt.view.web.ast; + +import groowt.util.di.DefaultRegistryObjectFactory; +import groowt.util.di.Registry; +import groowt.util.di.RegistryObjectFactory; +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.extension.*; +import groowt.view.web.ast.node.*; +import groowt.view.web.util.TokenRange; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Set; + +import static groowt.util.di.BindingUtil.*; +import static groowt.util.di.filters.FilterHandlers.getAllowsTypesFilterHandler; +import static groowt.util.di.filters.IterableFilterHandlers.getIterableElementTypesFilterHandler; + +public class DefaultNodeFactory implements NodeFactory { + + public static final Set> NODE_TYPES = Set.of( + CompilationUnitNode.class, + PreambleNode.class, + BodyNode.class, + GStringBodyTextNode.class, + JStringBodyTextNode.class, + TypedComponentNode.class, + FragmentComponentNode.class, + ComponentArgsNode.class, + ClassComponentTypeNode.class, + StringComponentTypeNode.class, + ComponentConstructorNode.class, + KeyValueAttrNode.class, + BooleanValueAttrNode.class, + KeyNode.class, + GStringValueNode.class, + JStringValueNode.class, + ClosureValueNode.class, + ComponentValueNode.class, + PlainScriptletNode.class, + DollarScriptletNode.class, + DollarReferenceNode.class + ); + + protected final RegistryObjectFactory objectFactory; + + public DefaultNodeFactory(TokenList tokenList) { + final var b = this.getRegistryObjectFactoryBuilder(); + b.configureRegistry(r -> { + this.configureDependencies(r, tokenList); + this.configureNodeImplementations(r); + this.configureRegistryExtensions(r); + }); + this.configureFilters(b); + this.objectFactory = b.build(); + } + + protected RegistryObjectFactory.Builder getRegistryObjectFactoryBuilder() { + return DefaultRegistryObjectFactory.Builder.withDefaults(); + } + + protected void configureDependencies(Registry registry, TokenList tokenList) { + registry.bind(RegistryObjectFactory.class, toProvider(() -> this.objectFactory)); + registry.bind(TokenList.class, toSingleton(tokenList)); + registry.bind(NodeExtensionFactory.class, toClass(SimpleNodeExtensionFactory.class)); + registry.bind( + NodeExtensionContainer.class, + toProvider(() -> new SimpleNodeExtensionContainer(this.objectFactory.get(NodeExtensionFactory.class))) + ); + } + + protected void configureNodeImplementations(Registry registry) { + NODE_TYPES.forEach(nodeType -> registry.bind(nodeType, toSelf())); + } + + protected void configureRegistryExtensions(Registry registry) { + registry.addExtension(new SelfNodeRegistryExtension()); + } + + protected void configureFilters(RegistryObjectFactory.Builder builder) { + builder.addFilterHandler(getAllowsTypesFilterHandler(Node.class)); + builder.addIterableFilterHandler(getIterableElementTypesFilterHandler()); + builder.addFilterHandler(ExtensionUtil.hasExtensionsFilterHandler); + builder.addIterableFilterHandler(ExtensionUtil.iterableHasExtensionsFilterHandler); + builder.addFilterHandler(ExtensionUtil.hasExtensionOneOfNodeFilterHandler); + builder.addIterableFilterHandler(ExtensionUtil.iterableHasExtensionOneOfFilterHandler); + } + + @Override + public CompilationUnitNode compilationUnitNode( + TokenRange tokenRange, + @Nullable PreambleNode preambleNode, + @Nullable BodyNode bodyNode + ) { + return this.objectFactory.get(CompilationUnitNode.class, tokenRange, preambleNode, bodyNode); + } + + @Override + public PreambleNode preambleNode(TokenRange tokenRange, int groovyIndex) { + return this.objectFactory.get(PreambleNode.class, tokenRange, groovyIndex); + } + + @Override + public BodyNode bodyNode(TokenRange tokenRange, List children) { + return this.objectFactory.get(BodyNode.class, tokenRange, children); + } + + @Override + public GStringBodyTextNode gStringBodyTextNode(TokenRange tokenRange, List children) { + return this.objectFactory.get(GStringBodyTextNode.class, tokenRange, children); + } + + @Override + public JStringBodyTextNode jStringBodyTextNode(TokenRange tokenRange, String content) { + return this.objectFactory.get(JStringBodyTextNode.class, tokenRange, content); + } + + @Override + public TypedComponentNode typedComponentNode( + TokenRange tokenRange, + ComponentArgsNode componentArgsNode, + @Nullable BodyNode body + ) { + return this.objectFactory.get(TypedComponentNode.class, tokenRange, componentArgsNode, body); + } + + @Override + public FragmentComponentNode fragmentComponentNode(TokenRange tokenRange, @Nullable BodyNode bodyNode) { + return this.objectFactory.get(FragmentComponentNode.class, tokenRange, bodyNode); + } + + @Override + public ComponentArgsNode componentArgsNode( + TokenRange tokenRange, + ComponentTypeNode componentTypeNode, + @Nullable ComponentConstructorNode componentConstructorNode, + List attributeNodes + ) { + return this.objectFactory.get( + ComponentArgsNode.class, + tokenRange, + componentTypeNode, + componentConstructorNode, + attributeNodes + ); + } + + @Override + public ClassComponentTypeNode classComponentTypeNode(TokenRange tokenRange) { + return this.objectFactory.get(ClassComponentTypeNode.class, tokenRange); + } + + @Override + public StringComponentTypeNode stringComponentTypeNode(TokenRange tokenRange, int typeTokenIndex) { + return this.objectFactory.get(StringComponentTypeNode.class, tokenRange, typeTokenIndex); + } + + @Override + public ComponentConstructorNode componentConstructorNode(TokenRange tokenRange, int groovyIndex) { + return this.objectFactory.get(ComponentConstructorNode.class, tokenRange, groovyIndex); + } + + @Override + public KeyValueAttrNode keyValueAttrNode(TokenRange tokenRange, KeyNode keyNode, ValueNode valueNode) { + return this.objectFactory.get(KeyValueAttrNode.class, tokenRange, keyNode, valueNode); + } + + @Override + public BooleanValueAttrNode booleanValueAttrNode(TokenRange tokenRange, KeyNode keyNode) { + return this.objectFactory.get(BooleanValueAttrNode.class, tokenRange, keyNode); + } + + @Override + public KeyNode keyNode(TokenRange tokenRange, int tokenIndex) { + return this.objectFactory.get(KeyNode.class, tokenRange, tokenIndex); + } + + @Override + public GStringValueNode gStringValueNode(TokenRange tokenRange, int contentTokenIndex) { + return this.objectFactory.createInstance(GStringValueNode.class, tokenRange, contentTokenIndex); + } + + @Override + public JStringValueNode jStringValueNode(TokenRange tokenRange, String content) { + return this.objectFactory.get(JStringValueNode.class, tokenRange, content); + } + + @Override + public ClosureValueNode closureValueNode(TokenRange tokenRange, int groovyIndex) { + return this.objectFactory.get(ClosureValueNode.class, tokenRange, groovyIndex); + } + + @Override + public EmptyClosureValueNode emptyClosureValueNode(TokenRange tokenRange) { + return this.objectFactory.get(EmptyClosureValueNode.class, tokenRange); + } + + @Override + public ComponentValueNode componentValueNode(TokenRange tokenRange, ComponentNode componentNode) { + return this.objectFactory.get(ComponentValueNode.class, tokenRange, componentNode); + } + + @Override + public PlainScriptletNode plainScriptletNode(TokenRange tokenRange, int groovyIndex) { + return this.objectFactory.get(PlainScriptletNode.class, tokenRange, groovyIndex); + } + + @Override + public DollarScriptletNode dollarScriptletNode(TokenRange tokenRange, int groovyIndex) { + return this.objectFactory.get(DollarScriptletNode.class, tokenRange, groovyIndex); + } + + @Override + public DollarReferenceNode dollarReferenceNode(TokenRange tokenRange, int groovyIndex) { + return this.objectFactory.get(DollarReferenceNode.class, tokenRange, groovyIndex); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/NodeFactory.java b/web-views/src/main/java/groowt/view/web/ast/NodeFactory.java new file mode 100644 index 0000000..cf64da2 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/NodeFactory.java @@ -0,0 +1,68 @@ +package groowt.view.web.ast; + +import groowt.view.web.ast.node.*; +import groowt.view.web.util.TokenRange; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public interface NodeFactory { + + CompilationUnitNode compilationUnitNode( + TokenRange tokenRange, + @Nullable PreambleNode preambleNode, + @Nullable BodyNode bodyNode + ); + + PreambleNode preambleNode(TokenRange tokenRange, int groovyIndex); + + BodyNode bodyNode(TokenRange tokenRange, List children); + + GStringBodyTextNode gStringBodyTextNode(TokenRange tokenRange, List children); + + JStringBodyTextNode jStringBodyTextNode(TokenRange tokenRange, String content); + + TypedComponentNode typedComponentNode( + TokenRange tokenRange, + ComponentArgsNode componentArgsNode, + @Nullable BodyNode body + ); + + FragmentComponentNode fragmentComponentNode(TokenRange tokenRange, @Nullable BodyNode bodyNode); + + ComponentArgsNode componentArgsNode( + TokenRange tokenRange, + ComponentTypeNode componentTypeNode, + @Nullable ComponentConstructorNode componentConstructorNode, + List attributeNodes + ); + + ClassComponentTypeNode classComponentTypeNode(TokenRange tokenRange); + + StringComponentTypeNode stringComponentTypeNode(TokenRange tokenRange, int typeTokenIndex); + + ComponentConstructorNode componentConstructorNode(TokenRange tokenRange, int groovyIndex); + + KeyValueAttrNode keyValueAttrNode(TokenRange tokenRange, KeyNode keyNode, ValueNode valueNode); + + BooleanValueAttrNode booleanValueAttrNode(TokenRange tokenRange, KeyNode keyNode); + + KeyNode keyNode(TokenRange tokenRange, int keyTokenIndex); + + GStringValueNode gStringValueNode(TokenRange tokenRange, int contentTokenIndex); + + JStringValueNode jStringValueNode(TokenRange tokenRange, String content); + + ClosureValueNode closureValueNode(TokenRange tokenRange, int groovyIndex); + + EmptyClosureValueNode emptyClosureValueNode(TokenRange tokenRange); + + ComponentValueNode componentValueNode(TokenRange tokenRange, ComponentNode componentNode); + + PlainScriptletNode plainScriptletNode(TokenRange tokenRange, int groovyIndex); + + DollarScriptletNode dollarScriptletNode(TokenRange tokenRange, int groovyIndex); + + DollarReferenceNode dollarReferenceNode(TokenRange tokenRange, int groovyIndex); + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/NodeUtil.java b/web-views/src/main/java/groowt/view/web/ast/NodeUtil.java new file mode 100644 index 0000000..80d273a --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/NodeUtil.java @@ -0,0 +1,57 @@ +package groowt.view.web.ast; + +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.node.LeafNode; +import groowt.view.web.ast.node.Node; +import groowt.view.web.ast.node.TreeNode; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public final class NodeUtil { + + public static boolean isAnyOfType(Node subject, Class... nodeTypes) { + for (final var type : nodeTypes) { + if (type.isAssignableFrom(subject.getClass())) { + return true; + } + } + return false; + } + + public static boolean isAnyOfType(Node subject, List> nodeTypes) { + for (final var type : nodeTypes) { + if (type.isAssignableFrom(subject.getClass())) { + return true; + } + } + return false; + } + + public static @Nullable LeafNode getDeepestLeftmostLeafNode(Node node) { + return switch (node) { + case LeafNode leafNode -> leafNode; + case TreeNode treeNode when treeNode.getChildrenSize() > 0 -> + getDeepestLeftmostLeafNode(treeNode.getAt(0)); + default -> null; + }; + } + + private static void doFormatAst(Node node, StringBuilder sb, int indentTimes, String indent, TokenList tokenList) { + NodeUtilKt.formatSingleNode(node, sb, indentTimes, indent, tokenList); + if (node instanceof TreeNode treeNode) { + treeNode.getChildren().forEach(child -> doFormatAst( + child, sb, indentTimes + 1, indent, tokenList + )); + } + } + + public static String formatAst(Node node, TokenList tokenList) { + final var sb = new StringBuilder(); + doFormatAst(node, sb, 0, " ", tokenList); + return sb.toString(); + } + + private NodeUtil() {} + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/NodeUtil.kt b/web-views/src/main/java/groowt/view/web/ast/NodeUtil.kt new file mode 100644 index 0000000..b5a4402 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/NodeUtil.kt @@ -0,0 +1,22 @@ +package groowt.view.web.ast + +import groowt.view.web.antlr.TokenList +import groowt.view.web.antlr.formatToken +import groowt.view.web.ast.node.LeafNode +import groowt.view.web.ast.node.Node + +fun formatSingleNode(node: Node, sb: StringBuilder, indentTimes: Int, indent: String, tokenList: TokenList) { + sb.append(indent.repeat(indentTimes)) + sb.append( + "${node.javaClass.simpleName}(${node.tokenRange.startPosition.toStringShort()}.." + + "${node.tokenRange.endPosition.toStringShort()})\n" + ) + if (node is LeafNode) { + val tokens = tokenList.getRange(node.tokenRange) + tokens.forEach { + sb.append(indent.repeat(indentTimes + 1)) + sb.append(formatToken(it)) + sb.append("\n") + } + } +} diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/ExtensionUtil.java b/web-views/src/main/java/groowt/view/web/ast/extension/ExtensionUtil.java new file mode 100644 index 0000000..21008b4 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/ExtensionUtil.java @@ -0,0 +1,35 @@ +package groowt.view.web.ast.extension; + +import groowt.util.di.filters.FilterHandler; +import groowt.util.di.filters.FilterHandlers; +import groowt.util.di.filters.IterableFilterHandler; +import groowt.util.di.filters.IterableFilterHandlers; +import groowt.view.web.ast.node.Node; + +import java.util.Arrays; + +public final class ExtensionUtil { + + private ExtensionUtil() {} + + public static final FilterHandler hasExtensionsFilterHandler = + FilterHandlers.of(HasExtensions.class, Node.class, (annotation, node) -> + Arrays.stream(annotation.value()).allMatch(node::hasExtension) + ); + + public static final IterableFilterHandler iterableHasExtensionsFilterHandler = + IterableFilterHandlers.of(IterableHasExtensions.class, (annotation, node) -> + Arrays.stream(annotation.value()).allMatch(node::hasExtension) + ); + + public static final FilterHandler hasExtensionOneOfNodeFilterHandler = + FilterHandlers.of(HasExtensionOneOf.class, Node.class, (annotation, node) -> + Arrays.stream(annotation.value()).anyMatch(node::hasExtension) + ); + + public static final IterableFilterHandler iterableHasExtensionOneOfFilterHandler = + IterableFilterHandlers.of(IterableHasExtensionOneOf.class, (annotation, node) -> + Arrays.stream(annotation.value()).anyMatch(node::hasExtension) + ); + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/GStringNodeExtension.java b/web-views/src/main/java/groowt/view/web/ast/extension/GStringNodeExtension.java new file mode 100644 index 0000000..95d7275 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/GStringNodeExtension.java @@ -0,0 +1,33 @@ +package groowt.view.web.ast.extension; + +import groowt.view.web.ast.node.Node; +import org.antlr.v4.runtime.Token; + +import java.util.List; +import java.util.stream.Collectors; + +public abstract sealed class GStringNodeExtension implements NodeExtension + permits GStringPathExtension, GStringScriptletExtension { + + private final Node self; + private final List rawTokens; + + public GStringNodeExtension(Node self, List rawTokens) { + this.self = self; + this.rawTokens = rawTokens; + } + + @Override + public Node getSelf() { + return this.self; + } + + public List getRawTokens() { + return this.rawTokens; + } + + public String getAsValidEmbeddableCode() { + return this.getRawTokens().stream().map(Token::getText).collect(Collectors.joining()); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/GStringPathExtension.java b/web-views/src/main/java/groowt/view/web/ast/extension/GStringPathExtension.java new file mode 100644 index 0000000..0af8c57 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/GStringPathExtension.java @@ -0,0 +1,21 @@ +package groowt.view.web.ast.extension; + +import groowt.util.di.annotation.Given; +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.node.Node; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; + +public non-sealed class GStringPathExtension extends GStringNodeExtension { + + @Inject + public GStringPathExtension(TokenList allTokens, @SelfNode Node self, @Given TokenRange rawTokenRange) { + super(self, allTokens.getRange(rawTokenRange)); + } + + @Override + public String getAsValidEmbeddableCode() { + return "$" + super.getAsValidEmbeddableCode(); + } + +} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/GStringScriptletExtension.java b/web-views/src/main/java/groowt/view/web/ast/extension/GStringScriptletExtension.java new file mode 100644 index 0000000..6192b36 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/GStringScriptletExtension.java @@ -0,0 +1,21 @@ +package groowt.view.web.ast.extension; + +import groowt.util.di.annotation.Given; +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.node.Node; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; + +public non-sealed class GStringScriptletExtension extends GStringNodeExtension { + + @Inject + public GStringScriptletExtension(TokenList allTokens, @SelfNode Node self, @Given TokenRange rawTokenRange) { + super(self, allTokens.getRange(rawTokenRange)); + } + + @Override + public String getAsValidEmbeddableCode() { + return "${" + super.getAsValidEmbeddableCode() + "}"; + } + +} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/GroovyCodeNodeExtension.java b/web-views/src/main/java/groowt/view/web/ast/extension/GroovyCodeNodeExtension.java new file mode 100644 index 0000000..6aca38d --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/GroovyCodeNodeExtension.java @@ -0,0 +1,48 @@ +package groowt.view.web.ast.extension; + +import groowt.util.di.annotation.Given; +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.node.Node; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; +import org.antlr.v4.runtime.Token; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public class GroovyCodeNodeExtension implements NodeExtension { + + private final Node self; + private final List groovyCodeTokens; + private final Function, String> toValidGroovyCode; + + @Inject + public GroovyCodeNodeExtension( + TokenList allTokens, + @SelfNode Node self, + @Given TokenRange groovyCodeTokenRange, + @Given Function, String> toValidGroovyCode + ) { + this.self = self; + this.groovyCodeTokens = allTokens.getRange(groovyCodeTokenRange); + this.toValidGroovyCode = toValidGroovyCode; + } + + @Override + public Node getSelf() { + return this.self; + } + + /** + * @return A copy of the tokens list. + */ + public List getGroovyCodeTokens() { + return new ArrayList<>(this.groovyCodeTokens); + } + + public String getAsValidGroovyCode() { + return this.toValidGroovyCode.apply(this.getGroovyCodeTokens()); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/HasExtensionOneOf.java b/web-views/src/main/java/groowt/view/web/ast/extension/HasExtensionOneOf.java new file mode 100644 index 0000000..2271f74 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/HasExtensionOneOf.java @@ -0,0 +1,15 @@ +package groowt.view.web.ast.extension; + +import groowt.util.di.filters.Filter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Filter +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface HasExtensionOneOf { + Class[] value(); +} diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/HasExtensions.java b/web-views/src/main/java/groowt/view/web/ast/extension/HasExtensions.java new file mode 100644 index 0000000..1e37aea --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/HasExtensions.java @@ -0,0 +1,15 @@ +package groowt.view.web.ast.extension; + +import groowt.util.di.filters.Filter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Filter +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface HasExtensions { + Class[] value(); +} diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/IterableHasExtensionOneOf.java b/web-views/src/main/java/groowt/view/web/ast/extension/IterableHasExtensionOneOf.java new file mode 100644 index 0000000..1f8795f --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/IterableHasExtensionOneOf.java @@ -0,0 +1,15 @@ +package groowt.view.web.ast.extension; + +import groowt.util.di.filters.IterableFilter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@IterableFilter +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface IterableHasExtensionOneOf { + Class[] value(); +} diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/IterableHasExtensions.java b/web-views/src/main/java/groowt/view/web/ast/extension/IterableHasExtensions.java new file mode 100644 index 0000000..28ce676 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/IterableHasExtensions.java @@ -0,0 +1,15 @@ +package groowt.view.web.ast.extension; + +import groowt.util.di.filters.IterableFilter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@IterableFilter +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface IterableHasExtensions { + Class[] value(); +} diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/NodeExtension.java b/web-views/src/main/java/groowt/view/web/ast/extension/NodeExtension.java new file mode 100644 index 0000000..feb82a0 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/NodeExtension.java @@ -0,0 +1,7 @@ +package groowt.view.web.ast.extension; + +import groowt.view.web.ast.node.Node; + +public interface NodeExtension { + Node getSelf(); +} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/NodeExtensionContainer.java b/web-views/src/main/java/groowt/view/web/ast/extension/NodeExtensionContainer.java new file mode 100644 index 0000000..95e80e3 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/NodeExtensionContainer.java @@ -0,0 +1,8 @@ +package groowt.view.web.ast.extension; + +import groowt.util.extensible.ExtensionContainer; +import groowt.view.web.ast.node.Node; + +public interface NodeExtensionContainer extends ExtensionContainer { + E createExtension(Class extensionClass, Node self, Object... givenArgs); +} diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/NodeExtensionFactory.java b/web-views/src/main/java/groowt/view/web/ast/extension/NodeExtensionFactory.java new file mode 100644 index 0000000..a6fd5cf --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/NodeExtensionFactory.java @@ -0,0 +1,8 @@ +package groowt.view.web.ast.extension; + +import groowt.view.web.ast.node.Node; + +@FunctionalInterface +public interface NodeExtensionFactory { + E create(Class extensionClass, Node self, Object... constructorArgs); +} diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/SelfNode.java b/web-views/src/main/java/groowt/view/web/ast/extension/SelfNode.java new file mode 100644 index 0000000..c64ae58 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/SelfNode.java @@ -0,0 +1,13 @@ +package groowt.view.web.ast.extension; + +import jakarta.inject.Qualifier; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD}) +public @interface SelfNode {} diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/SelfNodeRegistryExtension.java b/web-views/src/main/java/groowt/view/web/ast/extension/SelfNodeRegistryExtension.java new file mode 100644 index 0000000..a7c7932 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/SelfNodeRegistryExtension.java @@ -0,0 +1,44 @@ +package groowt.view.web.ast.extension; + +import groowt.util.di.*; +import groowt.view.web.ast.node.Node; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; + +public final class SelfNodeRegistryExtension implements RegistryExtension, QualifierHandlerContainer { + + private static final class SelfNodeQualifierHandler implements QualifierHandler { + + private final SelfNodeRegistryExtension extension; + + public SelfNodeQualifierHandler(SelfNodeRegistryExtension extension) { + this.extension = extension; + } + + @SuppressWarnings("unchecked") + @Override + public @Nullable Binding handle(SelfNode annotation, Class dependencyClass) { + return Node.class.isAssignableFrom(dependencyClass) ? (Binding) this.extension.selfNodeBinding : null; + } + + } + + private final QualifierHandler handler = new SelfNodeQualifierHandler(this); + private @Nullable Binding selfNodeBinding; + + public void setSelfNode(@Nullable Node self) { + if (self == null) { + this.selfNodeBinding = null; + } else { + this.selfNodeBinding = new SingletonBinding<>(self); + } + } + + @SuppressWarnings("unchecked") + @Override + public @Nullable QualifierHandler getQualifierHandler(Class qualifierType) { + return SelfNode.class.equals(qualifierType) ? (QualifierHandler) this.handler : null; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/SimpleNodeExtensionContainer.java b/web-views/src/main/java/groowt/view/web/ast/extension/SimpleNodeExtensionContainer.java new file mode 100644 index 0000000..b2e3396 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/SimpleNodeExtensionContainer.java @@ -0,0 +1,25 @@ +package groowt.view.web.ast.extension; + +import groowt.util.extensible.AbstractExtensionContainer; +import groowt.view.web.ast.node.Node; + +public class SimpleNodeExtensionContainer extends AbstractExtensionContainer + implements NodeExtensionContainer { + + public SimpleNodeExtensionContainer(NodeExtensionFactory extensionFactory) { + super(extensionFactory); + } + + @Override + public E createExtension(Class extensionClass, Node self, Object... givenArgs) { + if (this.hasExtension(extensionClass)) { + throw new IllegalArgumentException( + "There is already an extension registered of type " + extensionClass.getName() + ); + } + final E extension = this.getExtensionFactory().create(extensionClass, self, givenArgs); + this.registerExtension(extension); + return extension; + } + +} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/SimpleNodeExtensionFactory.java b/web-views/src/main/java/groowt/view/web/ast/extension/SimpleNodeExtensionFactory.java new file mode 100644 index 0000000..e165ee2 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/extension/SimpleNodeExtensionFactory.java @@ -0,0 +1,32 @@ +package groowt.view.web.ast.extension; + +import groowt.util.di.RegistryObjectFactory; +import groowt.view.web.ast.node.Node; +import jakarta.inject.Inject; + +public final class SimpleNodeExtensionFactory implements NodeExtensionFactory { + + private final RegistryObjectFactory objectFactory; + + @Inject + public SimpleNodeExtensionFactory(RegistryObjectFactory objectFactory) { + this.objectFactory = objectFactory; + } + + @Override + public E create(Class extensionClass, Node self, Object... constructorArgs) { + this.objectFactory.configureRegistry(r -> { + r.getExtension(SelfNodeRegistryExtension.class).setSelfNode(self); + }); + try { + return this.objectFactory.createInstance(extensionClass, constructorArgs); + } catch (RuntimeException e) { + throw new RuntimeException("Could not create " + extensionClass + "\n" + e.getMessage(), e); + } finally { + this.objectFactory.configureRegistry(r -> { + r.getExtension(SelfNodeRegistryExtension.class).setSelfNode(null); + }); + } + } + +} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/ast/node/AbstractLeafNode.java b/web-views/src/main/java/groowt/view/web/ast/node/AbstractLeafNode.java new file mode 100644 index 0000000..fa729e7 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/AbstractLeafNode.java @@ -0,0 +1,60 @@ +package groowt.view.web.ast.node; + +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.extension.NodeExtension; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +public abstract class AbstractLeafNode implements LeafNode { + + private final TokenRange tokenRange; + private final NodeExtensionContainer extensionContainer; + + public AbstractLeafNode(TokenRange tokenRange, NodeExtensionContainer extensionContainer) { + this.tokenRange = tokenRange; + this.extensionContainer = extensionContainer; + } + + protected static TokenRange getTokenRangeFromIndex(TokenList tokenList, int tokenIndex) { + return TokenRange.of(tokenList.get(tokenIndex)); + } + + @Override + public TokenRange getTokenRange() { + return this.tokenRange; + } + + @Override + public T createExtension(Class extensionClass, Object... constructorArgs) { + return this.extensionContainer.createExtension(extensionClass, this, constructorArgs); + } + + @Override + public NodeExtensionContainer getExtensionContainer() { + return this.extensionContainer; + } + + @Override + public @Nullable T findExtension(Class extensionClass) { + return this.extensionContainer.findExtension(extensionClass); + } + + @Override + public void configureExtension(Class extensionClass, Consumer configure) { + this.extensionContainer.configureExtension(extensionClass, configure); + } + + @Override + public T getExtension(Class extensionClass) { + return this.extensionContainer.getExtension(extensionClass); + } + + @Override + public boolean hasExtension(Class extensionClass) { + return this.extensionContainer.hasExtension(extensionClass); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/AbstractTreeNode.java b/web-views/src/main/java/groowt/view/web/ast/node/AbstractTreeNode.java new file mode 100644 index 0000000..68f155e --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/AbstractTreeNode.java @@ -0,0 +1,119 @@ +package groowt.view.web.ast.node; + +import groowt.view.web.ast.extension.NodeExtension; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +public abstract class AbstractTreeNode implements TreeNode { + + protected static List filterNulls(Iterable nodes) { + final List nonNulls = new ArrayList<>(); + for (final Node node : nodes) { + if (node != null) { + nonNulls.add(node); + } + } + return nonNulls; + } + + protected static List filterNulls(Node... nodes) { + return filterNulls(Arrays.asList(nodes)); + } + + protected static List checkForNulls(List children) { + for (final Node child : children) { + if (child == null) { + throw new NullPointerException("children may not contain null elements!"); + } + } + return new ArrayList<>(children); + } + + protected static List checkForNulls(Node... nodes) { + return checkForNulls(Arrays.asList(nodes)); + } + + private final TokenRange tokenRange; + private final NodeExtensionContainer extensionContainer; + private final List children = new ArrayList<>(); + + public AbstractTreeNode( + TokenRange tokenRange, + NodeExtensionContainer extensionContainer, + List children + ) { + this.tokenRange = tokenRange; + this.extensionContainer = extensionContainer; + this.children.addAll(checkForNulls(children)); + } + + @Override + public TokenRange getTokenRange() { + return this.tokenRange; + } + + @Override + public T createExtension(Class extensionClass, Object... constructorArgs) { + return this.extensionContainer.createExtension(extensionClass, this, constructorArgs); + } + + @Override + public NodeExtensionContainer getExtensionContainer() { + return this.extensionContainer; + } + + @Override + public @Nullable T findExtension(Class extensionClass) { + return this.extensionContainer.findExtension(extensionClass); + } + + @Override + public void configureExtension(Class extensionClass, Consumer configure) { + this.extensionContainer.configureExtension(extensionClass, configure); + } + + @Override + public T getExtension(Class extensionClass) { + return this.extensionContainer.getExtension(extensionClass); + } + + @Override + public boolean hasExtension(Class extensionClass) { + return this.extensionContainer.hasExtension(extensionClass); + } + + /** + * @return a copy of the children. + */ + @Override + public List getChildren() { + return new ArrayList<>(this.children); + } + + @Override + public Node getAt(int index) { + return this.children.get(index); + } + + @Override + public C getAt(int index, Class childClass) { + return childClass.cast(this.children.get(index)); + } + + @Override + public int indexOf(Node child) { + return this.children.indexOf(child); + } + + @Override + public int getChildrenSize() { + return this.children.size(); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/AttrNode.java b/web-views/src/main/java/groowt/view/web/ast/node/AttrNode.java new file mode 100644 index 0000000..e37a378 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/AttrNode.java @@ -0,0 +1,26 @@ +package groowt.view.web.ast.node; + +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; + +import java.util.List; + +public abstract sealed class AttrNode extends AbstractTreeNode permits BooleanValueAttrNode, KeyValueAttrNode { + + private final KeyNode keyNode; + + public AttrNode( + TokenRange tokenRange, + NodeExtensionContainer extensionContainer, + List children, + KeyNode keyNode + ) { + super(tokenRange, extensionContainer, children); + this.keyNode = keyNode; + } + + public KeyNode getKeyNode() { + return this.keyNode; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/BodyChildNode.java b/web-views/src/main/java/groowt/view/web/ast/node/BodyChildNode.java new file mode 100644 index 0000000..bbcc4b3 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/BodyChildNode.java @@ -0,0 +1,13 @@ +package groowt.view.web.ast.node; + +public interface BodyChildNode { + + default Node asNode() { + return (Node) this; + } + + default T asNode(Class nodeType) { + return nodeType.cast(this); + } + +} \ No newline at end of file diff --git a/web-views/src/main/java/groowt/view/web/ast/node/BodyNode.java b/web-views/src/main/java/groowt/view/web/ast/node/BodyNode.java new file mode 100644 index 0000000..3e9f8ba --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/BodyNode.java @@ -0,0 +1,28 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; + +import java.util.List; + +public class BodyNode extends AbstractTreeNode { + + protected static List checkChildren(List children) { + if (children.isEmpty()) { + throw new IllegalArgumentException("A valid BodyNode must have at least one child."); + } + return children; + } + + protected static List childrenAsNodes(List children) { + return children.stream().map(BodyChildNode::asNode).toList(); + } + + @Inject + public BodyNode(NodeExtensionContainer extensionContainer, @Given TokenRange tokenRange, @Given List children) { + super(tokenRange, extensionContainer, childrenAsNodes(checkChildren(children))); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/BooleanValueAttrNode.java b/web-views/src/main/java/groowt/view/web/ast/node/BooleanValueAttrNode.java new file mode 100644 index 0000000..ca28f61 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/BooleanValueAttrNode.java @@ -0,0 +1,21 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; + +import java.util.List; + +public non-sealed class BooleanValueAttrNode extends AttrNode { + + @Inject + public BooleanValueAttrNode( + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given KeyNode keyNode + ) { + super(tokenRange, extensionContainer, List.of(keyNode), keyNode); + } + +} 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 new file mode 100644 index 0000000..01c8578 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/ClassComponentTypeNode.java @@ -0,0 +1,32 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; + +import java.util.Objects; + +public non-sealed class ClassComponentTypeNode extends ComponentTypeNode { + + private String fqn; + + @Inject + public ClassComponentTypeNode( + TokenList tokenList, + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange + ) { + super(tokenList, tokenRange, extensionContainer, tokenRange); + } + + public void setFullyQualifiedName(String fullyQualifiedName) { + this.fqn = Objects.requireNonNull(fullyQualifiedName); + } + + public String getFullyQualifiedName() { + return Objects.requireNonNullElse(this.fqn, this.getIdentifier()); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/ClosureValueNode.java b/web-views/src/main/java/groowt/view/web/ast/node/ClosureValueNode.java new file mode 100644 index 0000000..550ad79 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/ClosureValueNode.java @@ -0,0 +1,52 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.extension.GroovyCodeNodeExtension; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; +import org.antlr.v4.runtime.Token; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class ClosureValueNode extends AbstractLeafNode implements ValueNode { + + private final int groovyIndex; + private final GroovyCodeNodeExtension groovyCode; + + @Inject + public ClosureValueNode( + TokenList tokenList, + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given int groovyIndex + ) { + super(tokenRange, extensionContainer); + this.groovyIndex = groovyIndex; + this.groovyCode = this.createGroovyCode(tokenList); + } + + protected GroovyCodeNodeExtension createGroovyCode(TokenList tokenList) { + return this.createExtension( + GroovyCodeNodeExtension.class, + TokenRange.fromIndex(tokenList, this.getGroovyIndex()), + (Function, String>) this::toValidGroovyCode + ); + } + + protected String toValidGroovyCode(List groovyTokens) { + return "{ " + groovyTokens.stream().map(Token::getText).collect(Collectors.joining()) + "\n}"; + } + + public GroovyCodeNodeExtension getGroovyCode() { + return this.groovyCode; + } + + public int getGroovyIndex() { + return this.groovyIndex; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/CompilationUnitNode.java b/web-views/src/main/java/groowt/view/web/ast/node/CompilationUnitNode.java new file mode 100644 index 0000000..0cbd06e --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/CompilationUnitNode.java @@ -0,0 +1,34 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; +import org.jetbrains.annotations.Nullable; + +public class CompilationUnitNode extends AbstractTreeNode { + + private final @Nullable PreambleNode preambleNode; + private final @Nullable BodyNode bodyNode; + + @Inject + public CompilationUnitNode( + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given @Nullable PreambleNode preambleNode, + @Given @Nullable BodyNode bodyNode + ) { + super(tokenRange, extensionContainer, filterNulls(preambleNode, bodyNode)); + this.preambleNode = preambleNode; + this.bodyNode = bodyNode; + } + + public @Nullable PreambleNode getPreambleNode() { + return this.preambleNode; + } + + public @Nullable BodyNode getBodyNode() { + return this.bodyNode; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/ComponentArgsNode.java b/web-views/src/main/java/groowt/view/web/ast/node/ComponentArgsNode.java new file mode 100644 index 0000000..5475d08 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/ComponentArgsNode.java @@ -0,0 +1,55 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class ComponentArgsNode extends AbstractTreeNode { + + private static List createChildren( + ComponentTypeNode type, + @Nullable ComponentConstructorNode constructor, + List attributes + ) { + final List children = new ArrayList<>(filterNulls(type, constructor)); + children.addAll(attributes); + return children; + } + + private final ComponentTypeNode type; + private final @Nullable ComponentConstructorNode constructor; + private final List attributes = new ArrayList<>(); + + @Inject + public ComponentArgsNode( + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given ComponentTypeNode type, + @Given @Nullable ComponentConstructorNode constructor, + @Given List attributes + ) { + super(tokenRange, extensionContainer, createChildren(type, constructor, attributes)); + this.type = type; + this.constructor = constructor; + this.attributes.addAll(attributes); + } + + public ComponentTypeNode getType() { + return this.type; + } + + @Nullable + public ComponentConstructorNode getConstructor() { + return this.constructor; + } + + public List getAttributes() { + return this.attributes; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/ComponentConstructorNode.java b/web-views/src/main/java/groowt/view/web/ast/node/ComponentConstructorNode.java new file mode 100644 index 0000000..e80d6fe --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/ComponentConstructorNode.java @@ -0,0 +1,56 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.extension.GroovyCodeNodeExtension; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; +import org.antlr.v4.runtime.Token; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class ComponentConstructorNode extends AbstractLeafNode { + + private final int groovyCodeIndex; + private final GroovyCodeNodeExtension groovyCode; + + @Inject + public ComponentConstructorNode( + TokenList tokenList, + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given int groovyCodeIndex + ) { + super(tokenRange, extensionContainer); + this.groovyCodeIndex = groovyCodeIndex; + this.groovyCode = this.createGroovyCode(tokenList); + } + + protected GroovyCodeNodeExtension createGroovyCode(TokenList tokenList) { + return this.createExtension( + GroovyCodeNodeExtension.class, + TokenRange.fromIndex(tokenList, this.groovyCodeIndex), + (Function, String>) this::toValidGroovyCode + ); + } + + protected String toValidGroovyCode(List tokens) { + final var rawCode = tokens.stream().map(Token::getText).collect(Collectors.joining()).trim(); + if (rawCode.isEmpty() || rawCode.isBlank()) { + throw new IllegalStateException("Raw code cannot be blank or empty."); + } + return "[" + rawCode + ']'; + } + + public int getGroovyCodeIndex() { + return this.groovyCodeIndex; + } + + public GroovyCodeNodeExtension getGroovyCode() { + return this.groovyCode; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/ComponentNode.java b/web-views/src/main/java/groowt/view/web/ast/node/ComponentNode.java new file mode 100644 index 0000000..af6b53e --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/ComponentNode.java @@ -0,0 +1,30 @@ +package groowt.view.web.ast.node; + +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public sealed abstract class ComponentNode extends AbstractTreeNode implements BodyChildNode permits + FragmentComponentNode, + TypedComponentNode { + + private final BodyNode bodyNode; + + public ComponentNode( + TokenRange tokenRange, + NodeExtensionContainer extensionContainer, + List children, + @Nullable BodyNode bodyNode + ) { + super(tokenRange, extensionContainer, children); + this.bodyNode = bodyNode; + } + + @Nullable + public BodyNode getBody() { + return this.bodyNode; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/ComponentTypeNode.java b/web-views/src/main/java/groowt/view/web/ast/node/ComponentTypeNode.java new file mode 100644 index 0000000..dab57a4 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/ComponentTypeNode.java @@ -0,0 +1,33 @@ +package groowt.view.web.ast.node; + +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import org.antlr.v4.runtime.Token; + +import java.util.List; +import java.util.stream.Collectors; + +public sealed abstract class ComponentTypeNode extends AbstractLeafNode permits + ClassComponentTypeNode, + StringComponentTypeNode { + + private final String identifier; + + public ComponentTypeNode( + TokenList tokenList, + TokenRange tokenRange, + NodeExtensionContainer extensionContainer, + TokenRange identifierTokenRange + ) { + super(tokenRange, extensionContainer); + this.identifier = tokenList.getRange(identifierTokenRange).stream() + .map(Token::getText) + .collect(Collectors.joining()); + } + + public String getIdentifier() { + return this.identifier; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/ComponentValueNode.java b/web-views/src/main/java/groowt/view/web/ast/node/ComponentValueNode.java new file mode 100644 index 0000000..f973bb0 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/ComponentValueNode.java @@ -0,0 +1,28 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; + +import java.util.List; + +public class ComponentValueNode extends AbstractTreeNode implements ValueNode { + + private final ComponentNode componentNode; + + @Inject + public ComponentValueNode( + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given ComponentNode componentNode + ) { + super(tokenRange, extensionContainer, List.of(componentNode)); + this.componentNode = componentNode; + } + + public ComponentNode getComponentNode() { + return this.componentNode; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/DollarReferenceNode.java b/web-views/src/main/java/groowt/view/web/ast/node/DollarReferenceNode.java new file mode 100644 index 0000000..4e0c191 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/DollarReferenceNode.java @@ -0,0 +1,34 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.extension.GStringPathExtension; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; + +public class DollarReferenceNode extends AbstractLeafNode { + + private final int groovyTokenIndex; + + @Inject + public DollarReferenceNode( + TokenList tokenList, + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given int groovyTokenIndex + ) { + super(tokenRange, extensionContainer); + this.groovyTokenIndex = groovyTokenIndex; + this.createGStringPath(tokenList); + } + + protected void createGStringPath(TokenList tokenList) { + this.createExtension(GStringPathExtension.class, TokenRange.fromIndex(tokenList, this.groovyTokenIndex)); + } + + public GStringPathExtension getGStringPath() { + return this.getExtension(GStringPathExtension.class); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/DollarScriptletNode.java b/web-views/src/main/java/groowt/view/web/ast/node/DollarScriptletNode.java new file mode 100644 index 0000000..420e9b7 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/DollarScriptletNode.java @@ -0,0 +1,33 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.extension.GStringScriptletExtension; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; + +public class DollarScriptletNode extends AbstractLeafNode { + + private final GStringScriptletExtension gStringScriptlet; + + @Inject + public DollarScriptletNode( + TokenList tokenList, + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given int groovyTokenIndex + ) { + super(tokenRange, extensionContainer); + this.gStringScriptlet = this.createGStringScriptlet(tokenList, groovyTokenIndex); + } + + protected GStringScriptletExtension createGStringScriptlet(TokenList tokenList, int groovyTokenIndex) { + return this.createExtension(GStringScriptletExtension.class, TokenRange.fromIndex(tokenList, groovyTokenIndex)); + } + + public GStringScriptletExtension getGStringScriptlet() { + return this.gStringScriptlet; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/EmptyClosureValueNode.java b/web-views/src/main/java/groowt/view/web/ast/node/EmptyClosureValueNode.java new file mode 100644 index 0000000..0bb4cf8 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/EmptyClosureValueNode.java @@ -0,0 +1,39 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.ast.extension.GroovyCodeNodeExtension; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; +import org.antlr.v4.runtime.Token; + +import java.util.List; +import java.util.function.Function; + +public class EmptyClosureValueNode extends AbstractLeafNode implements ValueNode { + + private final GroovyCodeNodeExtension groovyCode; + + @Inject + public EmptyClosureValueNode(NodeExtensionContainer extensionContainer, @Given TokenRange tokenRange) { + super(tokenRange, extensionContainer); + this.groovyCode = this.createGroovyCode(); + } + + protected GroovyCodeNodeExtension createGroovyCode() { + return this.createExtension( + GroovyCodeNodeExtension.class, + TokenRange.empty(), + (Function, String>) this::toValidGroovyCode + ); + } + + public GroovyCodeNodeExtension getGroovyCode() { + return this.groovyCode; + } + + protected String toValidGroovyCode(List tokens) { + return "{ }"; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/FragmentComponentNode.java b/web-views/src/main/java/groowt/view/web/ast/node/FragmentComponentNode.java new file mode 100644 index 0000000..fc2a364 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/FragmentComponentNode.java @@ -0,0 +1,20 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; +import org.jetbrains.annotations.Nullable; + +public non-sealed class FragmentComponentNode extends ComponentNode { + + @Inject + public FragmentComponentNode( + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given @Nullable BodyNode body + ) { + super(tokenRange, extensionContainer, filterNulls(body), body); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/GStringBodyTextNode.java b/web-views/src/main/java/groowt/view/web/ast/node/GStringBodyTextNode.java new file mode 100644 index 0000000..ade8096 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/GStringBodyTextNode.java @@ -0,0 +1,34 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.ast.extension.GStringNodeExtension; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; + +import java.util.List; + +public class GStringBodyTextNode extends AbstractTreeNode implements BodyChildNode { + + protected static List checkChildren(List children) { + for (final var child : children) { + if (!(child instanceof JStringBodyTextNode || child.hasExtension(GStringNodeExtension.class))) { + throw new IllegalArgumentException( + "Children of GStringBodyTextNode must be either a JStringBodyTextNode, " + + "or have a GStringNodeExtension." + ); + } + } + return children; + } + + @Inject + public GStringBodyTextNode( + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given List children + ) { + super(tokenRange, extensionContainer, checkChildren(children)); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/GStringValueNode.java b/web-views/src/main/java/groowt/view/web/ast/node/GStringValueNode.java new file mode 100644 index 0000000..cf8d2e6 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/GStringValueNode.java @@ -0,0 +1,46 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.extension.GroovyCodeNodeExtension; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; +import org.antlr.v4.runtime.Token; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class GStringValueNode extends AbstractLeafNode implements ValueNode { + + private final GroovyCodeNodeExtension groovyCode; + + @Inject + public GStringValueNode( + TokenList tokenList, + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given int contentIndex + ) { + super(tokenRange, extensionContainer); + this.groovyCode = this.createGroovyCode(tokenList, contentIndex); + } + + protected GroovyCodeNodeExtension createGroovyCode(TokenList tokenList, int contentIndex) { + return this.createExtension( + GroovyCodeNodeExtension.class, + TokenRange.fromIndex(tokenList, contentIndex), + (Function, String>) this::toValidGroovyCode + ); + } + + protected String toValidGroovyCode(List tokens) { + return "\"" + tokens.stream().map(Token::getText).collect(Collectors.joining()) + "\""; + } + + public GroovyCodeNodeExtension getGroovyCode() { + return this.groovyCode; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/JStringBodyTextNode.java b/web-views/src/main/java/groowt/view/web/ast/node/JStringBodyTextNode.java new file mode 100644 index 0000000..a12042b --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/JStringBodyTextNode.java @@ -0,0 +1,26 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; + +public class JStringBodyTextNode extends AbstractLeafNode implements BodyChildNode { + + private final String content; + + @Inject + public JStringBodyTextNode( + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given String content + ) { + super(tokenRange, extensionContainer); + this.content = content; + } + + public String getContent() { + return this.content; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/JStringValueNode.java b/web-views/src/main/java/groowt/view/web/ast/node/JStringValueNode.java new file mode 100644 index 0000000..8beda10 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/JStringValueNode.java @@ -0,0 +1,26 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; + +public class JStringValueNode extends AbstractLeafNode implements ValueNode { + + private final String content; + + @Inject + public JStringValueNode( + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given String content + ) { + super(tokenRange, extensionContainer); + this.content = content; + } + + public String getContent() { + return this.content; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/KeyNode.java b/web-views/src/main/java/groowt/view/web/ast/node/KeyNode.java new file mode 100644 index 0000000..d0a246f --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/KeyNode.java @@ -0,0 +1,28 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; + +public class KeyNode extends AbstractLeafNode { + + private final String key; + + @Inject + public KeyNode( + TokenList tokenList, + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given int keyTokenIndex + ) { + super(tokenRange, extensionContainer); + this.key = tokenList.get(keyTokenIndex).getText(); + } + + public String getKey() { + return this.key; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/KeyValueAttrNode.java b/web-views/src/main/java/groowt/view/web/ast/node/KeyValueAttrNode.java new file mode 100644 index 0000000..b0f1316 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/KeyValueAttrNode.java @@ -0,0 +1,27 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; + +public non-sealed class KeyValueAttrNode extends AttrNode { + + private final ValueNode valueNode; + + @Inject + public KeyValueAttrNode( + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given KeyNode keyNode, + @Given ValueNode valueNode + ) { + super(tokenRange, extensionContainer, checkForNulls(keyNode, valueNode.asNode()), keyNode); + this.valueNode = valueNode; + } + + public ValueNode getValueNode() { + return this.valueNode; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/LeafNode.java b/web-views/src/main/java/groowt/view/web/ast/node/LeafNode.java new file mode 100644 index 0000000..ae132a0 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/LeafNode.java @@ -0,0 +1,3 @@ +package groowt.view.web.ast.node; + +public non-sealed interface LeafNode extends Node {} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/Node.java b/web-views/src/main/java/groowt/view/web/ast/node/Node.java new file mode 100644 index 0000000..e1471fa --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/Node.java @@ -0,0 +1,22 @@ +package groowt.view.web.ast.node; + +import groowt.util.extensible.Extensible; +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.extension.NodeExtension; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.ast.extension.NodeExtensionFactory; +import groowt.view.web.util.TokenRange; +import org.antlr.v4.runtime.Token; + +import java.util.stream.Collectors; + +public sealed interface Node extends Extensible + permits TreeNode, LeafNode { + + TokenRange getTokenRange(); + + default String getText(TokenList tokenList) { + return tokenList.getRange(this.getTokenRange()).stream().map(Token::getText).collect(Collectors.joining()); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/PlainScriptletNode.java b/web-views/src/main/java/groowt/view/web/ast/node/PlainScriptletNode.java new file mode 100644 index 0000000..d98aee4 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/PlainScriptletNode.java @@ -0,0 +1,52 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.extension.GroovyCodeNodeExtension; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; +import org.antlr.v4.runtime.Token; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class PlainScriptletNode extends AbstractLeafNode implements BodyChildNode { + + private final int groovyIndex; + private final GroovyCodeNodeExtension groovyCode; + + @Inject + public PlainScriptletNode( + TokenList tokenList, + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given int groovyIndex + ) { + super(tokenRange, extensionContainer); + this.groovyIndex = groovyIndex; + this.groovyCode = this.createGroovyCode(tokenList); + } + + protected GroovyCodeNodeExtension createGroovyCode(TokenList tokenList) { + return this.createExtension( + GroovyCodeNodeExtension.class, + TokenRange.fromIndex(tokenList, this.groovyIndex), + (Function, String>) this::toValidGroovyCode + ); + } + + protected String toValidGroovyCode(List groovyTokens) { + return "{ Writer out ->\n" + groovyTokens.stream().map(Token::getText).collect(Collectors.joining()) + "\n}"; + } + + public int getGroovyIndex() { + return this.groovyIndex; + } + + public GroovyCodeNodeExtension getGroovyCode() { + return this.groovyCode; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/PreambleNode.java b/web-views/src/main/java/groowt/view/web/ast/node/PreambleNode.java new file mode 100644 index 0000000..9bc9f91 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/PreambleNode.java @@ -0,0 +1,26 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; + +public class PreambleNode extends AbstractLeafNode { + + private final int groovyIndex; + + @Inject + public PreambleNode( + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given int groovyCodeIndex + ) { + super(tokenRange, extensionContainer); + this.groovyIndex = groovyCodeIndex; + } + + public int getGroovyCodeIndex() { + return this.groovyIndex; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/StringComponentTypeNode.java b/web-views/src/main/java/groowt/view/web/ast/node/StringComponentTypeNode.java new file mode 100644 index 0000000..6327864 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/StringComponentTypeNode.java @@ -0,0 +1,21 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; + +public non-sealed class StringComponentTypeNode extends ComponentTypeNode { + + @Inject + public StringComponentTypeNode( + TokenList tokenList, + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given int typeTokenIndex + ) { + super(tokenList, tokenRange, extensionContainer, TokenRange.fromIndex(tokenList, typeTokenIndex)); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/TreeNode.java b/web-views/src/main/java/groowt/view/web/ast/node/TreeNode.java new file mode 100644 index 0000000..22ffd61 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/TreeNode.java @@ -0,0 +1,11 @@ +package groowt.view.web.ast.node; + +import java.util.List; + +public non-sealed interface TreeNode extends Node { + List getChildren(); + Node getAt(int index); + C getAt(int index, Class childClass); + int indexOf(Node child); + int getChildrenSize(); +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/TypedComponentNode.java b/web-views/src/main/java/groowt/view/web/ast/node/TypedComponentNode.java new file mode 100644 index 0000000..be7256b --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/TypedComponentNode.java @@ -0,0 +1,28 @@ +package groowt.view.web.ast.node; + +import groowt.util.di.annotation.Given; +import groowt.view.web.ast.extension.NodeExtensionContainer; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; +import org.jetbrains.annotations.Nullable; + +public non-sealed class TypedComponentNode extends ComponentNode { + + private final ComponentArgsNode args; + + @Inject + public TypedComponentNode( + NodeExtensionContainer extensionContainer, + @Given TokenRange tokenRange, + @Given ComponentArgsNode args, + @Given @Nullable BodyNode children + ) { + super(tokenRange, extensionContainer, filterNulls(args, children), children); + this.args = args; + } + + public ComponentArgsNode getArgs() { + return this.args; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/ValueNode.java b/web-views/src/main/java/groowt/view/web/ast/node/ValueNode.java new file mode 100644 index 0000000..49a71a5 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/ast/node/ValueNode.java @@ -0,0 +1,13 @@ +package groowt.view.web.ast.node; + +public interface ValueNode { + + default Node asNode() { + return (Node) this; + } + + default T asNode(Class nodeType) { + return nodeType.cast(this); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/lib/FragmentComponent.java b/web-views/src/main/java/groowt/view/web/lib/FragmentComponent.java new file mode 100644 index 0000000..c68927b --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/lib/FragmentComponent.java @@ -0,0 +1,8 @@ +package groowt.view.web.lib; + +import groowt.view.web.DefaultWebViewComponent; + +// TODO: anything special? +public class FragmentComponent extends DefaultWebViewComponent { + +} diff --git a/web-views/src/main/java/groowt/view/web/runtime/DefaultSourceMapDeserializer.java b/web-views/src/main/java/groowt/view/web/runtime/DefaultSourceMapDeserializer.java new file mode 100644 index 0000000..1869760 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/runtime/DefaultSourceMapDeserializer.java @@ -0,0 +1,47 @@ +package groowt.view.web.runtime; + +import groowt.view.web.transpile.DefaultSourceMapSerializer; +import groowt.view.web.transpile.SourceMap.SourceMapEntry; +import groowt.view.web.transpile.SourceMapDeserializer; +import groowt.view.web.util.SourcePosition; + +import java.util.ArrayList; +import java.util.List; + +public final class DefaultSourceMapDeserializer implements SourceMapDeserializer { + + @Override + public List deserialize(String serializedSourceMap) { + final List result = new ArrayList<>(); + int line = -1; + SourcePosition from = null; + StringBuilder numberBuilder = new StringBuilder(); + for (int i = 0; i < serializedSourceMap.length(); i++) { + final char c = serializedSourceMap.charAt(i); + if (c == DefaultSourceMapSerializer.LINE_COL_SEP) { + line = Integer.parseInt(numberBuilder.toString()); + numberBuilder = new StringBuilder(); + } else if (c == DefaultSourceMapSerializer.POS_SEP) { + if (line == -1) { + throw new IllegalStateException(); + } + final int col = Integer.parseInt(numberBuilder.toString()); + from = new SourcePosition(line, col); + numberBuilder = new StringBuilder(); + } else if (c == DefaultSourceMapSerializer.ENTRY_SEP) { + if (from == null) { + throw new IllegalStateException(); + } + final int col = Integer.parseInt(numberBuilder.toString()); + result.add(new SourceMapEntry(from, new SourcePosition(line, col))); + line = -1; + from = null; + numberBuilder = new StringBuilder(); + } else { + numberBuilder.append(c); + } + } + return result; + } + +} 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..9adec95 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollector.java @@ -0,0 +1,34 @@ +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 WebViewComponentChildCollector { + + private final List children = new ArrayList<>(); + + public void add(String jString, Closure renderer) { + this.children.add(new WebViewChildJStringRenderer(jString, renderer)); + } + + public void add(GString gString, Closure renderer) { + this.children.add(new WebViewChildGStringRenderer(gString, renderer)); + } + + public void add(ViewComponent component, Closure renderer) { + this.children.add(new WebViewChildComponentRenderer(component, renderer)); + } + + public List getChildren() { + return this.children; + } + +} 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 new file mode 100644 index 0000000..7fbf07b --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentWriter.java @@ -0,0 +1,82 @@ +package groowt.view.web.runtime; + +import groovy.lang.GString; +import groowt.view.component.ComponentRenderException; +import groowt.view.component.ViewComponent; + +import java.io.IOException; +import java.io.Writer; + +public class WebViewComponentWriter { + + private final Writer delegate; + + public WebViewComponentWriter(Writer delegate) { + this.delegate = delegate; + } + + public void append(String string) { + try { + this.delegate.append(string); + } catch (IOException ioException) { + throw new RuntimeException(ioException); + } + } + + public void append(GString gString) { + final String content; + try { + content = gString.toString(); + } catch (Exception exception) { + throw new ComponentRenderException(exception); + } + try { + this.delegate.append(content); + } catch (IOException ioException) { + throw new RuntimeException(ioException); + } + } + + public void append(GString gString, int line, int column) { + final String content; + try { + content = gString.toString(); + } catch (Exception exception) { + throw new ComponentRenderException(line, column, exception); + } + try { + this.delegate.append(content); + } catch (IOException ioException) { + throw new RuntimeException(ioException); + } + } + + public void append(ViewComponent viewComponent) { + try { + viewComponent.renderTo(this.delegate); + } catch (IOException ioException) { + throw new RuntimeException(ioException); + } catch (Exception exception) { + throw new ComponentRenderException(viewComponent, exception); + } + } + + public void append(ViewComponent viewComponent, int line, int column) { + try { + viewComponent.renderTo(this.delegate); + } catch (IOException ioException) { + throw new RuntimeException(ioException); + } catch (Exception exception) { + throw new ComponentRenderException(viewComponent, line, column, exception); + } + } + + public void append(Object object) { + try { + this.delegate.append(object.toString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/BodyTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/BodyTranspiler.java new file mode 100644 index 0000000..3710dda --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/BodyTranspiler.java @@ -0,0 +1,26 @@ +package groowt.view.web.transpile; + +import groowt.view.web.ast.node.BodyNode; +import groowt.view.web.ast.node.Node; +import groowt.view.web.transpile.TranspilerUtil.TranspilerState; +import org.codehaus.groovy.ast.VariableScope; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.Statement; + +import java.util.List; + +public interface BodyTranspiler { + + @FunctionalInterface + interface ExpressionStatementConverter { + Statement createStatement(Node source, Expression expression); + } + + BlockStatement transpileBody( + BodyNode bodyNode, + ExpressionStatementConverter converter, + TranspilerState state + ); + +} 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 new file mode 100644 index 0000000..ebb2397 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/ComponentTranspiler.java @@ -0,0 +1,15 @@ +package groowt.view.web.transpile; + +import groowt.view.web.ast.node.ComponentNode; +import groowt.view.web.transpile.TranspilerUtil.TranspilerState; +import org.codehaus.groovy.ast.Variable; +import org.codehaus.groovy.ast.expr.VariableExpression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.Statement; + +public interface ComponentTranspiler { + BlockStatement createComponentStatements( + ComponentNode componentNode, + TranspilerState state + ); +} 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 new file mode 100644 index 0000000..d68db22 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultBodyTranspiler.java @@ -0,0 +1,74 @@ +package groowt.view.web.transpile; + +import groowt.view.web.ast.node.*; +import groowt.view.web.transpile.TranspilerUtil.TranspilerState; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.codehaus.groovy.ast.expr.GStringExpression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.Statement; + +import java.util.ArrayList; +import java.util.List; + +@Singleton +public class DefaultBodyTranspiler implements BodyTranspiler { + + private final GStringTranspiler gStringTranspiler; + private final JStringTranspiler jStringTranspiler; + private final ComponentTranspiler componentTranspiler; + + @Inject + public DefaultBodyTranspiler( + GStringTranspiler gStringTranspiler, + JStringTranspiler jStringTranspiler, + ComponentTranspiler componentTranspiler + ) { + this.gStringTranspiler = gStringTranspiler; + this.jStringTranspiler = jStringTranspiler; + this.componentTranspiler = componentTranspiler; + } + + @Override + public BlockStatement transpileBody( + BodyNode bodyNode, + ExpressionStatementConverter converter, + TranspilerState state + ) { + final BlockStatement block = new BlockStatement(); + block.setVariableScope(state.currentScope()); + for (final Node child : bodyNode.getChildren()) { + switch (child) { + case GStringBodyTextNode gStringBodyTextNode -> { + final GStringExpression gString = this.gStringTranspiler.createGStringExpression( + gStringBodyTextNode + ); + block.addStatement(converter.createStatement(gStringBodyTextNode, gString)); + } + case JStringBodyTextNode jStringBodyTextNode -> { + block.addStatement( + converter.createStatement( + jStringBodyTextNode, + this.jStringTranspiler.createStringLiteral(jStringBodyTextNode) + ) + ); + } + case ComponentNode componentNode -> { + final BlockStatement componentBlock = this.componentTranspiler.createComponentStatements( + componentNode, + state + ); + block.addStatement(componentBlock); + } + case PlainScriptletNode plainScriptletNode -> { + throw new UnsupportedOperationException("TODO"); + } + default -> throw new UnsupportedOperationException( + "BodyNode child of type " + child.getClass().getSimpleName() + " is not supported." + ); + } + } + 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 new file mode 100644 index 0000000..ab91044 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java @@ -0,0 +1,371 @@ +package groowt.view.web.transpile; + +import groowt.view.component.*; +import groowt.view.web.ast.node.*; +import groowt.view.web.lib.FragmentComponent; +import groowt.view.web.runtime.WebViewComponentChildCollector; +import groowt.view.web.transpile.TranspilerUtil.TranspilerState; +import groowt.view.web.transpile.util.GroovyUtil; +import groowt.view.web.transpile.util.GroovyUtil.ConvertResult; +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 static groowt.view.web.transpile.TranspilerUtil.lineAndColumn; +import static groowt.view.web.transpile.TranspilerUtil.makeStringLiteral; + +public class DefaultComponentTranspiler implements ComponentTranspiler { + + private static final ClassNode VIEW_COMPONENT = ClassHelper.make(ViewComponent.class); + private static final ClassNode CHILD_COLLECTOR = ClassHelper.make(WebViewComponentChildCollector.class); + + private static final ClassNode EXCEPTION = ClassHelper.make(Exception.class); + private static final ClassNode COMPONENT_CREATE = ClassHelper.make(ComponentCreateException.class); + + private static final ClassNode NO_FACTORY_MISSING_EXCEPTION = ClassHelper.make(NoFactoryMissingException.class); + + 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 RESOLVE = "resolve"; + private static final String ADD = "add"; + private static final String APPEND = "append"; + private static final String FRAGMENT_FQN = FragmentComponent.class.getCanonicalName(); + + private ValueNodeTranspiler valueNodeTranspiler; + private BodyTranspiler bodyTranspiler; + + public void setValueNodeTranspiler(ValueNodeTranspiler valueNodeTranspiler) { + this.valueNodeTranspiler = valueNodeTranspiler; + } + + public void setBodyTranspiler(BodyTranspiler bodyTranspiler) { + this.bodyTranspiler = bodyTranspiler; + } + + // ViewComponent c0 + protected ExpressionStatement getComponentDeclaration(Variable component) { + final var componentDeclaration = new DeclarationExpression( + new VariableExpression(component), + new Token(Types.ASSIGN, "=", -1, -1), + EmptyExpression.INSTANCE + ); + return new ExpressionStatement(componentDeclaration); + } + + // '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; + }; + 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); + } + + // key: value + protected MapEntryExpression getAttrExpression( + AttrNode attrNode, TranspilerState state + ) { + final var keyExpr = makeStringLiteral(attrNode.getKeyNode().getKey()); + final Expression valueExpr = switch (attrNode) { + case BooleanValueAttrNode ignored -> ConstantExpression.PRIM_TRUE; + case KeyValueAttrNode keyValueAttrNode -> + this.valueNodeTranspiler.createExpression(keyValueAttrNode.getValueNode(), state); + }; + return new MapEntryExpression(keyExpr, valueExpr); + } + + // [key: value, ...] + protected MapExpression getAttrMap(List attributeNodes, TranspilerState state) { + if (attributeNodes.isEmpty()) { + throw new IllegalArgumentException("attributeNodes cannot be empty"); + } + final var result = new MapExpression(); + attributeNodes.stream() + .map(attrNode -> this.getAttrExpression(attrNode, state)) + .forEach(result::addMapEntryExpression); + return result; + } + + // arg0, arg1, arg2, etc + protected List getConstructorArgs(ComponentConstructorNode componentConstructorNode) { + final ConvertResult convertResult = GroovyUtil.convert(componentConstructorNode.getGroovyCode() + .getAsValidGroovyCode()); + final var blockStatement = convertResult.blockStatement(); + if (blockStatement == null) { + throw new IllegalStateException("Did not expect blockStatement to be null"); + } + final var statements = blockStatement.getStatements(); + if (statements.size() != 1) { + throw new IllegalStateException("statements size is not 1"); + } + final ExpressionStatement exprStmt = (ExpressionStatement) statements.getFirst(); + final ListExpression listExpr = (ListExpression) exprStmt.getExpression(); + return listExpr.getExpressions(); + } + + private void addLineAndColumn(Node sourceNode, ArgumentListExpression args) { + final var lineAndColumn = lineAndColumn(sourceNode.getTokenRange().getStartPosition()); + args.addExpression(lineAndColumn.getV1()); + args.addExpression(lineAndColumn.getV2()); + } + + protected MethodCallExpression getOutCall(Node 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, args); + case ComponentNode ignored -> this.addLineAndColumn(sourceNode, args); + default -> { + } + } + return new MethodCallExpression(outVariableExpr, APPEND, args); + } + + // { out << jString | gString | component } + protected ClosureExpression getOutClosure(Node 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( + Node sourceNode, + TranspilerState state, + Variable childCollector, + Expression toAdd + ) { + final var childCollectorVariableExpr = new VariableExpression(childCollector); + final ClosureExpression renderChild = this.getOutClosure(sourceNode, state, toAdd); + final MethodCallExpression methodCall = new MethodCallExpression( + childCollectorVariableExpr, + ADD, + new ArgumentListExpression(List.of(toAdd, renderChild)) + ); + return new ExpressionStatement(methodCall); + } + + // { WebViewComponentChildCollector c0_childCollector -> ... } + protected ClosureExpression getBodyClosure(BodyNode bodyNode, TranspilerState state, String componentVariableName) { + final Parameter childCollectorParam = new Parameter( + CHILD_COLLECTOR, + componentVariableName + "_childCollector" + ); + + final var scope = state.pushScope(); + scope.putDeclaredVariable(childCollectorParam); + final BlockStatement bodyStatements = this.bodyTranspiler.transpileBody( + bodyNode, + (sourceNode, expr) -> this.getChildCollectorAdd(sourceNode, state, childCollectorParam, expr), + state + ); + state.popScope(); + + return new ClosureExpression(new Parameter[]{childCollectorParam}, bodyStatements); + } + + // context.create(...) {...} + protected MethodCallExpression getCreateExpression( + ComponentNode componentNode, TranspilerState state, String componentVariableName + ) { + final var createArgs = new ArgumentListExpression(); + + final var contextResolve = this.getContextResolveExpr(componentNode, state.context()); + createArgs.addExpression(contextResolve); + + if (componentNode instanceof TypedComponentNode typedComponentNode) { + 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); + } + } + + final @Nullable BodyNode bodyNode = componentNode.getBody(); + if (bodyNode != null) { + createArgs.addExpression(this.getBodyClosure(bodyNode, state, componentVariableName)); + } + + return new MethodCallExpression(new VariableExpression(state.context()), CREATE, createArgs); + } + + // c0 = context.create(context.resolve(''), [:], ...) {...} + protected ExpressionStatement getCreateAssignStatement( + ComponentNode componentNode, TranspilerState state, String componentVariableName + ) { + final var componentAssignLeft = new VariableExpression(state.getDeclaredVariable(componentVariableName)); + final var createExpr = this.getCreateExpression(componentNode, state, componentVariableName); + final var componentAssignExpr = new BinaryExpression( + componentAssignLeft, + new Token(Types.ASSIGN, "=", -1, -1), + createExpr + ); + 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); + + 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); + final VariableExpression exceptionVar = new VariableExpression(exceptionName); + + final var lineAndColumn = lineAndColumn(componentNode.getTokenRange().getStartPosition()); + + final ConstructorCallExpression cce = new ConstructorCallExpression( + COMPONENT_CREATE, + new ArgumentListExpression(List.of( + VariableExpression.THIS_EXPRESSION, + lineAndColumn.getV1(), + lineAndColumn.getV2(), + exceptionVar + )) + ); + final Statement throwCcStmt = new ThrowStatement(cce); + return new CatchStatement(exceptionParam, throwCcStmt); + } + + 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; + } + + 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 Statement createComponentOutCall( + ComponentNode componentNode, + TranspilerState state, + Variable component + ) { + // out << c0 + final VariableExpression toOutput = new VariableExpression(component); + final Expression outCallExpr = this.getOutCall(componentNode, state, toOutput); + return new ExpressionStatement(outCallExpr); + } + + protected String getComponentVariableName(int componentNumber) { + return "c" + componentNumber; + } + + @Override + public BlockStatement createComponentStatements( + ComponentNode componentNode, + TranspilerState state + ) { + final var componentVariableName = this.getComponentVariableName(state.newComponentNumber()); + final Variable component = new VariableExpression(componentVariableName, VIEW_COMPONENT); + + final BlockStatement result = new BlockStatement(); + final VariableScope scope = state.pushScope(); + result.setVariableScope(scope); + scope.putDeclaredVariable(component); + + // ViewComponent c0; + result.addStatement(this.getComponentDeclaration(component)); + + // try { context.create(...) } catch { ... } + final var tryCreateStatement = new TryCatchStatement(this.getCreateAssignStatement( + componentNode, + state, + componentVariableName + ), EmptyStatement.INSTANCE); + this.getCreateCatches(componentNode, componentVariableName).forEach(tryCreateStatement::addCatch); + result.addStatement(tryCreateStatement); + + // component.setContext(context) + result.addStatement(this.createSetContext(state, component)); + + // out << component + result.addStatement(this.createComponentOutCall(componentNode, state, component)); + + state.popScope(); + + return result; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultGStringTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultGStringTranspiler.java new file mode 100644 index 0000000..e32ccf1 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultGStringTranspiler.java @@ -0,0 +1,186 @@ +package groowt.view.web.transpile; + +import groowt.view.web.antlr.MergedGroovyCodeToken; +import groowt.view.web.antlr.WebViewComponentsLexer; +import groowt.view.web.ast.extension.GStringNodeExtension; +import groowt.view.web.ast.extension.GStringPathExtension; +import groowt.view.web.ast.extension.GStringScriptletExtension; +import groowt.view.web.ast.node.GStringBodyTextNode; +import groowt.view.web.ast.node.JStringBodyTextNode; +import groowt.view.web.ast.node.Node; +import groowt.view.web.util.FilteringIterable; +import groowt.view.web.util.Option; +import groowt.view.web.util.TokenRange; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.antlr.v4.runtime.Token; +import org.codehaus.groovy.ast.expr.*; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; +import java.util.stream.Collectors; + +@Singleton +public class DefaultGStringTranspiler implements GStringTranspiler { + + private final PositionSetter positionSetter; + private final JStringTranspiler jStringTranspiler; + + @Inject + public DefaultGStringTranspiler(PositionSetter positionSetter, JStringTranspiler jStringTranspiler) { + this.positionSetter = positionSetter; + this.jStringTranspiler = jStringTranspiler; + } + + protected Option checkPrevBeforeDollar(@Nullable Node prev, Node current) { + if (!(prev instanceof JStringBodyTextNode)) { + return Option.liftLazy(() -> { + final ConstantExpression expression = this.jStringTranspiler.createEmptyStringLiteral(); + this.positionSetter.setToStartOf(expression, current); + return expression; + }); + } else { + return Option.empty(); + } + } + + protected Option checkNextAfterDollar(Node current, @Nullable Node next) { + if (!(next instanceof JStringBodyTextNode)) { + return Option.liftLazy(() -> { + final ConstantExpression expression = this.jStringTranspiler.createEmptyStringLiteral(); + if (next != null) { + this.positionSetter.setToStartOf(expression, next); + } else { + this.positionSetter.setToStartOf(expression, current); + } + return expression; + }); + } else { + return Option.empty(); + } + } + + protected ConstantExpression handleText(JStringBodyTextNode jStringBodyTextNode, @Nullable Node prev) { + if (prev instanceof JStringBodyTextNode) { + throw new IllegalStateException("Cannot have two texts in a row"); + } + return this.jStringTranspiler.createStringLiteral(jStringBodyTextNode); + } + + protected record PathResult( + Expression result, Option before, + Option after + ) {} + + protected PathResult handlePath(Node current, GStringPathExtension path, @Nullable Node prev, @Nullable Node next) { + final List groowtTokens = path.getRawTokens(); + + VariableExpression begin = null; + PropertyExpression propertyExpression = null; + + for (final Token groowtToken : groowtTokens) { + if (groowtToken instanceof MergedGroovyCodeToken groovyCodeToken) { + final Iterable identifierTokenIterable = FilteringIterable.continuingUntilSuccess( + groovyCodeToken.getOriginals(), + token -> token.getType() == WebViewComponentsLexer.GStringIdentifier + ); + for (final Token identifierToken : identifierTokenIterable) { + final String identifier = identifierToken.getText(); + final TokenRange identifierTokenRange = TokenRange.of(identifierToken); + if (begin == null) { + begin = new VariableExpression(identifier); + this.positionSetter.setPosition(begin, identifierTokenRange); + } else if (propertyExpression == null) { + propertyExpression = new PropertyExpression(begin, identifier); + this.positionSetter.setPosition(propertyExpression, identifierTokenRange); + } else { + propertyExpression = new PropertyExpression(propertyExpression, identifier); + this.positionSetter.setPosition(propertyExpression, identifierTokenRange); + } + } + } else { + throw new IllegalStateException("Received a non-MergedGroovyToken from a GStringExtension"); + } + } + + if (begin == null) { + throw new IllegalStateException("begin is null!"); + } + + if (propertyExpression != null) { + return new PathResult( + propertyExpression, + this.checkPrevBeforeDollar(prev, current), + this.checkNextAfterDollar(current, next) + ); + } else { + return new PathResult( + begin, + this.checkPrevBeforeDollar(prev, current), + this.checkNextAfterDollar(current, next) + ); + } + } + + @Override + public GStringExpression createGStringExpression(GStringBodyTextNode gStringBodyTextNode) { + final var children = gStringBodyTextNode.getChildren(); + if (children.isEmpty()) { + throw new IllegalArgumentException("Cannot make a gStringOutStatement from zero GStringParts"); + } + + final String verbatimText = children.stream().map(node -> { + if (node instanceof JStringBodyTextNode jStringBodyTextNode) { + return jStringBodyTextNode.getContent(); + } else if (node.hasExtension(GStringNodeExtension.class)) { + final var gString = node.getExtension(GStringNodeExtension.class); + return gString.getAsValidEmbeddableCode(); + } else { + throw new IllegalArgumentException( + "Cannot get verbatim text when one of the given parts has " + + "neither a JStringNodeExtension nor a GStringNodeExtension" + ); + } + }).collect(Collectors.joining()); + + final List texts = new ArrayList<>(); + final List values = new ArrayList<>(); + final ListIterator iter = children.listIterator(); + + while (iter.hasNext()) { + final var prev = iter.previousIndex() > -1 ? children.get(iter.previousIndex()) : null; + final var current = iter.next(); + final var next = iter.nextIndex() < children.size() ? children.get(iter.nextIndex()) : null; + if (current instanceof JStringBodyTextNode jStringBodyTextNode) { + texts.add(this.handleText(jStringBodyTextNode, prev)); + } else { + switch (current.getExtension(GStringNodeExtension.class)) { + case GStringPathExtension path -> { + final var pathResult = this.handlePath(current, path, prev, next); + pathResult.before().ifPresent(texts::add); + values.add(pathResult.result()); + pathResult.after().ifPresent(texts::add); + } + case GStringScriptletExtension scriptlet -> { + checkPrevBeforeDollar(prev, current).ifPresent(texts::add); + // TODO + checkNextAfterDollar(current, next).ifPresent(texts::add); + } + } + } + } + + if (texts.size() != values.size() + 1) { + throw new IllegalStateException( + "incorrect amount of texts vs. values: " + texts.size() + " " + values.size() + ); + } + + final var gString = new GStringExpression(verbatimText, texts, values); + this.positionSetter.setPosition(gString, gStringBodyTextNode); + return gString; + } + +} 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 new file mode 100644 index 0000000..79729cd --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultGroovyTranspiler.java @@ -0,0 +1,159 @@ +package groowt.view.web.transpile; + +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.node.BodyNode; +import groowt.view.web.ast.node.CompilationUnitNode; +import groowt.view.web.transpile.PreambleTranspiler.PreambleResult; +import org.codehaus.groovy.ast.*; +import org.codehaus.groovy.ast.expr.ClosureExpression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +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.SourceUnit; +import org.codehaus.groovy.control.io.ReaderSource; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import static groowt.view.web.transpile.TranspilerUtil.*; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; + +/** + * Takes a Groovy {@link CompilationUnit} in the constructor and adds a {@link SourceUnit} representing the target + * Groovy code for the template represented by the AST passed to {@link #transpile}. + *

+ * Note that while the terminology is similar, a Groovy {@link CompilationUnit} is distinct from our own + * {@link CompilationUnitNode}. + */ +public class DefaultGroovyTranspiler implements GroovyTranspiler { + + private final CompilationUnit groovyCompilationUnit; + private final String defaultPackageName; + private final Supplier configurationSupplier; + + public DefaultGroovyTranspiler( + CompilationUnit groovyCompilationUnit, + @Nullable String defaultPackageName, + Supplier configurationSupplier + ) { + 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(); + } + } + + // Cases: + // - no preamble -> create our own class + // - some preamble, but no script -> create our own class but use imports/packageName from preamble + // - 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( + CompilationUnitNode compilationUnitNode, + TokenList tokens, + String ownerComponentName, + ReaderSource readerSource + ) { + final var configuration = this.getConfiguration(); + final String templateName = ownerComponentName + "Template"; + + final var sourceUnit = new WebViewComponentSourceUnit( + templateName, + readerSource, + this.groovyCompilationUnit.getConfiguration(), + this.groovyCompilationUnit.getClassLoader(), + this.groovyCompilationUnit.getErrorCollector() + ); + final var moduleNode = new WebViewComponentModuleNode(sourceUnit); + sourceUnit.setModuleNode(moduleNode); + + ClassNode mainClassNode; + + final PreambleResult preambleResult = configuration.getPreambleTranspiler().getPreambleResult( + compilationUnitNode.getPreambleNode(), + templateName, + tokens + ); + if (preambleResult.moduleNode() != null) { + WebViewComponentModuleNode.copyTo(preambleResult.moduleNode(), moduleNode); + } + if (preambleResult.scriptClass() != null) { + mainClassNode = preambleResult.scriptClass(); + // do not add it to moduleNode because it's already there + } else { + final String packageName = this.getPackageName(moduleNode); + final String templateClassName = packageName + "." + templateName; + + mainClassNode = new ClassNode( + templateClassName, + ACC_PUBLIC, + ClassHelper.OBJECT_TYPE + ); + mainClassNode.setScript(true); + mainClassNode.addInterface(TranspilerUtil.COMPONENT_TEMPLATE); + + moduleNode.addClass(mainClassNode); + } + + final var renderBlock = new BlockStatement(); + + final TranspilerState state = TranspilerState.withDefaultRootScope(); + renderBlock.setVariableScope(state.currentScope()); + + final BodyNode bodyNode = compilationUnitNode.getBodyNode(); + if (bodyNode != null) { + final var outStatementFactory = configuration.getOutStatementFactory(); + renderBlock.addStatement( + configuration.getBodyTranspiler() + .transpileBody( + compilationUnitNode.getBodyNode(), + (ignored, expr) -> outStatementFactory.create(expr), + state + ) + ); + } + + final ClosureExpression renderer = new ClosureExpression( + new Parameter[] { + (Parameter) state.getDeclaredVariable(CONTEXT), + (Parameter) state.getDeclaredVariable(OUT) + }, + renderBlock + ); + final Statement returnRendererStmt = new ReturnStatement(renderer); + + final var voidClosure = ClassHelper.CLOSURE_TYPE.getPlainNodeReference(); + voidClosure.setGenericsTypes(new GenericsType[] { new GenericsType(ClassHelper.void_WRAPPER_TYPE) }); + + final var getRenderer = new MethodNode( + GET_RENDERER, + ACC_PUBLIC, + voidClosure, + Parameter.EMPTY_ARRAY, + ClassNode.EMPTY_ARRAY, + returnRendererStmt + ); + mainClassNode.addMethod(getRenderer); + + this.groovyCompilationUnit.addSource(sourceUnit); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultJStringTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultJStringTranspiler.java new file mode 100644 index 0000000..b252ae6 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultJStringTranspiler.java @@ -0,0 +1,48 @@ +package groowt.view.web.transpile; + +import groowt.view.web.ast.node.JStringBodyTextNode; +import groowt.view.web.ast.node.JStringValueNode; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.codehaus.groovy.ast.expr.ConstantExpression; + +import static org.apache.groovy.parser.antlr4.util.StringUtils.*; + +@Singleton +public class DefaultJStringTranspiler implements JStringTranspiler { + + private final PositionSetter positionSetter; + + @Inject + public DefaultJStringTranspiler(PositionSetter positionSetter) { + this.positionSetter = positionSetter; + } + + @Override + public ConstantExpression createStringLiteral(JStringBodyTextNode jStringBodyTextNode) { + final var withoutCR = removeCR(jStringBodyTextNode.getContent()); + final var escaped = replaceEscapes(withoutCR, NONE_SLASHY); + final var expression = new ConstantExpression(escaped); + expression.setNodeMetaData("_IS_STRING", true); + this.positionSetter.setPosition(expression, jStringBodyTextNode); + return expression; + } + + @Override + public ConstantExpression createStringLiteral(JStringValueNode jStringValueNode) { + final var content = jStringValueNode.getContent(); + final var escaped = replaceEscapes(content, NONE_SLASHY); + final var expression = new ConstantExpression(escaped); + expression.setNodeMetaData("_IS_STRING", true); + this.positionSetter.setPosition(expression, jStringValueNode); + return expression; + } + + @Override + public ConstantExpression createEmptyStringLiteral() { + final var expression = new ConstantExpression(""); + expression.setNodeMetaData("_IS_STRING", true); + return expression; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultPreambleTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultPreambleTranspiler.java new file mode 100644 index 0000000..7719be0 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultPreambleTranspiler.java @@ -0,0 +1,32 @@ +package groowt.view.web.transpile; + +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.node.PreambleNode; +import groowt.view.web.transpile.util.GroovyUtil; +import org.antlr.v4.runtime.Token; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class DefaultPreambleTranspiler implements PreambleTranspiler { + + @Override + public PreambleResult getPreambleResult( + @Nullable PreambleNode preambleNode, + String templateName, + TokenList tokens + ) { + if (preambleNode == null) { + return new PreambleResult(null, null, List.of()); + } else { + final Token groovyToken = tokens.getGroovyToken(preambleNode.getGroovyCodeIndex()); + final GroovyUtil.ConvertResult convertResult = GroovyUtil.convert(groovyToken.getText(), templateName); + return new PreambleResult( + convertResult.moduleNode(), + convertResult.scriptClass(), + convertResult.classNodes() + ); + } + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultSourceMapSerializer.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultSourceMapSerializer.java new file mode 100644 index 0000000..18ab4fa --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultSourceMapSerializer.java @@ -0,0 +1,33 @@ +package groowt.view.web.transpile; + +import groowt.view.web.util.SourcePosition; + +public final class DefaultSourceMapSerializer implements SourceMapSerializer { + + public static final char LINE_COL_SEP = ','; + public static final char POS_SEP = '.'; + public static final char ENTRY_SEP = ':'; + + private void serialize(StringBuilder sb, SourcePosition sourcePosition) { + final var line = sourcePosition.line(); + final var column = sourcePosition.column(); + sb.append(line).append(LINE_COL_SEP).append(column); + } + + @Override + public String serialize(SourceMap sourceMap) { + final StringBuilder sb = new StringBuilder(); + final var iter = sourceMap.getAll().iterator(); + while (iter.hasNext()) { + final var entry = iter.next(); + this.serialize(sb, entry.from()); + sb.append(POS_SEP); + this.serialize(sb, entry.to()); + if (iter.hasNext()) { + sb.append(ENTRY_SEP); + } + } + return sb.toString(); + } + +} 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 new file mode 100644 index 0000000..9327e8c --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultTranspilerConfiguration.java @@ -0,0 +1,38 @@ +package groowt.view.web.transpile; + +import jakarta.inject.Inject; + +public class DefaultTranspilerConfiguration implements TranspilerConfiguration { + + private final OutStatementFactory outStatementFactory = new SimpleOutStatementFactory(); + private final PreambleTranspiler preambleTranspiler = new DefaultPreambleTranspiler(); + private final BodyTranspiler bodyTranspiler; + + @Inject + public DefaultTranspilerConfiguration() { + final var positionSetter = new SimplePositionSetter(); + final var jStringTranspiler = new DefaultJStringTranspiler(positionSetter); + final var gStringTranspiler = new DefaultGStringTranspiler(positionSetter, jStringTranspiler); + final var componentTranspiler = new DefaultComponentTranspiler(); + this.bodyTranspiler = new DefaultBodyTranspiler(gStringTranspiler, jStringTranspiler, componentTranspiler); + componentTranspiler.setBodyTranspiler(this.bodyTranspiler); + final var valueNodeTranspiler = new DefaultValueNodeTranspiler(componentTranspiler); + componentTranspiler.setValueNodeTranspiler(valueNodeTranspiler); + } + + @Override + public PreambleTranspiler getPreambleTranspiler() { + return this.preambleTranspiler; + } + + @Override + public BodyTranspiler getBodyTranspiler() { + return this.bodyTranspiler; + } + + @Override + public OutStatementFactory getOutStatementFactory() { + return this.outStatementFactory; + } + +} 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 new file mode 100644 index 0000000..3f603e9 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultValueNodeTranspiler.java @@ -0,0 +1,81 @@ +package groowt.view.web.transpile; + +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; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +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; + +public class DefaultValueNodeTranspiler implements ValueNodeTranspiler { + + private final ComponentTranspiler componentTranspiler; + + public DefaultValueNodeTranspiler(ComponentTranspiler componentTranspiler) { + this.componentTranspiler = componentTranspiler; + } + + protected ClosureExpression closureValue(ClosureValueNode closureValueNode) { + final var rawCode = closureValueNode.getGroovyCode().getAsValidGroovyCode(); + final ConvertResult convertResult = GroovyUtil.convert(rawCode); + final @Nullable BlockStatement blockStatement = convertResult.blockStatement(); + if (blockStatement == null || blockStatement.isEmpty()) { + throw new IllegalStateException("block statement is null or empty"); + } + final ExpressionStatement exprStmt = (ExpressionStatement) blockStatement.getStatements().getFirst(); + return (ClosureExpression) exprStmt.getExpression(); + } + + private Expression gStringValue(GStringValueNode gStringValueNode) { + final var rawCode = gStringValueNode.getGroovyCode().getAsValidGroovyCode(); + final ConvertResult convertResult = GroovyUtil.convert(rawCode); + final @Nullable BlockStatement blockStatement = convertResult.blockStatement(); + if (blockStatement == null || blockStatement.isEmpty()) { + throw new IllegalStateException("block statement is null or empty"); + } + final ExpressionStatement exprStmt = (ExpressionStatement) blockStatement.getStatements().getFirst(); + return exprStmt.getExpression(); + } + + private ConstantExpression jStringValue(JStringValueNode jStringValueNode) { + return makeStringLiteral(jStringValueNode.getContent()); + } + + private ClosureExpression emptyClosureValue(EmptyClosureValueNode emptyClosureValueNode) { + return new ClosureExpression(Parameter.EMPTY_ARRAY, EmptyStatement.INSTANCE); + } + + private ClosureExpression componentValue(ComponentValueNode componentValueNode, TranspilerState state) { + return new ClosureExpression( + Parameter.EMPTY_ARRAY, + this.componentTranspiler.createComponentStatements( + componentValueNode.getComponentNode(), + state + ) + ); + } + + @Override + public Expression createExpression(ValueNode valueNode, TranspilerState state) { + return switch (valueNode) { + case ClosureValueNode closureValueNode -> this.closureValue(closureValueNode); + case GStringValueNode gStringValueNode -> this.gStringValue(gStringValueNode); + case JStringValueNode jStringValueNode -> this.jStringValue(jStringValueNode); + case EmptyClosureValueNode emptyClosureValueNode -> this.emptyClosureValue(emptyClosureValueNode); + case ComponentValueNode componentValueNode -> this.componentValue(componentValueNode, state); + default -> throw new IllegalArgumentException( + "Unsupported ValueNode type: " + valueNode.getClass() + ); + }; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/GStringTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/GStringTranspiler.java new file mode 100644 index 0000000..7676cc6 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/GStringTranspiler.java @@ -0,0 +1,11 @@ +package groowt.view.web.transpile; + +import groowt.view.web.ast.node.GStringBodyTextNode; +import groowt.view.web.ast.node.Node; +import org.codehaus.groovy.ast.expr.GStringExpression; + +import java.util.List; + +public interface GStringTranspiler { + GStringExpression createGStringExpression(GStringBodyTextNode parent); +} 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 new file mode 100644 index 0000000..2d5b494 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/GroovyTranspiler.java @@ -0,0 +1,16 @@ +package groowt.view.web.transpile; + +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.node.CompilationUnitNode; +import org.codehaus.groovy.control.io.ReaderSource; + +public interface GroovyTranspiler { + + void transpile( + CompilationUnitNode compilationUnitNode, + TokenList tokens, + String ownerComponentName, + ReaderSource readerSource + ); + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/JStringTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/JStringTranspiler.java new file mode 100644 index 0000000..51b13bb --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/JStringTranspiler.java @@ -0,0 +1,12 @@ +package groowt.view.web.transpile; + +import groowt.view.web.ast.node.JStringBodyTextNode; +import groowt.view.web.ast.node.JStringValueNode; +import groowt.view.web.ast.node.Node; +import org.codehaus.groovy.ast.expr.ConstantExpression; + +public interface JStringTranspiler { + ConstantExpression createStringLiteral(JStringBodyTextNode bodyTextNode); + ConstantExpression createStringLiteral(JStringValueNode jStringValueNode); + ConstantExpression createEmptyStringLiteral(); +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/OutStatementFactory.java b/web-views/src/main/java/groowt/view/web/transpile/OutStatementFactory.java new file mode 100644 index 0000000..039dfbc --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/OutStatementFactory.java @@ -0,0 +1,8 @@ +package groowt.view.web.transpile; + +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.stmt.Statement; + +public interface OutStatementFactory { + Statement create(Expression rightSide); +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/PositionSetter.java b/web-views/src/main/java/groowt/view/web/transpile/PositionSetter.java new file mode 100644 index 0000000..fa330f1 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/PositionSetter.java @@ -0,0 +1,12 @@ +package groowt.view.web.transpile; + +import groowt.view.web.ast.node.Node; +import groowt.view.web.util.TokenRange; +import org.codehaus.groovy.ast.ASTNode; + +public interface PositionSetter { + void setPosition(ASTNode target, TokenRange tokenRange); + void setPosition(ASTNode target, Node source); + void setPosition(ASTNode target, Node start, Node end); + void setToStartOf(ASTNode target, Node source); +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/PreambleTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/PreambleTranspiler.java new file mode 100644 index 0000000..267e507 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/PreambleTranspiler.java @@ -0,0 +1,20 @@ +package groowt.view.web.transpile; + +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.node.PreambleNode; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ModuleNode; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public interface PreambleTranspiler { + + record PreambleResult( + @Nullable ModuleNode moduleNode, @Nullable ClassNode scriptClass, + List allClasses + ) {} + + PreambleResult getPreambleResult(@Nullable PreambleNode preambleNode, String templateName, TokenList tokens); + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/SimpleOutStatementFactory.java b/web-views/src/main/java/groowt/view/web/transpile/SimpleOutStatementFactory.java new file mode 100644 index 0000000..02bde05 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/SimpleOutStatementFactory.java @@ -0,0 +1,27 @@ +package groowt.view.web.transpile; + +import groowt.view.web.transpile.util.GroovyUtil; +import org.codehaus.groovy.ast.expr.BinaryExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import org.codehaus.groovy.ast.expr.VariableExpression; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.Statement; +import org.codehaus.groovy.syntax.Types; + +import static org.codehaus.groovy.syntax.Token.newSymbol; + +public class SimpleOutStatementFactory implements OutStatementFactory { + + @Override + public Statement create(Expression rightSide) { + final VariableExpression out = new VariableExpression("out"); + final MethodCallExpression methodCallExpression = new MethodCallExpression( + out, + "append", + rightSide + ); + return new ExpressionStatement(methodCallExpression); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/SimplePositionSetter.java b/web-views/src/main/java/groowt/view/web/transpile/SimplePositionSetter.java new file mode 100644 index 0000000..f82776f --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/SimplePositionSetter.java @@ -0,0 +1,47 @@ +package groowt.view.web.transpile; + +import groowt.view.web.ast.node.Node; +import groowt.view.web.util.SourcePosition; +import groowt.view.web.util.TokenRange; +import org.codehaus.groovy.ast.ASTNode; + +public class SimplePositionSetter implements PositionSetter { + + protected void set(ASTNode target, int startLine, int startColumn, int endLine, int endColumn) { + target.setLineNumber(startLine); + target.setColumnNumber(startColumn); + target.setLastLineNumber(endLine); + target.setLastColumnNumber(endColumn); + } + + protected void set(ASTNode target, SourcePosition start, SourcePosition end) { + this.set(target, start.line(), start.column(), end.line(), end.column()); + } + + @Override + public void setPosition(ASTNode target, TokenRange tokenRange) { + this.set(target, tokenRange.getStartPosition(), tokenRange.getEndPosition()); + } + + @Override + public void setPosition(ASTNode target, Node source) { + this.setPosition(target, source.getTokenRange()); + } + + @Override + public void setPosition(ASTNode target, Node start, Node end) { + final var startPosition = start.getTokenRange().getStartPosition(); + target.setLineNumber(startPosition.line()); + target.setColumnNumber(startPosition.column()); + final var endPosition = end.getTokenRange().getEndPosition(); + target.setLastLineNumber(endPosition.line()); + target.setLastColumnNumber(endPosition.column()); + } + + @Override + public void setToStartOf(ASTNode target, Node source) { + final var tokenRange = source.getTokenRange(); + this.set(target, tokenRange.getStartPosition(), tokenRange.getStartPosition()); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/SourceMap.java b/web-views/src/main/java/groowt/view/web/transpile/SourceMap.java new file mode 100644 index 0000000..21dce76 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/SourceMap.java @@ -0,0 +1,58 @@ +package groowt.view.web.transpile; + +import groowt.view.web.util.SourcePosition; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +public final class SourceMap { + + public record SourceMapEntry(SourcePosition from, SourcePosition to) {} + + private final Map mapFromTo = new LinkedHashMap<>(); + + public SourceMap() {} + + public SourceMap(String serialized, SourceMapDeserializer deserializer) { + this.putAll(deserializer.deserialize(serialized)); + } + + public void put(SourcePosition from, SourcePosition to) { + this.mapFromTo.put(from, to); + } + + public void put(SourceMapEntry entry) { + this.mapFromTo.put(entry.from, entry.to); + } + + public void putAll(Map map) { + this.mapFromTo.putAll(map); + } + + public void putAll(SourceMap sourceMap) { + this.mapFromTo.putAll(sourceMap.mapFromTo); + } + + public void putAll(List entries) { + entries.forEach(this::put); + } + + public SourcePosition getTo(SourcePosition from) { + return this.mapFromTo.getOrDefault(from, SourcePosition.UNKNOWN); + } + + public Stream stream() { + return this.mapFromTo.entrySet().stream().map(entry -> new SourceMapEntry(entry.getKey(), entry.getValue())); + } + + public List getAll() { + return this.stream().toList(); + } + + public String serialize(SourceMapSerializer serializer) { + return serializer.serialize(this); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/SourceMapDeserializer.java b/web-views/src/main/java/groowt/view/web/transpile/SourceMapDeserializer.java new file mode 100644 index 0000000..a419da3 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/SourceMapDeserializer.java @@ -0,0 +1,8 @@ +package groowt.view.web.transpile; + +import java.util.List; + +@FunctionalInterface +public interface SourceMapDeserializer { + List deserialize(String serializedSourceMap); +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/SourceMapSerializer.java b/web-views/src/main/java/groowt/view/web/transpile/SourceMapSerializer.java new file mode 100644 index 0000000..48b391f --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/SourceMapSerializer.java @@ -0,0 +1,6 @@ +package groowt.view.web.transpile; + +@FunctionalInterface +public interface SourceMapSerializer { + String serialize(SourceMap sourceMap); +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/TranspilerConfiguration.java b/web-views/src/main/java/groowt/view/web/transpile/TranspilerConfiguration.java new file mode 100644 index 0000000..1306cf8 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/TranspilerConfiguration.java @@ -0,0 +1,7 @@ +package groowt.view.web.transpile; + +public interface TranspilerConfiguration { + PreambleTranspiler getPreambleTranspiler(); + BodyTranspiler getBodyTranspiler(); + OutStatementFactory getOutStatementFactory(); +} 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 new file mode 100644 index 0000000..d155298 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/TranspilerUtil.java @@ -0,0 +1,108 @@ +package groowt.view.web.transpile; + +import groovy.lang.Tuple2; +import groowt.view.component.ComponentContext; +import groowt.view.component.ComponentTemplate; +import groowt.view.web.runtime.WebViewComponentWriter; +import groowt.view.web.util.SourcePosition; +import org.codehaus.groovy.ast.*; +import org.codehaus.groovy.ast.expr.ConstantExpression; + +import java.util.Deque; +import java.util.LinkedList; +import java.util.Objects; +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 String GROOWT_VIEW_WEB = "groowt.view.web"; + public static final String OUT = "out"; + public static final String CONTEXT = "context"; + public static final String GET_RENDERER = "getRenderer"; + + public static Tuple2 lineAndColumn(SourcePosition sourcePosition) { + return new Tuple2<>( + new ConstantExpression(sourcePosition.line(), true), + new ConstantExpression(sourcePosition.column(), true) + ); + } + + public static ConstantExpression makeStringLiteral(String content) { + final var e = new ConstantExpression(content); + e.setNodeMetaData("_IS_STRING", true); + return e; + } + + 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); + + final VariableScope rootScope = new VariableScope(); + rootScope.putDeclaredVariable(contextParam); + rootScope.putDeclaredVariable(outParam); + + return new TranspilerState(rootScope); + } + + public static TranspilerState withRootScope(VariableScope rootScope) { + return new TranspilerState(rootScope); + } + + private final AtomicInteger componentCounter = new AtomicInteger(); + private final Deque scopeStack = new LinkedList<>(); + + private TranspilerState(VariableScope rootScope) { + this.scopeStack.push(rootScope); + } + + public VariableScope pushScope() { + final VariableScope parent = this.scopeStack.peek(); + final VariableScope result = new VariableScope(parent); + this.scopeStack.push(result); + return result; + } + + public void popScope() { + this.scopeStack.pop(); + } + + public VariableScope currentScope() { + return Objects.requireNonNull(this.scopeStack.peek()); + } + + public int newComponentNumber() { + return this.componentCounter.getAndIncrement(); + } + + public Variable out() { + return this.getDeclaredVariable(OUT); + } + + public Variable context() { + return this.getDeclaredVariable(CONTEXT); + } + + public Variable getDeclaredVariable(String name) { + VariableScope scope = this.currentScope(); + while (scope != null) { + final Variable potential = scope.getDeclaredVariable(name); + if (potential != null) { + return potential; + } else { + scope = scope.getParent(); + } + } + throw new NullPointerException("Cannot find variable: " + name); + } + + } + + private TranspilerUtil() {} + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/ValueNodeTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/ValueNodeTranspiler.java new file mode 100644 index 0000000..829fc78 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/ValueNodeTranspiler.java @@ -0,0 +1,14 @@ +package groowt.view.web.transpile; + +import groowt.view.web.ast.node.ValueNode; +import groowt.view.web.transpile.TranspilerUtil.TranspilerState; +import org.codehaus.groovy.ast.expr.Expression; + +public interface ValueNodeTranspiler { + + Expression createExpression( + ValueNode valueNode, + TranspilerState state + ); + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentModuleNode.java b/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentModuleNode.java new file mode 100644 index 0000000..a8a46fb --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentModuleNode.java @@ -0,0 +1,125 @@ +package groowt.view.web.transpile; + +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.CompileUnit; +import org.codehaus.groovy.ast.ImportNode; +import org.codehaus.groovy.ast.ModuleNode; +import org.codehaus.groovy.control.SourceUnit; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class WebViewComponentModuleNode extends ModuleNode { + + public static void copyTo(ModuleNode from, WebViewComponentModuleNode to) { + to.setDescription(from.getDescription()); + to.setPackage(from.getPackage()); + to.setPackageName(from.getPackageName()); + to.setImportsResolved(from.hasImportsResolved()); + to.setMetaDataMap(from.getMetaDataMap()); + from.getImports().forEach(to::addImport); + from.getStarImports().forEach(to::addStarImport); + from.getStaticImports().forEach(to::addStaticImport); + from.getStaticStarImports().forEach(to::addStaticStarImport); + from.getClasses().forEach(to::addClass); + } + + protected final List imports = new ArrayList<>(); + protected final List starImports = new ArrayList<>(); + protected final Map staticImports = new LinkedHashMap<>(); + protected final Map staticStarImports = new LinkedHashMap<>(); + + public WebViewComponentModuleNode(SourceUnit context) { + super(context); + } + + @Override + public List getImports() { + final List r = new ArrayList<>(super.getImports()); + r.addAll(this.imports); + return r; + } + + @Override + public List getStarImports() { + final List r = new ArrayList<>(super.getStarImports()); + r.addAll(this.starImports); + return r; + } + + @Override + public Map getStaticImports() { + final Map r = new HashMap<>(super.getStaticImports()); + r.putAll(this.staticImports); + return r; + } + + @Override + public Map getStaticStarImports() { + final Map r = new HashMap<>(super.getStaticStarImports()); + r.putAll(this.staticStarImports); + return r; + } + + @Override + public ClassNode getImportType(String alias) { + final ClassNode superResult = super.getImportType(alias); + if (superResult != null) { + return superResult; + } else { + return Optional.ofNullable(this.getImport(alias)).map(ImportNode::getType).orElse(null); + } + } + + @Override + public @Nullable ImportNode getImport(String alias) { + final ImportNode superResult = super.getImport(alias); + if (superResult != null) { + return superResult; + } else { + final Map aliases = this.getNodeMetaData("import.aliases", x -> { + return this.imports.stream() + .collect(Collectors.toMap(ImportNode::getAlias, + Function.identity(), + (first, second) -> second + )); + }); + return aliases.get(alias); + } + } + + // Copied from super + protected void storeLastAddedImportNode(ImportNode importNode) { + if (this.getNodeMetaData(ImportNode.class) == ImportNode.class) { + this.putNodeMetaData(ImportNode.class, importNode); + } + } + + public void addImport(ImportNode importNode) { + this.imports.add(importNode); + this.removeNodeMetaData("import.aliases"); + this.storeLastAddedImportNode(importNode); + } + + public void addStarImport(ImportNode importNode) { + this.starImports.add(importNode); + this.storeLastAddedImportNode(importNode); + } + + public void addStaticImport(String alias, ImportNode importNode) { + final ImportNode prev = this.staticImports.put(alias, importNode); + if (prev != null) { + this.staticImports.put(prev.toString(), prev); + this.staticImports.put(alias, this.staticImports.remove(alias)); + } + this.storeLastAddedImportNode(importNode); + } + + public void addStaticStarImport(String alias, ImportNode importNode) { + this.staticStarImports.put(alias, importNode); + this.storeLastAddedImportNode(importNode); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentReaderSource.java b/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentReaderSource.java new file mode 100644 index 0000000..c058b96 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentReaderSource.java @@ -0,0 +1,265 @@ +package groowt.view.web.transpile; + +import groowt.view.web.util.Range; +import org.codehaus.groovy.control.HasCleanup; +import org.codehaus.groovy.control.Janitor; +import org.codehaus.groovy.control.io.ReaderSource; +import org.jetbrains.annotations.Nullable; + +import java.io.*; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@Deprecated +public class WebViewComponentReaderSource implements ReaderSource { + + public record LineAndColumnRange(LineAndColumn start, LineAndColumn end) {} + + public record LineAndColumn(int line, int column) {} + + public static final LineAndColumn minusOneLineAndColumn = new LineAndColumn(-1, -1); + public static final LineAndColumnRange minusOneLineAndColumnRange = new LineAndColumnRange( + minusOneLineAndColumn, + minusOneLineAndColumn + ); + + protected interface Source { + Reader toReader() throws IOException; + @Nullable String getLine(int lineNumber); + @Nullable URI toUri(); + LineAndColumnRange convertToLineAndColumnRange(Range range); + LineAndColumn convertToLineAndColumn(int index); + } + + protected static abstract class AbstractSource implements Source { + + @Override + public LineAndColumnRange convertToLineAndColumnRange(Range range) { + try (final Reader reader = new BufferedReader(this.toReader())) { + int read = reader.read(); + boolean enteredRangeYet = false; + int startLine = -1; + int startColumn = -1; + int index = 0; + int curLine = 0; + int curColumn = 1; + while (read > -1) { + if ((char) read == '\n') { + reader.mark(1); + final int next = reader.read(); + if ((char) next != '\r') { + reader.reset(); + } + curLine++; + curColumn = 1; + } else if ((char) read == '\r') { + curLine++; + curColumn = 1; + } + if (!enteredRangeYet && range.isInRange(index)) { + enteredRangeYet = true; + startLine = curLine; + startColumn = curColumn; + } else if (enteredRangeYet && !range.isInRange(index)) { + break; + } + read = reader.read(); + if (read > -1) { + index++; + curColumn++; + } + } + return new LineAndColumnRange( + new LineAndColumn(startLine, startColumn), + new LineAndColumn(curLine, curColumn) + ); + } catch (IOException ignored) { + return minusOneLineAndColumnRange; + } finally { + if (this instanceof HasCleanup hasCleanup) { + hasCleanup.cleanup(); + } + } + } + + @Override + public LineAndColumn convertToLineAndColumn(final int index) { + try (final Reader reader = new BufferedReader(this.toReader())) { + int read = reader.read(); + int currentIndex = 0; + int currentLine = 0; + int currentColumn = 1; + while (read > -1 && currentIndex <= index) { + if ((char) read == '\n') { + reader.mark(1); + final int next = reader.read(); + if ((char) next != '\r') { + reader.reset(); + } + currentLine++; + currentColumn = 1; + } else if ((char) read == '\r') { + currentLine++; + currentColumn = 1; + } + read = reader.read(); + if (read > -1) { + currentIndex++; + currentColumn++; + } + } + return new LineAndColumn(currentLine, currentColumn); + } catch (IOException ignored) { + return minusOneLineAndColumn; + } finally { + if (this instanceof HasCleanup hasCleanup) { + hasCleanup.cleanup(); + } + } + } + + } + + protected static abstract class AbstractSourceWithHasCleanup extends AbstractSource implements HasCleanup { + + private final Collection createdReaders = new ArrayList<>(); + + protected void addReader(Reader reader) { + this.createdReaders.add(reader); + } + + @Override + public void cleanup() { + this.createdReaders.forEach(reader -> { + try { + reader.close(); + } catch (IOException ignored) { + } + }); + } + + } + + protected static final class FileSource extends AbstractSourceWithHasCleanup { + + private final File source; + @Nullable + private List cachedLines; + private boolean triedFetchingLines; + + public FileSource(File source) { + this.source = source; + } + + @Override + public Reader toReader() throws IOException { + final Reader r = new FileReader(this.source); + this.addReader(r); + return r; + } + + private void initCachedLines() { + try (final FileInputStream inputStream = new FileInputStream(this.source)) { + final byte[] allBytes = inputStream.readAllBytes(); + final String allSource = new String(allBytes); + this.cachedLines = allSource.lines().toList(); + } catch (IOException ignored) { + // ignored + } finally { + this.triedFetchingLines = true; + } + } + + @Override + public @Nullable String getLine(int lineNumber) { + if (!this.triedFetchingLines) { + this.initCachedLines(); + } + return this.cachedLines != null ? this.cachedLines.get(lineNumber) : null; + } + + @Override + public URI toUri() { + return this.source.toURI(); + } + + } + + protected static final class StringSource extends AbstractSourceWithHasCleanup { + + private final String source; + private List cachedLines; + + public StringSource(String source) { + this.source = source; + } + + @Override + public Reader toReader() throws IOException { + final Reader r = new StringReader(this.source); + this.addReader(r); + return r; + } + + @Override + public String getLine(int lineNumber) { + if (this.cachedLines == null) { + this.cachedLines = this.source.lines().toList(); + } + return this.cachedLines.get(lineNumber); + } + + @Override + public @Nullable URI toUri() { + return null; + } + + } + + private final Source source; + + public WebViewComponentReaderSource(File sourceFile) { + this.source = new FileSource(sourceFile); + } + + public WebViewComponentReaderSource(String sourceString) { + this.source = new StringSource(sourceString); + } + + @Override + public Reader getReader() throws IOException { + return this.source.toReader(); + } + + @Override + public boolean canReopenSource() { + return true; + } + + @Override + public String getLine(int lineNumber, Janitor janitor) { + if (lineNumber < 0) { + return null; + } + final var line = this.source.getLine(lineNumber); + if (this.source instanceof HasCleanup hasCleanup) { + janitor.register(hasCleanup); + } + return line; + } + + @Override + public void cleanup() { + if (this.source instanceof HasCleanup hasCleanup) { + hasCleanup.cleanup(); + } + } + + @Override + public URI getURI() { + return this.source.toUri(); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentSourceUnit.java b/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentSourceUnit.java new file mode 100644 index 0000000..6f11c87 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentSourceUnit.java @@ -0,0 +1,57 @@ +package groowt.view.web.transpile; + +import groovy.lang.GroovyClassLoader; +import org.codehaus.groovy.ast.ModuleNode; +import org.codehaus.groovy.control.CompilationFailedException; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.ErrorCollector; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.control.io.ReaderSource; +import org.codehaus.groovy.syntax.Reduction; + +import java.util.Objects; + +public class WebViewComponentSourceUnit extends SourceUnit { + + private WebViewComponentModuleNode moduleNode; + + public WebViewComponentSourceUnit( + String name, + ReaderSource source, + CompilerConfiguration configuration, + GroovyClassLoader loader, + ErrorCollector er + ) { + super(name, source, configuration, loader, er); + } + + public void setModuleNode(WebViewComponentModuleNode moduleNode) { + this.moduleNode = moduleNode; + } + + @Override + public Reduction getCST() { + throw new UnsupportedOperationException(); + } + + @Override + public ModuleNode getAST() { + return Objects.requireNonNull(this.moduleNode); + } + + @Override + public void parse() throws CompilationFailedException { + // No-op + } + + @Override + public void convert() throws CompilationFailedException { + // No-op + } + + @Override + public ModuleNode buildAST() { + return this.getAST(); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/util/GroovyPrettyPrinter.java b/web-views/src/main/java/groowt/view/web/transpile/util/GroovyPrettyPrinter.java new file mode 100644 index 0000000..1be4663 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/util/GroovyPrettyPrinter.java @@ -0,0 +1,556 @@ +package groowt.view.web.transpile.util; + +import org.codehaus.groovy.ast.*; +import org.codehaus.groovy.ast.expr.*; +import org.codehaus.groovy.ast.stmt.*; +import org.codehaus.groovy.classgen.BytecodeExpression; +import org.codehaus.groovy.control.SourceUnit; + +import java.util.List; + +public final class GroovyPrettyPrinter extends ClassCodeVisitorSupport { + + public interface OnEachNode { + + /** + * @param typeName the type name of the node being visited + * @param astNode the node + * @param stringBuilder target for your message + * @implNote Do not append a newline at the end of your message, as that is done later by the + * {@link GroovyPrettyPrinter}. + */ + void accept(StringBuilder stringBuilder, String typeName, ASTNode astNode); + + /** + * Called when a visitMethod exposes a list of nodes. See {@link #accept(StringBuilder, String, ASTNode)}. + */ + void accept(StringBuilder stringBuilder, String visitMethodName, List nodeList); + + } + + public static final OnEachNode SIMPLE = new OnEachNode() { + + @Override + public void accept(StringBuilder sb, String typeName, ASTNode astNode) { + sb.append(typeName).append(GroovyUtilKt.formatGroovyPosition(astNode)); + } + + @Override + public void accept(StringBuilder sb, String visitMethodName, List nodeList) { + sb.append(visitMethodName); + } + + }; + + private static final String INDENT = " "; + private final StringBuilder sb = new StringBuilder(); + private final OnEachNode onEachNode; + private int indentTimes; + + public GroovyPrettyPrinter(OnEachNode onEachNode) { + this.onEachNode = onEachNode; + } + + public GroovyPrettyPrinter() { + this.onEachNode = SIMPLE; + } + + public String getResult() { + return this.sb.toString(); + } + + private void before(String name, ASTNode astNode) { + if (astNode != null) { + this.sb.repeat(INDENT, this.indentTimes); + this.onEachNode.accept(this.sb, name, astNode); + this.sb.append("\n"); + } + this.indentTimes++; + } + + private void before(String name, List nodes) { + this.sb.repeat(INDENT, this.indentTimes); + this.onEachNode.accept(this.sb, name, nodes); + this.sb.append("\n"); + this.indentTimes++; + } + + private void before(String name) { + this.sb.repeat(INDENT, this.indentTimes); + this.sb.append("\n"); + this.indentTimes++; + } + + private void after() { + this.indentTimes--; + } + + @Override + protected SourceUnit getSourceUnit() { + return null; + } + + @Override + public void visitClass(ClassNode node) { + this.before("Class", node); + super.visitClass(node); + this.after(); + } + + @Override + public void visitAnnotations(AnnotatedNode node) { + this.before("Annotations", node.getAnnotations()); + super.visitAnnotations(node); + this.after(); + } + + @Override + protected void visitAnnotation(AnnotationNode node) { + this.before("Annotation", node); + super.visitAnnotation(node); + this.after(); + } + + @Override + public void visitPackage(PackageNode node) { + this.before("Package", node); + super.visitPackage(node); + this.after(); + } + + @Override + public void visitImports(ModuleNode node) { + this.before("Imports", node.getImports()); + node.getImports().forEach(this::visitImport); + node.getStarImports().forEach(this::visitImport); + node.getStaticImports().forEach(this::visitImport); + node.getStaticStarImports().forEach(this::visitImport); + this.after(); + } + + public void visitImport(ImportNode importNode) { + this.before("Import", importNode); + this.visitAnnotations(importNode.getAnnotations()); + this.after(); + } + + public void visitImport(String alias, ImportNode importNode) { + this.before("Import", importNode); + this.visitAnnotations(importNode.getAnnotations()); + this.after(); + } + + @Override + public void visitConstructor(ConstructorNode node) { + this.before("Constructor", node); + super.visitConstructor(node); + this.after(); + } + + @Override + public void visitMethod(MethodNode node) { + this.before("Method", node); + super.visitMethod(node); + this.after(); + } + + @Override + public void visitField(FieldNode node) { + this.before("Field", node); + super.visitField(node); + this.after(); + } + + @Override + public void visitProperty(PropertyNode node) { + this.before("Property", node); + super.visitProperty(node); + this.after(); + } + + @Override + protected void visitObjectInitializerStatements(ClassNode node) { + this.before("ObjectInitializerStatements", node.getObjectInitializerStatements()); + super.visitObjectInitializerStatements(node); + this.after(); + } + + @Override + public void visitBlockStatement(BlockStatement block) { + this.before("BlockStatement", block); + super.visitBlockStatement(block); + this.after(); + } + + @Override + public void visitForLoop(ForStatement forLoop) { + this.before("ForLoop", forLoop); + super.visitForLoop(forLoop); + this.after(); + } + + @Override + public void visitWhileLoop(WhileStatement loop) { + this.before("WhileLoop", loop); + super.visitWhileLoop(loop); + this.after(); + } + + @Override + public void visitDoWhileLoop(DoWhileStatement loop) { + this.before("DoWhileLoop", loop); + super.visitDoWhileLoop(loop); + this.after(); + } + + @Override + public void visitIfElse(IfStatement ifElse) { + this.before("IfElse", ifElse); + super.visitIfElse(ifElse); + this.after(); + } + + @Override + public void visitExpressionStatement(ExpressionStatement statement) { + this.before("ExpressionStatement", statement); + super.visitExpressionStatement(statement); + this.after(); + } + + @Override + public void visitReturnStatement(ReturnStatement statement) { + this.before("ReturnStatement", statement); + super.visitReturnStatement(statement); + this.after(); + } + + @Override + public void visitAssertStatement(AssertStatement statement) { + this.before("AssertStatement", statement); + super.visitAssertStatement(statement); + this.after(); + } + + @Override + public void visitTryCatchFinally(TryCatchStatement statement) { + this.before("TryCatchFinally", statement); + super.visitTryCatchFinally(statement); + this.after(); + } + + @Override + public void visitEmptyStatement(EmptyStatement statement) { + this.before("EmptyStatement", statement); + super.visitEmptyStatement(statement); + this.after(); + } + + @Override + public void visitSwitch(SwitchStatement statement) { + this.before("Switch", statement); + super.visitSwitch(statement); + this.after(); + } + + @Override + public void visitCaseStatement(CaseStatement statement) { + this.before("CaseStatement", statement); + super.visitCaseStatement(statement); + this.after(); + } + + @Override + public void visitBreakStatement(BreakStatement statement) { + this.before("BreakStatement", statement); + super.visitBreakStatement(statement); + this.after(); + } + + @Override + public void visitContinueStatement(ContinueStatement statement) { + this.before("ContinueStatement", statement); + super.visitContinueStatement(statement); + this.after(); + } + + @Override + public void visitSynchronizedStatement(SynchronizedStatement statement) { + this.before("SynchronizedStatement", statement); + super.visitSynchronizedStatement(statement); + this.after(); + } + + @Override + public void visitThrowStatement(ThrowStatement statement) { + this.before("ThrowStatement", statement); + super.visitThrowStatement(statement); + this.after(); + } + + @Override + public void visitMethodCallExpression(MethodCallExpression call) { + this.before("MethodCallExpression", call); + super.visitMethodCallExpression(call); + this.after(); + } + + @Override + public void visitStaticMethodCallExpression(StaticMethodCallExpression call) { + this.before("StaticMethodCallExpression", call); + super.visitStaticMethodCallExpression(call); + this.after(); + } + + @Override + public void visitConstructorCallExpression(ConstructorCallExpression call) { + this.before("ConstructorCallExpression", call); + super.visitConstructorCallExpression(call); + this.after(); + } + + @Override + public void visitBinaryExpression(BinaryExpression expression) { + this.before("BinaryExpression", expression); + super.visitBinaryExpression(expression); + this.after(); + } + + @Override + public void visitTernaryExpression(TernaryExpression expression) { + this.before("TernaryExpression", expression); + super.visitTernaryExpression(expression); + this.after(); + } + + @Override + public void visitShortTernaryExpression(ElvisOperatorExpression expression) { + this.before("ShortTernaryExpression", expression); + super.visitShortTernaryExpression(expression); + this.after(); + } + + @Override + public void visitPostfixExpression(PostfixExpression expression) { + this.before("PostfixExpression", expression); + super.visitPostfixExpression(expression); + this.after(); + } + + @Override + public void visitPrefixExpression(PrefixExpression expression) { + this.before("PrefixExpression", expression); + super.visitPrefixExpression(expression); + this.after(); + } + + @Override + public void visitBooleanExpression(BooleanExpression expression) { + this.before("BooleanExpression", expression); + super.visitBooleanExpression(expression); + this.after(); + } + + @Override + public void visitNotExpression(NotExpression expression) { + this.before("NotExpression", expression); + super.visitNotExpression(expression); + this.after(); + } + + @Override + public void visitClosureExpression(ClosureExpression expression) { + this.before("ClosureExpression", expression); + super.visitClosureExpression(expression); + this.after(); + } + + @Override + public void visitLambdaExpression(LambdaExpression expression) { + this.before("LambdaExpression", expression); + super.visitLambdaExpression(expression); + this.after(); + } + + @Override + public void visitTupleExpression(TupleExpression expression) { + this.before("TupleExpression", expression); + super.visitTupleExpression(expression); + this.after(); + } + + @Override + public void visitListExpression(ListExpression expression) { + this.before("ListExpression", expression); + super.visitListExpression(expression); + this.after(); + } + + @Override + public void visitArrayExpression(ArrayExpression expression) { + this.before("ArrayExpression", expression); + super.visitArrayExpression(expression); + this.after(); + } + + @Override + public void visitMapExpression(MapExpression expression) { + this.before("MapExpression", expression); + super.visitMapExpression(expression); + this.after(); + } + + @Override + public void visitMapEntryExpression(MapEntryExpression expression) { + this.before("MapEntryExpression", expression); + super.visitMapEntryExpression(expression); + this.after(); + } + + @Override + public void visitRangeExpression(RangeExpression expression) { + this.before("RangeExpression", expression); + super.visitRangeExpression(expression); + this.after(); + } + + @Override + public void visitSpreadExpression(SpreadExpression expression) { + this.before("SpreadExpression", expression); + super.visitSpreadExpression(expression); + this.after(); + } + + @Override + public void visitSpreadMapExpression(SpreadMapExpression expression) { + this.before("SpreadMapExpression", expression); + super.visitSpreadMapExpression(expression); + this.after(); + } + + @Override + public void visitMethodPointerExpression(MethodPointerExpression expression) { + this.before("MethodPointerExpression", expression); + super.visitMethodPointerExpression(expression); + this.after(); + } + + @Override + public void visitMethodReferenceExpression(MethodReferenceExpression expression) { + this.before("MethodReferenceExpression", expression); + super.visitMethodReferenceExpression(expression); + this.after(); + } + + @Override + public void visitUnaryMinusExpression(UnaryMinusExpression expression) { + this.before("UnaryMinusExpression", expression); + super.visitUnaryMinusExpression(expression); + this.after(); + } + + @Override + public void visitUnaryPlusExpression(UnaryPlusExpression expression) { + this.before("UnaryPlusExpression", expression); + super.visitUnaryPlusExpression(expression); + this.after(); + } + + @Override + public void visitBitwiseNegationExpression(BitwiseNegationExpression expression) { + this.before("BitwiseNegationExpression", expression); + super.visitBitwiseNegationExpression(expression); + this.after(); + } + + @Override + public void visitCastExpression(CastExpression expression) { + this.before("CastExpression", expression); + super.visitCastExpression(expression); + this.after(); + } + + @Override + public void visitConstantExpression(ConstantExpression expression) { + this.before("ConstantExpression", expression); + super.visitConstantExpression(expression); + this.after(); + } + + @Override + public void visitClassExpression(ClassExpression expression) { + this.before("ClassExpression", expression); + super.visitClassExpression(expression); + this.after(); + } + + @Override + public void visitVariableExpression(VariableExpression expression) { + this.before("VariableExpression", expression); + super.visitVariableExpression(expression); + this.after(); + } + + @Override + public void visitDeclarationExpression(DeclarationExpression expression) { + this.before("DeclarationExpression", expression); + super.visitDeclarationExpression(expression); + this.after(); + } + + @Override + public void visitPropertyExpression(PropertyExpression expression) { + this.before("PropertyExpression", expression); + super.visitPropertyExpression(expression); + this.after(); + } + + @Override + public void visitAttributeExpression(AttributeExpression expression) { + this.before("AttributeExpression", expression); + super.visitAttributeExpression(expression); + this.after(); + } + + @Override + public void visitFieldExpression(FieldExpression expression) { + this.before("FieldExpression", expression); + super.visitFieldExpression(expression); + this.after(); + } + + @Override + public void visitGStringExpression(GStringExpression expression) { + this.before("GStringExpression", expression); + super.visitGStringExpression(expression); + this.after(); + } + + @Override + public void visitCatchStatement(CatchStatement statement) { + this.before("CatchStatement", statement); + super.visitCatchStatement(statement); + this.after(); + } + + @Override + public void visitArgumentlistExpression(ArgumentListExpression expression) { + this.before("ArgumentlistExpression", expression); + super.visitArgumentlistExpression(expression); + this.after(); + } + + @Override + public void visitClosureListExpression(ClosureListExpression expression) { + this.before("ClosureListExpression", expression); + super.visitClosureListExpression(expression); + this.after(); + } + + @Override + public void visitBytecodeExpression(BytecodeExpression expression) { + this.before("BytecodeExpression", expression); + super.visitBytecodeExpression(expression); + this.after(); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/util/GroovyUtil.java b/web-views/src/main/java/groowt/view/web/transpile/util/GroovyUtil.java new file mode 100644 index 0000000..28002a8 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/util/GroovyUtil.java @@ -0,0 +1,102 @@ +package groowt.view.web.transpile.util; + +import groovy.lang.GroovyCodeSource; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ModuleNode; +import org.codehaus.groovy.ast.builder.AstStringCompiler; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +public final class GroovyUtil { + + public static String formatGroovy(ASTNode node) { + return formatGroovy(List.of(node), GroovyPrettyPrinter.SIMPLE); + } + + public static String formatGroovy(ASTNode node, GroovyPrettyPrinter.OnEachNode onEachNode) { + return formatGroovy(List.of(node), onEachNode); + } + + public static String formatGroovy(List nodes) { + return formatGroovy(nodes, GroovyPrettyPrinter.SIMPLE); + } + + public static String formatGroovy(List nodes, GroovyPrettyPrinter.OnEachNode onEachNode) { + return nodes.stream().map(node -> { + final var pp = new GroovyPrettyPrinter(onEachNode); + if (node instanceof ClassNode classNode) { + pp.visitClass(classNode); + } else { + node.visit(pp); + } + return pp.getResult(); + }).collect(Collectors.joining("\n")); + } + + public record ConvertResult( + ModuleNode moduleNode, + @Nullable BlockStatement blockStatement, + @Nullable ClassNode scriptClass, + List classNodes + ) {} + + private static String makeScriptClassName() { + return "Script" + System.nanoTime(); + } + + public static ConvertResult convert(String source) { + return convert(source, makeScriptClassName()); + } + + /** + * See {@link AstStringCompiler#compile(String, CompilePhase, boolean)} + */ + public static ConvertResult convert(String source, String scriptClassName) { + final var groovyCodeSource = new GroovyCodeSource( + source, scriptClassName + ".groovy", "/groovy/script" + ); + final var compilationUnit = new CompilationUnit( + CompilerConfiguration.DEFAULT, + groovyCodeSource.getCodeSource(), + null + ); + try { + compilationUnit.addSource(groovyCodeSource.getName(), source); + compilationUnit.compile(CompilePhase.CONVERSION.getPhaseNumber()); + } finally { + try { + compilationUnit.getClassLoader().close(); + } catch (IOException ignored) { + } + } + final List moduleNodes = compilationUnit.getAST().getModules(); + if (moduleNodes.size() != 1) { + throw new RuntimeException("moduleNodes.size() != 1; is actually " + moduleNodes.size()); + } + final ModuleNode moduleNode = moduleNodes.getFirst(); + final BlockStatement blockStatement = moduleNode.getStatementBlock(); + final List classNodes = moduleNode.getClasses(); + ClassNode scriptClassNode = null; + for (final ClassNode classNode : classNodes) { + if (classNode.isScript()) { + if (scriptClassNode == null) { + scriptClassNode = classNode; + } else { + throw new RuntimeException("Found more than one scriptClassNode!"); + } + } + } + return new ConvertResult(moduleNode, blockStatement, scriptClassNode, classNodes); + } + + private GroovyUtil() {} + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/util/GroovyUtil.kt b/web-views/src/main/java/groowt/view/web/transpile/util/GroovyUtil.kt new file mode 100644 index 0000000..2f59d16 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/util/GroovyUtil.kt @@ -0,0 +1,6 @@ +package groowt.view.web.transpile.util + +import org.codehaus.groovy.ast.ASTNode + +fun formatGroovyPosition(astNode: ASTNode): String = + "[${astNode.lineNumber},${astNode.columnNumber}..${astNode.lastLineNumber},${astNode.lastColumnNumber}]" diff --git a/web-views/src/main/java/groowt/view/web/util/AbstractClosedRange.java b/web-views/src/main/java/groowt/view/web/util/AbstractClosedRange.java new file mode 100644 index 0000000..54ab8a5 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/AbstractClosedRange.java @@ -0,0 +1,69 @@ +package groowt.view.web.util; + +import java.util.Objects; + +public abstract class AbstractClosedRange implements ClosedRange { + + protected final T start; + protected final T end; + protected final boolean inclusiveStart; + protected final boolean inclusiveEnd; + protected final RangeIterator.StartIndexFunction startIndexFunction; + + protected AbstractClosedRange( + T start, + T end, + boolean inclusiveEnd, + boolean inclusiveStart, + RangeIterator.StartIndexFunction startIndexFunction + ) { + this.start = Objects.requireNonNull(start); + this.end = Objects.requireNonNull(end); + this.inclusiveStart = inclusiveStart; + this.inclusiveEnd = inclusiveEnd; + this.startIndexFunction = startIndexFunction; + } + + @Override + public T getStart() { + return this.start; + } + + @Override + public T getEnd() { + return this.end; + } + + @Override + public boolean isInclusiveStart() { + return this.inclusiveStart; + } + + @Override + public boolean isInclusiveEnd() { + return this.inclusiveEnd; + } + + protected abstract int compare(T left, T right); + + private boolean compare(T item) { + Objects.requireNonNull(item); + return this.inclusiveStart ? this.compare(item, this.start) >= 0 : this.compare(item, this.start) > 0; + } + + private boolean checkEnd(T item) { + Objects.requireNonNull(item); + return this.inclusiveEnd ? this.compare(item, this.end) <= 0 : this.compare(item, this.end) < 0; + } + + @Override + public boolean isInRange(T item) { + return this.compare(item) && this.checkEnd(item); + } + + @Override + public RangeIterator rangeIterator(RangeIterator.NextSupplier nextSupplier) { + return new SimpleRangeIterator<>(this, nextSupplier, this.startIndexFunction.getStartIndex(this.start)); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/ClosedRange.java b/web-views/src/main/java/groowt/view/web/util/ClosedRange.java new file mode 100644 index 0000000..c6f4f90 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/ClosedRange.java @@ -0,0 +1,25 @@ +package groowt.view.web.util; + +public interface ClosedRange extends Range { + + static ClosedRange intRange(int startAndEnd) { + return intRange(startAndEnd, startAndEnd, false, true); + } + + static ClosedRange intRange(int start, int end) { + return intRange(start, end, false, true); + } + + static ClosedRange intRange(int start, int end, boolean inclusiveEnd) { + return intRange(start, end, inclusiveEnd, true); + } + + static ClosedRange intRange(int start, int end, boolean inclusiveEnd, boolean inclusiveStart) { + return new ComparableClosedRange<>(start, end, inclusiveEnd, inclusiveStart, _start -> _start); + } + + T getEnd(); + boolean isInclusiveStart(); + boolean isInclusiveEnd(); + +} diff --git a/web-views/src/main/java/groowt/view/web/util/ComparableClosedRange.java b/web-views/src/main/java/groowt/view/web/util/ComparableClosedRange.java new file mode 100644 index 0000000..97df0cf --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/ComparableClosedRange.java @@ -0,0 +1,20 @@ +package groowt.view.web.util; + +public final class ComparableClosedRange> extends AbstractClosedRange { + + public ComparableClosedRange( + T start, + T end, + boolean inclusiveEnd, + boolean inclusiveStart, + RangeIterator.StartIndexFunction startIndexFunction + ) { + super(start, end, inclusiveEnd, inclusiveStart, startIndexFunction); + } + + @Override + protected int compare(T left, T right) { + return left.compareTo(right); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/ComparatorClosedRange.java b/web-views/src/main/java/groowt/view/web/util/ComparatorClosedRange.java new file mode 100644 index 0000000..4dba49e --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/ComparatorClosedRange.java @@ -0,0 +1,26 @@ +package groowt.view.web.util; + +import java.util.Comparator; + +public class ComparatorClosedRange extends AbstractClosedRange { + + private final Comparator comparator; + + public ComparatorClosedRange( + T start, + T end, + boolean inclusiveEnd, + boolean inclusiveStart, + Comparator comparator, + RangeIterator.StartIndexFunction startIndexFunction + ) { + super(start, end, inclusiveEnd, inclusiveStart, startIndexFunction); + this.comparator = comparator; + } + + @Override + protected int compare(T left, T right) { + return this.comparator.compare(left, right); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/EmptyRange.java b/web-views/src/main/java/groowt/view/web/util/EmptyRange.java new file mode 100644 index 0000000..449760e --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/EmptyRange.java @@ -0,0 +1,52 @@ +package groowt.view.web.util; + +import org.jetbrains.annotations.NotNull; + +public class EmptyRange implements Range { + + private static final EmptyRange INSTANCE = new EmptyRange<>(); + + @SuppressWarnings("unchecked") + public static EmptyRange get() { + return (EmptyRange) INSTANCE; + } + + protected EmptyRange() {} + + @Override + public boolean isInRange(@NotNull T index) { + return false; + } + + @Override + public @NotNull T getStart() { + throw new UnsupportedOperationException(); + } + + @Override + public RangeIterator rangeIterator(RangeIterator.NextSupplier nextSupplier) { + return new RangeIterator() { + + @Override + public int currentIndex() { + throw new UnsupportedOperationException( + "Cannot currentIndex() on an EmptyRange's RangeIterator. Did you call hasNext() first?" + ); + } + + @Override + public boolean hasNext() { + return false; + } + + @Override + public T next() { + throw new UnsupportedOperationException( + "Cannot next() on an EmptyRange's RangeIterator. Did you call hasNext() first?" + ); + } + + }; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/EmptyTokenRange.java b/web-views/src/main/java/groowt/view/web/util/EmptyTokenRange.java new file mode 100644 index 0000000..579a69c --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/EmptyTokenRange.java @@ -0,0 +1,30 @@ +package groowt.view.web.util; + +import org.antlr.v4.runtime.Token; + +final class EmptyTokenRange extends EmptyRange implements TokenRange { + + private static final EmptyTokenRange instance = new EmptyTokenRange(); + + public static EmptyTokenRange getInstance() { + return instance; + } + + private EmptyTokenRange() {} + + @Override + public Token getEnd() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isInclusiveStart() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isInclusiveEnd() { + throw new UnsupportedOperationException(); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/FilteringIterable.java b/web-views/src/main/java/groowt/view/web/util/FilteringIterable.java new file mode 100644 index 0000000..5cbbbff --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/FilteringIterable.java @@ -0,0 +1,62 @@ +package groowt.view.web.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Predicate; + +public final class FilteringIterable implements Iterable { + + public static FilteringIterable stoppingOnFail(List elements, Predicate filter) { + return stoppingOnFail(elements, filter, ClosedRange.intRange(0, elements.size())); + } + + public static FilteringIterable stoppingOnFail( + List elements, + Predicate filter, + Range range + ) { + return new FilteringIterable<>(elements, filter, FilteringIterator.OnFilterFail.STOP, range); + } + + public static FilteringIterable continuingUntilSuccess( + List elements, + Predicate filter + ) { + return continuingUntilSuccess(elements, filter, ClosedRange.intRange(0, elements.size())); + } + + public static FilteringIterable continuingUntilSuccess( + List elements, + Predicate filter, + Range range + ) { + return new FilteringIterable<>(elements, filter, FilteringIterator.OnFilterFail.CONTINUE_UNTIL_SUCCESS, range); + } + + private final List elements = new ArrayList<>(); + private final Predicate filter; + private final FilteringIterator.OnFilterFail onFilterFail; + private final Range range; + + private FilteringIterable( + List elements, + Predicate filter, + FilteringIterator.OnFilterFail onFilterFail, + Range range + ) { + this.elements.addAll(elements); + this.filter = filter; + this.onFilterFail = onFilterFail; + this.range = range; + } + + @NotNull + @Override + public Iterator iterator() { + return new FilteringIterator<>(this.elements, this.filter, this.onFilterFail, this.range); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/FilteringIterator.java b/web-views/src/main/java/groowt/view/web/util/FilteringIterator.java new file mode 100644 index 0000000..8e431f3 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/FilteringIterator.java @@ -0,0 +1,73 @@ +package groowt.view.web.util; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Predicate; + +final class FilteringIterator implements Iterator { + + public enum OnFilterFail {STOP, CONTINUE_UNTIL_SUCCESS} + + private final List elements = new ArrayList<>(); + private final Predicate filter; + private final OnFilterFail onFilterFail; + private final Range range; + private int currentPosition; + private int lastFetchedPosition = -1; + private T fetched = null; + + public FilteringIterator( + List elements, + Predicate filter, + OnFilterFail onFilterFail, + Range range + ) { + this.elements.addAll(elements); + this.filter = filter; + this.onFilterFail = onFilterFail; + this.range = range; + this.currentPosition = this.range.getStart(); + } + + private void fetchAt(int index) { + if (index != this.lastFetchedPosition) { + if (this.range.isInRange(index) && index < this.elements.size()) { + final T potentialFetched = this.elements.get(index); + if (this.filter.test(potentialFetched)) { + this.fetched = potentialFetched; + this.lastFetchedPosition = index; + } else { + switch (this.onFilterFail) { + case STOP -> { + this.fetched = null; + } + case CONTINUE_UNTIL_SUCCESS -> { + this.fetchAt(index + 1); + } + } + } + } else { + this.fetched = null; + } + } + } + + @Override + public boolean hasNext() { + this.fetchAt(this.currentPosition); + this.currentPosition = this.lastFetchedPosition; + return this.fetched != null; + } + + @Override + public T next() { + if (this.hasNext()) { + this.currentPosition++; + return this.fetched; + } else { + throw new IndexOutOfBoundsException("Cannot next() when hasNext is false."); + } + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/MappingIterable.java b/web-views/src/main/java/groowt/view/web/util/MappingIterable.java new file mode 100644 index 0000000..fc811db --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/MappingIterable.java @@ -0,0 +1,24 @@ +package groowt.view.web.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; +import java.util.function.Function; + +public final class MappingIterable implements Iterable { + + private final Iterable source; + private final Function mapper; + + public MappingIterable(Iterable source, Function mapper) { + this.source = source; + this.mapper = mapper; + } + + @NotNull + @Override + public Iterator iterator() { + return new MappingIterator<>(this.source.iterator(), this.mapper); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/MappingIterator.java b/web-views/src/main/java/groowt/view/web/util/MappingIterator.java new file mode 100644 index 0000000..deef34b --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/MappingIterator.java @@ -0,0 +1,30 @@ +package groowt.view.web.util; + +import java.util.Iterator; +import java.util.function.Function; + +public final class MappingIterator implements Iterator { + + private final Iterator source; + private final Function mapper; + + public MappingIterator(Iterator source, Function mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + public boolean hasNext() { + return this.source.hasNext(); + } + + @Override + public U next() { + if (this.hasNext()) { + return this.mapper.apply(this.source.next()); + } else { + throw new IndexOutOfBoundsException("Cannot next() when hasNext() is false."); + } + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/Monoid.java b/web-views/src/main/java/groowt/view/web/util/Monoid.java new file mode 100644 index 0000000..ae37bd2 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/Monoid.java @@ -0,0 +1,21 @@ +package groowt.view.web.util; + +public final class Monoid { + + private final SemiGroup semiGroup; + private final T empty; + + public Monoid(SemiGroup semiGroup, T empty) { + this.semiGroup = semiGroup; + this.empty = empty; + } + + public T concat(T left, T right) { + return this.semiGroup.concat(left, right); + } + + public T empty() { + return this.empty; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/OpenRange.java b/web-views/src/main/java/groowt/view/web/util/OpenRange.java new file mode 100644 index 0000000..1218760 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/OpenRange.java @@ -0,0 +1,32 @@ +package groowt.view.web.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class OpenRange> implements Range { + + private final T start; + private final RangeIterator.StartIndexFunction startIndexFunction; + + public OpenRange(T start, RangeIterator.StartIndexFunction startIndexFunction) { + this.start = start; + this.startIndexFunction = startIndexFunction; + } + + @Override + public @NotNull T getStart() { + return this.start; + } + + @Override + public boolean isInRange(@NotNull T item) { + return Objects.requireNonNull(item).compareTo(this.start) >= 0; + } + + @Override + public RangeIterator rangeIterator(RangeIterator.NextSupplier nextSupplier) { + return new SimpleRangeIterator<>(this, nextSupplier, this.startIndexFunction.getStartIndex(this.start)); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/Option.java b/web-views/src/main/java/groowt/view/web/util/Option.java new file mode 100644 index 0000000..4ce9b40 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/Option.java @@ -0,0 +1,158 @@ +package groowt.view.web.util; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public abstract sealed class Option { + + private static EmptyOption emptyInstance; + + public static Option lift(T value) { + return new ValueOption<>(Objects.requireNonNull(value)); + } + + public static Option liftNullable(@Nullable T value) { + return value == null ? empty() : lift(value); + } + + public static Option liftLazy(Supplier valueSupplier) { + return new SupplierOption<>(Objects.requireNonNull(valueSupplier)); + } + + @SuppressWarnings("unchecked") + public static Option empty() { + if (emptyInstance == null) { + emptyInstance = new EmptyOption<>(); + } + return (Option) emptyInstance; + } + + private static final class EmptyOption extends Option { + + @Override + public T get() { + throw new NullPointerException("Cannot get() on EmptyOption"); + } + + @Override + public boolean isPresent() { + return false; + } + + } + + private static final class ValueOption extends Option { + + private final T value; + + public ValueOption(T value) { + this.value = value; + } + + @Override + public T get() { + return this.value; + } + + } + + private static final class SupplierOption extends Option { + + private final Supplier valueSupplier; + + public SupplierOption(Supplier valueSupplier) { + this.valueSupplier = valueSupplier::get; + } + + @Override + public T get() { + return Objects.requireNonNull( + this.valueSupplier.get(), + "Cannot get() when the given valueSupplier returns null." + ); + } + + } + + public abstract T get(); + + public boolean isPresent() { + return true; + } + + public @NotNull T getOrElse(T other) { + return this.isPresent() ? this.get() : Objects.requireNonNull(other); + } + + public @Nullable T getOrElseNull() { + return this.isPresent() ? this.get() : null; + } + + public Option orElseLift(T other) { + return this.isPresent() ? this : new ValueOption<>(Objects.requireNonNull(other)); + } + + public Option orElseLiftLazy(Supplier lazyOther) { + return this.isPresent() ? this : new SupplierOption<>(Objects.requireNonNull(lazyOther)); + } + + public Option map(Function mapper) { + return new SupplierOption<>(() -> mapper.apply(this.get())); + } + + public Option mapLazy(Function> lazyMapper) { + return new SupplierOption<>(() -> lazyMapper.apply(this.get()).get()); + } + + public Option flatMap(Function> mapper) { + return new SupplierOption<>(() -> mapper.apply(this.get()).get()); + } + + public Option flatMapLazy(Function>> lazyMapper) { + return new SupplierOption<>(() -> lazyMapper.apply(this.get()).get().get()); + } + + public void ifPresent(Consumer onPresent) { + if (this.isPresent()) { + onPresent.accept(this.get()); + } + } + + public void ifPresentOrElse(Consumer onPresent, Runnable orElse) { + if (this.isPresent()) { + onPresent.accept(this.get()); + } else { + orElse.run(); + } + } + + public R fold(Function onPresent, Supplier onEmpty) { + if (this.isPresent()) { + return onPresent.apply(this.get()); + } else { + return onEmpty.get(); + } + } + + public R foldMap(Monoid monoid, Function onPresent) { + if (this.isPresent()) { + return onPresent.apply(this.get()); + } else { + return monoid.empty(); + } + } + + public R foldFlatMap(Monoid monoid, Function> onPresent) { + if (this.isPresent()) { + return onPresent.apply(this.get()).get(); + } else { + return monoid.empty(); + } + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/Range.java b/web-views/src/main/java/groowt/view/web/util/Range.java new file mode 100644 index 0000000..e0479dc --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/Range.java @@ -0,0 +1,7 @@ +package groowt.view.web.util; + +public interface Range { + boolean isInRange(T item); + T getStart(); + RangeIterator rangeIterator(RangeIterator.NextSupplier nextSupplier); +} diff --git a/web-views/src/main/java/groowt/view/web/util/RangeIterator.java b/web-views/src/main/java/groowt/view/web/util/RangeIterator.java new file mode 100644 index 0000000..ad60a96 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/RangeIterator.java @@ -0,0 +1,19 @@ +package groowt.view.web.util; + +import org.jetbrains.annotations.Nullable; + +import java.util.Iterator; + +public interface RangeIterator extends Iterator { + + interface NextSupplier { + @Nullable T next(int index); + } + + interface StartIndexFunction { + int getStartIndex(T start); + } + + int currentIndex(); + +} diff --git a/web-views/src/main/java/groowt/view/web/util/SemiGroup.java b/web-views/src/main/java/groowt/view/web/util/SemiGroup.java new file mode 100644 index 0000000..d2fda27 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/SemiGroup.java @@ -0,0 +1,13 @@ +package groowt.view.web.util; + +import java.util.function.BinaryOperator; + +public final class SemiGroup { + private final BinaryOperator concat; + public SemiGroup(BinaryOperator concat) { + this.concat = concat; + } + public T concat(T left, T right) { + return this.concat.apply(left, right); + } +} diff --git a/web-views/src/main/java/groowt/view/web/util/SimpleRangeIterator.java b/web-views/src/main/java/groowt/view/web/util/SimpleRangeIterator.java new file mode 100644 index 0000000..9b3537b --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/SimpleRangeIterator.java @@ -0,0 +1,42 @@ +package groowt.view.web.util; + +import org.jetbrains.annotations.Nullable; + +final class SimpleRangeIterator implements RangeIterator { + + private final Range range; + private final NextSupplier nextSupplier; + private @Nullable T next; + private int currentIndex; + + public SimpleRangeIterator(Range range, NextSupplier nextSupplier, int startIndex) { + this.range = range; + this.nextSupplier = nextSupplier; + this.currentIndex = startIndex; + } + + @Override + public int currentIndex() { + return this.currentIndex; + } + + @Override + public boolean hasNext() { + if (this.next == null) { + this.next = this.nextSupplier.next(this.currentIndex); + } + return this.next != null && this.range.isInRange(next); + } + + @Override + public T next() { + if (!this.hasNext()) { + throw new IndexOutOfBoundsException("Cannot next() when hasNext() is false."); + } + this.currentIndex++; + final T result = this.next; + this.next = null; + return result; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/SimpleTokenRange.java b/web-views/src/main/java/groowt/view/web/util/SimpleTokenRange.java new file mode 100644 index 0000000..7e71ff4 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/SimpleTokenRange.java @@ -0,0 +1,21 @@ +package groowt.view.web.util; + +import org.antlr.v4.runtime.Token; + +import java.util.Comparator; + +final class SimpleTokenRange extends ComparatorClosedRange implements TokenRange { + + private static final Comparator TOKEN_COMPARATOR = (left, right) -> { + if (left.equals(right)) { + return 0; + } else { + return left.getTokenIndex() - right.getTokenIndex(); + } + }; + + public SimpleTokenRange(Token start, Token end) { + super(start, end, true, true, TOKEN_COMPARATOR, Token::getTokenIndex); + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/SourcePosition.java b/web-views/src/main/java/groowt/view/web/util/SourcePosition.java new file mode 100644 index 0000000..3312f83 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/SourcePosition.java @@ -0,0 +1,49 @@ +package groowt.view.web.util; + +import org.antlr.v4.runtime.Token; + +public record SourcePosition(int line, int column) { + + public static final SourcePosition UNKNOWN = new SourcePosition(-1, -1); + + public static String formatStartOfToken(Token token) { + return fromStartOfToken(token).toStringShort(); + } + + public static SourcePosition fromStartOfToken(Token token) { + return new SourcePosition(token.getLine(), token.getCharPositionInLine() + 1); + } + + public static SourcePosition fromEndOfToken(Token token) { + final var text = token.getText(); + int line = token.getLine(); + int col = token.getCharPositionInLine() + 1; + int i = 0; + while (i < text.length()) { + final char c0 = text.charAt(i); + if (c0 == '\r') { + line++; + col = 1; + final char c1 = text.charAt(i + 1); + if (c1 == '\n') { + i += 2; + } else { + i++; + } + } else if (c0 == '\n') { + line++; + col = 1; + i++; + } else { + col++; + i++; + } + } + return new SourcePosition(line, col); + } + + public String toStringShort() { + return this.line + "," + this.column; + } + +} diff --git a/web-views/src/main/java/groowt/view/web/util/TokenRange.java b/web-views/src/main/java/groowt/view/web/util/TokenRange.java new file mode 100644 index 0000000..50f84d2 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/util/TokenRange.java @@ -0,0 +1,38 @@ +package groowt.view.web.util; + +import groowt.view.web.antlr.TokenList; +import org.antlr.v4.runtime.Token; + +import java.util.stream.Collectors; + +public interface TokenRange extends ClosedRange { + + static TokenRange fromIndex(TokenList tokenList, int index) { + return of(tokenList.get(index)); + } + + static TokenRange of(Token startAndEnd) { + return new SimpleTokenRange(startAndEnd, startAndEnd); + } + + static TokenRange of(Token start, Token end) { + return new SimpleTokenRange(start, end); + } + + static TokenRange empty() { + return EmptyTokenRange.getInstance(); + } + + default String getText(TokenList tokenList) { + return tokenList.getRange(this).stream().map(Token::getText).collect(Collectors.joining()); + } + + default SourcePosition getStartPosition() { + return SourcePosition.fromStartOfToken(this.getStart()); + } + + default SourcePosition getEndPosition() { + return SourcePosition.fromEndOfToken(this.getEnd()); + } + +} diff --git a/web-views/src/main/resources/log4j2.xml b/web-views/src/main/resources/log4j2.xml new file mode 100644 index 0000000..5081d05 --- /dev/null +++ b/web-views/src/main/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/web-views/src/test/ast/complicated.wvc b/web-views/src/test/ast/complicated.wvc new file mode 100644 index 0000000..0a37f78 --- /dev/null +++ b/web-views/src/test/ast/complicated.wvc @@ -0,0 +1,20 @@ +--- +import some.Thing // a comment + +def greeting = 'Hello, World!' +--- + + + + +

${greeting}

+ + +

It's true! :)

+
+ +

It's false... :(

+
+
+ + diff --git a/web-views/src/test/ast/helloTarget.wvc b/web-views/src/test/ast/helloTarget.wvc new file mode 100644 index 0000000..a9c4d42 --- /dev/null +++ b/web-views/src/test/ast/helloTarget.wvc @@ -0,0 +1 @@ +Hello, $target! diff --git a/web-views/src/test/ast/simpleComponentWithBody.wvc b/web-views/src/test/ast/simpleComponentWithBody.wvc new file mode 100644 index 0000000..940936c --- /dev/null +++ b/web-views/src/test/ast/simpleComponentWithBody.wvc @@ -0,0 +1 @@ +to the world! diff --git a/web-views/src/test/ast/trees/complicated_ast.txt b/web-views/src/test/ast/trees/complicated_ast.txt new file mode 100644 index 0000000..119389b --- /dev/null +++ b/web-views/src/test/ast/trees/complicated_ast.txt @@ -0,0 +1,75 @@ +CompilationUnitNode(1,1..21,6) + PreambleNode(1,1..6,1) + PreambleBreak[1,0](---\n) + GroovyCode[2,0](import some.Thing // a comment...World!') + PreambleBreak[4,30](\n---\n) + BodyNode(6,1..21,1) + JStringBodyTextNode(6,1..7,1) + RawText[6,0](\n) + TypedComponentNode(7,1..20,8) + ComponentArgsNode(7,2..7,6) + StringComponentTypeNode(7,2..7,6) + Identifier[7,1](html) + BodyNode(7,7..20,1) + TypedComponentNode(8,5..8,18) + ComponentArgsNode(8,6..8,10) + StringComponentTypeNode(8,6..8,10) + Identifier[8,5](head) + TypedComponentNode(9,5..19,12) + ComponentArgsNode(9,6..9,10) + StringComponentTypeNode(9,6..9,10) + Identifier[9,5](body) + BodyNode(9,11..19,5) + TypedComponentNode(10,9..10,29) + ComponentArgsNode(10,10..10,12) + StringComponentTypeNode(10,10..10,12) + Identifier[10,9](h1) + BodyNode(10,13..10,24) + GStringBodyTextNode(10,13..10,24) + DollarScriptletNode(10,13..10,24) + DollarScriptletOpen[10,12](${) + GroovyCode[10,14](greeting) + DollarScriptletClose[10,22](}) + TypedComponentNode(11,9..18,34) + ComponentArgsNode(11,10..11,32) + ClassComponentTypeNode(11,10..11,32) + Identifier[11,9](groowt) + Dot[11,15](.) + Identifier[11,16](view) + Dot[11,20](.) + Identifier[11,21](web) + Dot[11,24](.) + Identifier[11,25](Select) + BodyNode(11,33..18,9) + TypedComponentNode(12,13..14,20) + ComponentArgsNode(12,14..12,36) + ClassComponentTypeNode(12,14..12,18) + Identifier[12,13](Case) + KeyValueAttrNode(12,19..12,36) + KeyNode(12,19..12,24) + Identifier[12,18](cond) + Equals[12,22](=) + ClosureValueNode(12,24..12,36) + ClosureAttrValueStart[12,23]({) + GroovyCode[12,24](isItTrue()) + ClosureAttrValueEnd[12,34](}) + BodyNode(12,37..14,13) + TypedComponentNode(13,17..13,37) + ComponentArgsNode(13,18..13,19) + StringComponentTypeNode(13,18..13,19) + Identifier[13,17](p) + BodyNode(13,20..13,33) + JStringBodyTextNode(13,20..13,33) + RawText[13,19](It's true! :)) + TypedComponentNode(15,13..17,23) + ComponentArgsNode(15,14..15,21) + ClassComponentTypeNode(15,14..15,21) + Identifier[15,13](Default) + BodyNode(15,22..17,13) + TypedComponentNode(16,17..16,40) + ComponentArgsNode(16,18..16,19) + StringComponentTypeNode(16,18..16,19) + Identifier[16,17](p) + BodyNode(16,20..16,36) + JStringBodyTextNode(16,20..16,36) + RawText[16,19](It's false... :() diff --git a/web-views/src/test/ast/trees/helloTarget_ast.txt b/web-views/src/test/ast/trees/helloTarget_ast.txt new file mode 100644 index 0000000..7d0a83d --- /dev/null +++ b/web-views/src/test/ast/trees/helloTarget_ast.txt @@ -0,0 +1,10 @@ +CompilationUnitNode(1,1..2,6) + BodyNode(1,1..2,1) + GStringBodyTextNode(1,1..2,1) + JStringBodyTextNode(1,1..1,8) + RawText[1,0](Hello, ) + DollarReferenceNode(1,8..1,15) + DollarReferenceStart[1,7]($) + GroovyCode[1,8](target) + JStringBodyTextNode(1,15..2,1) + RawText[1,14](!\n) diff --git a/web-views/src/test/ast/trees/simpleComponentWithBody_ast.txt b/web-views/src/test/ast/trees/simpleComponentWithBody_ast.txt new file mode 100644 index 0000000..129cdfb --- /dev/null +++ b/web-views/src/test/ast/trees/simpleComponentWithBody_ast.txt @@ -0,0 +1,9 @@ +CompilationUnitNode(1,1..2,6) + BodyNode(1,1..2,1) + TypedComponentNode(1,1..1,35) + ComponentArgsNode(1,2..1,10) + ClassComponentTypeNode(1,2..1,10) + Identifier[1,1](Greeting) + BodyNode(1,11..1,24) + JStringBodyTextNode(1,11..1,24) + RawText[1,10](to the world!) diff --git a/web-views/src/test/groovy/groowt/view/web/DefaultWebComponentTemplateCompilerTests.java b/web-views/src/test/groovy/groowt/view/web/DefaultWebComponentTemplateCompilerTests.java new file mode 100644 index 0000000..f3fa965 --- /dev/null +++ b/web-views/src/test/groovy/groowt/view/web/DefaultWebComponentTemplateCompilerTests.java @@ -0,0 +1,71 @@ +package groowt.view.web; + +import groowt.view.component.*; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.junit.jupiter.api.Test; + +import java.io.Reader; +import java.io.StringReader; +import java.util.Map; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.*; + +public class DefaultWebComponentTemplateCompilerTests { + + private static ComponentTemplate doCompile(Class componentClass, Reader source) { + final var compiler = new DefaultWebComponentTemplateCompiler( + CompilerConfiguration.DEFAULT, + componentClass.getPackageName() + ); + return compiler.compile(componentClass, source); + } + + private static ComponentTemplate doCompile(Class componentClass, String source) { + return doCompile(componentClass, new StringReader(source)); + } + + private static final class Greeter extends DefaultWebViewComponent { + + private final String target; + + public Greeter(Map attr) { + super(doCompile(Greeter.class, "Hello, $target!")); + this.target = (String) Objects.requireNonNull(attr.get("target")); + } + + public String getTarget() { + return this.target; + } + + } + + private static final class GreeterFactory extends AbstractComponentFactory { + + public Greeter doCreate(Map attr) { + return new Greeter(attr); + } + + } + + private static final class UsingGreeter extends DefaultWebViewComponent { + + public UsingGreeter(ComponentContext context) { + super(doCompile(UsingGreeter.class, "")); + this.setContext(context); + } + + } + + @Test + public void usingGreeter() { + final var context = new DefaultComponentContext(); + final var scope = new DefaultComponentScope(); + scope.add("Greeter", new GreeterFactory()); + context.pushScope(scope); + + final UsingGreeter usingGreeter = new UsingGreeter(context); + assertEquals("Hello, World!", usingGreeter.render()); + } + +} diff --git a/web-views/src/test/groovy/groowt/view/web/antlr/WebViewComponentsLexerTests.java b/web-views/src/test/groovy/groowt/view/web/antlr/WebViewComponentsLexerTests.java new file mode 100644 index 0000000..ac79031 --- /dev/null +++ b/web-views/src/test/groovy/groowt/view/web/antlr/WebViewComponentsLexerTests.java @@ -0,0 +1,51 @@ +package groowt.view.web.antlr; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.Token; +import org.junit.jupiter.api.Test; + +import java.util.stream.Collectors; + +import static groowt.view.web.antlr.WebViewComponentsLexer.*; +import static org.junit.jupiter.api.Assertions.*; + +public class WebViewComponentsLexerTests { + + private static void assertTokenType(int type, Token token) { + assertEquals( + type, + token.getType(), + () -> "Expected " + VOCABULARY.getDisplayName(type) + + " but got " + VOCABULARY.getDisplayName(token.getType()) + ); + } + + @Test + public void helloTarget() { + final var input = CharStreams.fromString("Hello, $target!"); + final var lexer = new WebViewComponentsLexer(input); + final var tokenStream = new WebViewComponentsTokenStream(lexer); + final var allTokens = tokenStream.getAllTokens(); + assertEquals(5, allTokens.size(), () -> { + return "Wrong number of tokens; tokens: " + allTokens.stream() + .map(Token::toString) + .collect(Collectors.joining(", ")); + }); + final var t0 = allTokens.get(0); + final var t1 = allTokens.get(1); + final var t2 = allTokens.get(2); + final var t3 = allTokens.get(3); + final var t4 = allTokens.get(4); + assertEquals("Hello, ", t0.getText()); + assertTokenType(RawText, t0); + assertEquals("$", t1.getText()); + assertTokenType(DollarReferenceStart, t1); + assertEquals("target", t2.getText()); + assertTokenType(GroovyCode, t2); + assertInstanceOf(MergedGroovyCodeToken.class, t2); + assertEquals("!", t3.getText()); + assertTokenType(RawText, t3); + assertTokenType(EOF, t4); + } + +} diff --git a/web-views/src/test/groovy/groowt/view/web/antlr/WebViewComponentsParserTests.java b/web-views/src/test/groovy/groowt/view/web/antlr/WebViewComponentsParserTests.java new file mode 100644 index 0000000..2159bd5 --- /dev/null +++ b/web-views/src/test/groovy/groowt/view/web/antlr/WebViewComponentsParserTests.java @@ -0,0 +1,110 @@ +package groowt.view.web.antlr; + +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.function.Executable; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Set; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.*; + +public final class WebViewComponentsParserTests { + + private static final String parserFileBase = String.join(File.separator, "src", "test", "parser"); + private static final String parserTreeFileBase = String.join(File.separator, parserFileBase, "trees"); + private static final String parseTreeFileSuffix = "_parseTree"; + private static final String parseTreeFileExtension = ".txt"; + private static final Set parserFileGlobs = Set.of( + String.join(File.separator, parserFileBase, "*.wvc") + ); + private static final Pattern nameAndExtension = Pattern.compile("(?.*)\\.(?.+)"); + + private WebViewComponentsParser getParser(CharStream input) { + final var lexer = new WebViewComponentsLexer(input); + final var tokenStream = new WebViewComponentsTokenStream(lexer); + return new WebViewComponentsParser(tokenStream); + } + + private WebViewComponentsParser getParser(File file) { + try { + return this.getParser(CharStreams.fromFileName(file.toString())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private WebViewComponentsParser getParser(String source) { + return this.getParser(CharStreams.fromString(source)); + } + + private String getFileName(File parserFile) { + final var m = nameAndExtension.matcher(parserFile.getName()); + if (m.matches()) { + return m.group("name"); + } else { + throw new IllegalArgumentException("Could not determine file name from: " + parserFile.getName()); + } + } + + private Executable getParserFileTest(File parserFile) { + return () -> { + final var parser = this.getParser(parserFile); + final var cu = parser.compilationUnit(); + assertTrue(AntlrUtil.findErrorNodes(cu).isEmpty(), () -> { + final var formatted = ParserUtil.formatTree(parser, cu, true); + return "Parse result had errors:\n" + formatted; + }); + final var parseTreeFileName = this.getFileName(parserFile) + parseTreeFileSuffix + parseTreeFileExtension; + final File parseTreeFile = new File(parserTreeFileBase, parseTreeFileName); + if (!parseTreeFile.exists()) { + throw new IllegalArgumentException( + "There is no parse tree file for " + parseTreeFileName + " at " + parseTreeFile + ); + } + try (final FileInputStream fis = new FileInputStream(parseTreeFile)) { + final var expectedFormatted = new String(fis.readAllBytes()); + final var actualFormatted = ParserUtil.formatTree(parser, cu, false); + assertEquals(expectedFormatted, actualFormatted); + } catch (IOException ioException) { + throw new RuntimeException(ioException); + } + }; + } + + @TestFactory + public Collection getParserFileTests() { + final Collection tests = new ArrayList<>(); + final FileSystem fs = FileSystems.getDefault(); + for (final var glob : parserFileGlobs) { + final var matcher = fs.getPathMatcher("glob:" + glob); + try (final var paths = Files.walk(Path.of(parserFileBase))) { + paths.filter(matcher::matches).forEach(path -> { + final File asFile = path.toFile(); + if (!asFile.isDirectory()) { + final DynamicTest test = DynamicTest.dynamicTest( + this.getFileName(asFile), + this.getParserFileTest(asFile) + ); + tests.add(test); + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return tests; + } + +} diff --git a/web-views/src/test/groovy/groowt/view/web/antlr/WebViewComponentsTokenStreamTests.groovy b/web-views/src/test/groovy/groowt/view/web/antlr/WebViewComponentsTokenStreamTests.groovy new file mode 100644 index 0000000..8554e3d --- /dev/null +++ b/web-views/src/test/groovy/groowt/view/web/antlr/WebViewComponentsTokenStreamTests.groovy @@ -0,0 +1,60 @@ +package groowt.view.web.antlr + +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType +import org.antlr.v4.runtime.CharStream +import org.antlr.v4.runtime.CharStreams +import org.antlr.v4.runtime.Token +import org.junit.jupiter.api.Test + +import static groowt.view.web.antlr.TokenUtil.getTokenName +import static groowt.view.web.antlr.WebViewComponentsLexer.GroovyCode +import static groowt.view.web.antlr.WebViewComponentsLexer.PreambleBreak +import static org.antlr.v4.runtime.Recognizer.EOF +import static org.junit.jupiter.api.Assertions.* + +class WebViewComponentsTokenStreamTests { + + private static CharStream fromResource(String name) { + CharStreams.fromStream(WebViewComponentsTokenStreamTests.getResourceAsStream(name)) + } + + private static void assertType(int expectedType, Token actual) { + assertEquals(expectedType, actual.type) { + "expected ${getTokenName(expectedType)} but got ${getTokenName(actual)}; " + + "actual.tokenIndex: ${actual.tokenIndex}" + } + } + + private static void assertTypes(List expected, List actual) { + if (expected.size() != actual.size()) { + fail("expected.size() and actual.size() differ: ${expected.size()}, ${actual.size()}") + } + expected.eachWithIndex { expectedType, index -> + assertType(expectedType, actual[index]) + } + } + + private static void assertMergedGroovyCodeToken( + Token token, + @ClosureParams(value = SimpleType, options = ['MergedGroovyCodeToken']) + Closure onSuccess = {} + ) { + assertInstanceOf(MergedGroovyCodeToken, token) + onSuccess(token) + } + + @Test + void mergesGroovyTokens() { + def input = fromResource('mergesGroovyTokens.gst') + def lexer = new WebViewComponentsLexer(input) + def tokenStream = new WebViewComponentsTokenStream(lexer) + def tokens = tokenStream.allTokens + assertTypes([PreambleBreak, GroovyCode, PreambleBreak, EOF], tokens) + assertMergedGroovyCodeToken(tokens[1]) { + assertEquals('println \'Hello, World!\' // comment\n', it.text) + } + assertIterableEquals(0..3, tokens*.tokenIndex) + } + +} diff --git a/web-views/src/test/groovy/groowt/view/web/ast/DefaultAstBuilderTests.java b/web-views/src/test/groovy/groowt/view/web/ast/DefaultAstBuilderTests.java new file mode 100644 index 0000000..9ffe9b1 --- /dev/null +++ b/web-views/src/test/groovy/groowt/view/web/ast/DefaultAstBuilderTests.java @@ -0,0 +1,34 @@ +package groowt.view.web.ast; + +import groowt.view.web.antlr.ParserUtil; +import groowt.view.web.antlr.TokenList; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.file.Path; + +public class DefaultAstBuilderTests extends AstBuilderTests { + + public DefaultAstBuilderTests() { + super( + Path.of(String.join(File.separator, "src", "test", "ast")), + "*.wvc", + new File(String.join(File.separator, "src", "test", "ast", "trees")), + "_ast.txt" + ); + } + + @Override + protected BuildResult buildFromSource(String source) { + final var parseResult = ParserUtil.parseCompilationUnit(source); + final var tokenList = new TokenList(parseResult.getTokenStream()); + final var b = new DefaultAstBuilder(new DefaultNodeFactory(tokenList)); + return new BuildResult(b.build(parseResult.getCompilationUnitContext()), tokenList); + } + + @Override + protected String format(BuildResult buildResult) { + return NodeUtil.formatAst(buildResult.node(), buildResult.tokenList()); + } + +} diff --git a/web-views/src/test/groovy/groowt/view/web/ast/DefaultAstBuilderVisitorTests.groovy b/web-views/src/test/groovy/groowt/view/web/ast/DefaultAstBuilderVisitorTests.groovy new file mode 100644 index 0000000..232ac8b --- /dev/null +++ b/web-views/src/test/groovy/groowt/view/web/ast/DefaultAstBuilderVisitorTests.groovy @@ -0,0 +1,79 @@ +package groowt.view.web.ast + +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.FirstParam +import groowt.view.web.antlr.TokenList +import groowt.view.web.antlr.WebViewComponentsLexer +import groowt.view.web.antlr.WebViewComponentsParser +import groowt.view.web.antlr.WebViewComponentsTokenStream +import groowt.view.web.ast.node.* +import org.antlr.v4.runtime.CharStreams +import org.junit.jupiter.api.Test + +import static groowt.view.web.antlr.WebViewComponentsParser.CompilationUnitContext +import static org.junit.jupiter.api.Assertions.* + +class DefaultAstBuilderVisitorTests { + + private static void assertNodeWith( + @DelegatesTo.Target Class expectedType, + Node actual, + @ClosureParams(FirstParam.FirstGenericType) + @DelegatesTo(strategy = Closure.DELEGATE_FIRST, genericTypeIndex = 0) + Closure with + ) { + Objects.requireNonNull(expectedType) + assertNotNull(actual) + assertInstanceOf(expectedType, actual) + with.delegate = actual + with.resolveStrategy = Closure.DELEGATE_FIRST + with.call(actual) + } + + private static void assertNode(Class expectedType, Node actual) { + assertNodeWith(expectedType, actual) { } + } + + private Tuple2 parse(String source) { + def input = CharStreams.fromString(source) + def lexer = new WebViewComponentsLexer(input) + def tokenStream = new WebViewComponentsTokenStream(lexer) + def parser = new WebViewComponentsParser(tokenStream) + def cu = parser.compilationUnit() + def tokenList = new TokenList(tokenStream) + return new Tuple2<>(cu, tokenList) + } + + private Tuple2 doBuild(String source) { + def (cu, tokenList) = this.parse(source) + def nodeFactory = new DefaultNodeFactory(tokenList) + def visitor = new DefaultAstBuilderVisitor(nodeFactory) + return new Tuple2<>(cu.accept(visitor), tokenList) + } + + @Test + void helloTarget() { + def (node, tokenList) = this.doBuild('Hello, $target!') + assertNodeWith(CompilationUnitNode, node) { + assertNull(preambleNode) + bodyNode.with { + assertEquals(1, childrenSize) + assertNodeWith(GStringBodyTextNode, children.first) { + assertEquals(3, childrenSize) + assertNodeWith(JStringBodyTextNode, it[0]) { + assertEquals('Hello, ', it.getText(tokenList)) + assertEquals('Hello, ', it.content) + } + assertNodeWith(DollarReferenceNode, it[1]) { + assertEquals('$target', it.getText(tokenList)) + assertEquals('$target', it.GStringPath.asValidEmbeddableCode) + } + assertNodeWith(JStringBodyTextNode, it[2]) { + assertEquals('!', it.content) + } + } + } + } + } + +} diff --git a/web-views/src/test/groovy/groowt/view/web/ast/DefaultNodeFactoryTests.java b/web-views/src/test/groovy/groowt/view/web/ast/DefaultNodeFactoryTests.java new file mode 100644 index 0000000..b48d3b8 --- /dev/null +++ b/web-views/src/test/groovy/groowt/view/web/ast/DefaultNodeFactoryTests.java @@ -0,0 +1,20 @@ +package groowt.view.web.ast; + +import groowt.view.web.antlr.TokenList; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +@ExtendWith(MockitoExtension.class) +public class DefaultNodeFactoryTests extends NodeFactoryTests { + + public DefaultNodeFactoryTests() { + super(nft -> ((DefaultNodeFactoryTests) nft).getNodeFactory()); + } + + protected NodeFactory getNodeFactory() { + return new DefaultNodeFactory(new TokenList(List.of(this.getStartAndEndToken()))); + } + +} 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 new file mode 100644 index 0000000..f1586d3 --- /dev/null +++ b/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultBodyTranspilerTests.java @@ -0,0 +1,25 @@ +package groowt.view.web.transpiler; + +import groowt.view.web.transpile.*; + +public class DefaultBodyTranspilerTests extends BodyTranspilerTests { + + @Override + protected BodyTranspiler getBodyTranspiler() { + 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); + componentTranspiler.setValueNodeTranspiler(valueNodeTranspiler); + final var bodyTranspiler = new DefaultBodyTranspiler(gStringTranspiler, jStringTranspiler, componentTranspiler); + componentTranspiler.setBodyTranspiler(bodyTranspiler); + return bodyTranspiler; + } + + @Override + protected OutStatementFactory getOutStatementFactory() { + return new SimpleOutStatementFactory(); + } + +} diff --git a/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultGStringTranspilerTests.java b/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultGStringTranspilerTests.java new file mode 100644 index 0000000..91cda1a --- /dev/null +++ b/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultGStringTranspilerTests.java @@ -0,0 +1,16 @@ +package groowt.view.web.transpiler; + +import groowt.view.web.transpile.DefaultGStringTranspiler; +import groowt.view.web.transpile.DefaultJStringTranspiler; +import groowt.view.web.transpile.GStringTranspiler; +import groowt.view.web.transpile.SimplePositionSetter; + +public class DefaultGStringTranspilerTests extends GStringTranspilerTests { + + @Override + protected GStringTranspiler getGStringTranspiler() { + final var positionSetter = new SimplePositionSetter(); + return new DefaultGStringTranspiler(positionSetter, new DefaultJStringTranspiler(positionSetter)); + } + +} 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 new file mode 100644 index 0000000..16c903c --- /dev/null +++ b/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultGroovyTranspilerTests.java @@ -0,0 +1,24 @@ +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 + ); + } + + public DefaultGroovyTranspilerTests() { + super(getDefaultGroovyTranspiler()); + } + +} diff --git a/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultPreambleTranspilerTests.java b/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultPreambleTranspilerTests.java new file mode 100644 index 0000000..1dae285 --- /dev/null +++ b/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultPreambleTranspilerTests.java @@ -0,0 +1,15 @@ +package groowt.view.web.transpiler; + +import groowt.view.web.transpile.DefaultPreambleTranspiler; + +public class DefaultPreambleTranspilerTests extends PreambleTranspilerTests { + + protected static DefaultPreambleTranspiler getDefaultPreambleTranspiler() { + return new DefaultPreambleTranspiler(); + } + + public DefaultPreambleTranspilerTests() { + super(getDefaultPreambleTranspiler()); + } + +} diff --git a/web-views/src/test/parser/blankPreambleOnly.wvc b/web-views/src/test/parser/blankPreambleOnly.wvc new file mode 100644 index 0000000..a845151 --- /dev/null +++ b/web-views/src/test/parser/blankPreambleOnly.wvc @@ -0,0 +1,2 @@ +--- +--- diff --git a/web-views/src/test/parser/blankPreambleWithExtraLines.wvc b/web-views/src/test/parser/blankPreambleWithExtraLines.wvc new file mode 100644 index 0000000..b90d6d8 --- /dev/null +++ b/web-views/src/test/parser/blankPreambleWithExtraLines.wvc @@ -0,0 +1,7 @@ +--- + + + + + +--- diff --git a/web-views/src/test/parser/complicated.wvc b/web-views/src/test/parser/complicated.wvc new file mode 100644 index 0000000..0a37f78 --- /dev/null +++ b/web-views/src/test/parser/complicated.wvc @@ -0,0 +1,20 @@ +--- +import some.Thing // a comment + +def greeting = 'Hello, World!' +--- + + + + +

${greeting}

+ + +

It's true! :)

+
+ +

It's false... :(

+
+
+ + diff --git a/web-views/src/test/parser/helloTarget.wvc b/web-views/src/test/parser/helloTarget.wvc new file mode 100644 index 0000000..177feed --- /dev/null +++ b/web-views/src/test/parser/helloTarget.wvc @@ -0,0 +1 @@ +Hello, $target! \ No newline at end of file diff --git a/web-views/src/test/parser/preambleWithClass.wvc b/web-views/src/test/parser/preambleWithClass.wvc new file mode 100644 index 0000000..f6e0555 --- /dev/null +++ b/web-views/src/test/parser/preambleWithClass.wvc @@ -0,0 +1,5 @@ +--- +class Greeting { + String myGreeting +} +--- diff --git a/web-views/src/test/parser/trees/blankPreambleOnly_parseTree.txt b/web-views/src/test/parser/trees/blankPreambleOnly_parseTree.txt new file mode 100644 index 0000000..ea16ee2 --- /dev/null +++ b/web-views/src/test/parser/trees/blankPreambleOnly_parseTree.txt @@ -0,0 +1,5 @@ +compilationUnit[1,1..3,1] + preamble[1,1..2,1] + PreambleBreak[1,1](---\n) + PreambleBreak[2,1](---\n) + EOF[3,1]() diff --git a/web-views/src/test/parser/trees/blankPreambleWithExtraLines_parseTree.txt b/web-views/src/test/parser/trees/blankPreambleWithExtraLines_parseTree.txt new file mode 100644 index 0000000..63b68c3 --- /dev/null +++ b/web-views/src/test/parser/trees/blankPreambleWithExtraLines_parseTree.txt @@ -0,0 +1,6 @@ +compilationUnit[1,1..8,1] + preamble[1,1..6,1] + PreambleBreak[1,1](---\n) + GroovyCode[2,1](\n\n\n\n) + PreambleBreak[6,1](\n---\n) + EOF[8,1]() diff --git a/web-views/src/test/parser/trees/complicated_parseTree.txt b/web-views/src/test/parser/trees/complicated_parseTree.txt new file mode 100644 index 0000000..51ca430 --- /dev/null +++ b/web-views/src/test/parser/trees/complicated_parseTree.txt @@ -0,0 +1,212 @@ +compilationUnit[1,1..21,1] + preamble[1,1..4,31] + PreambleBreak[1,1](---\n) + GroovyCode[2,1](import some.Thing // a comment\n\ndef greeting = 'Hello, World!') + PreambleBreak[4,31](\n---\n) + body[6,1..20,8] + bodyText[6,1..6,1] + jStringBodyText[6,1..6,1] + RawText[6,1](\n) + component[7,1..20,7] + componentWithChildren[7,1..20,7] + openComponent[7,1..7,6] + ComponentOpen[7,1](<) + componentArgs[7,2..7,2] + componentType[7,2..7,2] + Identifier[7,2](html) + ComponentClose[7,6](>) + body[7,7..19,12] + bodyText[7,7..7,7] + jStringBodyText[7,7..7,7] + RawText[7,7](\n ) + component[8,5..8,17] + componentWithChildren[8,5..8,17] + openComponent[8,5..8,10] + ComponentOpen[8,5](<) + componentArgs[8,6..8,6] + componentType[8,6..8,6] + Identifier[8,6](head) + ComponentClose[8,10](>) + closingComponent[8,11..8,17] + ClosingComponentOpen[8,11]() + bodyText[8,18..8,18] + jStringBodyText[8,18..8,18] + RawText[8,18](\n ) + component[9,5..19,11] + componentWithChildren[9,5..19,11] + openComponent[9,5..9,10] + ComponentOpen[9,5](<) + componentArgs[9,6..9,6] + componentType[9,6..9,6] + Identifier[9,6](body) + ComponentClose[9,10](>) + body[9,11..18,34] + bodyText[9,11..9,11] + jStringBodyText[9,11..9,11] + RawText[9,11](\n ) + component[10,9..10,28] + componentWithChildren[10,9..10,28] + openComponent[10,9..10,12] + ComponentOpen[10,9](<) + componentArgs[10,10..10,10] + componentType[10,10..10,10] + Identifier[10,10](h1) + ComponentClose[10,12](>) + body[10,13..10,23] + bodyText[10,13..10,23] + gStringBodyText[10,13..10,23] + gStringBodyTextGroovyElement[10,13..10,23] + dollarScriptlet[10,13..10,23] + DollarScriptletOpen[10,13](${) + GroovyCode[10,15](greeting) + DollarScriptletClose[10,23](}) + closingComponent[10,24..10,28] + ClosingComponentOpen[10,24]() + bodyText[10,29..10,29] + jStringBodyText[10,29..10,29] + RawText[10,29](\n ) + component[11,9..18,33] + componentWithChildren[11,9..18,33] + openComponent[11,9..11,32] + ComponentOpen[11,9](<) + componentArgs[11,10..11,26] + componentType[11,10..11,26] + Identifier[11,10](groowt) + Dot[11,16](.) + Identifier[11,17](view) + Dot[11,21](.) + Identifier[11,22](web) + Dot[11,25](.) + Identifier[11,26](Select) + ComponentClose[11,32](>) + body[11,33..17,23] + bodyText[11,33..11,33] + jStringBodyText[11,33..11,33] + RawText[11,33](\n ) + component[12,13..14,19] + componentWithChildren[12,13..14,19] + openComponent[12,13..12,36] + ComponentOpen[12,13](<) + componentArgs[12,14..12,35] + componentType[12,14..12,14] + Identifier[12,14](Case) + ComponentNlws[12,18]( ) + attr[12,19..12,35] + keyValueAttr[12,19..12,35] + Identifier[12,19](cond) + Equals[12,23](=) + value[12,24..12,35] + closureAttrValue[12,24..12,35] + ClosureAttrValueStart[12,24]({) + GroovyCode[12,25](isItTrue()) + ClosureAttrValueEnd[12,35](}) + ComponentClose[12,36](>) + body[12,37..13,37] + bodyText[12,37..12,37] + jStringBodyText[12,37..12,37] + RawText[12,37](\n ) + component[13,17..13,36] + componentWithChildren[13,17..13,36] + openComponent[13,17..13,19] + ComponentOpen[13,17](<) + componentArgs[13,18..13,18] + componentType[13,18..13,18] + Identifier[13,18](p) + ComponentClose[13,19](>) + body[13,20..13,20] + bodyText[13,20..13,20] + jStringBodyText[13,20..13,20] + RawText[13,20](It's true! :)) + closingComponent[13,33..13,36] + ClosingComponentOpen[13,33]() + bodyText[13,37..13,37] + jStringBodyText[13,37..13,37] + RawText[13,37](\n ) + closingComponent[14,13..14,19] + ClosingComponentOpen[14,13]() + bodyText[14,20..14,20] + jStringBodyText[14,20..14,20] + RawText[14,20](\n ) + component[15,13..17,22] + componentWithChildren[15,13..17,22] + openComponent[15,13..15,21] + ComponentOpen[15,13](<) + componentArgs[15,14..15,14] + componentType[15,14..15,14] + Identifier[15,14](Default) + ComponentClose[15,21](>) + body[15,22..16,40] + bodyText[15,22..15,22] + jStringBodyText[15,22..15,22] + RawText[15,22](\n ) + component[16,17..16,39] + componentWithChildren[16,17..16,39] + openComponent[16,17..16,19] + ComponentOpen[16,17](<) + componentArgs[16,18..16,18] + componentType[16,18..16,18] + Identifier[16,18](p) + ComponentClose[16,19](>) + body[16,20..16,20] + bodyText[16,20..16,20] + jStringBodyText[16,20..16,20] + RawText[16,20](It's false... :() + closingComponent[16,36..16,39] + ClosingComponentOpen[16,36]() + bodyText[16,40..16,40] + jStringBodyText[16,40..16,40] + RawText[16,40](\n ) + closingComponent[17,13..17,22] + ClosingComponentOpen[17,13]() + bodyText[17,23..17,23] + jStringBodyText[17,23..17,23] + RawText[17,23](\n ) + closingComponent[18,9..18,33] + ClosingComponentOpen[18,9]() + bodyText[18,34..18,34] + jStringBodyText[18,34..18,34] + RawText[18,34](\n ) + closingComponent[19,5..19,11] + ClosingComponentOpen[19,5]() + bodyText[19,12..19,12] + jStringBodyText[19,12..19,12] + RawText[19,12](\n) + closingComponent[20,1..20,7] + ClosingComponentOpen[20,1]() + bodyText[20,8..20,8] + jStringBodyText[20,8..20,8] + RawText[20,8](\n) + EOF[21,1]() diff --git a/web-views/src/test/parser/trees/helloTarget_parseTree.txt b/web-views/src/test/parser/trees/helloTarget_parseTree.txt new file mode 100644 index 0000000..54b15e4 --- /dev/null +++ b/web-views/src/test/parser/trees/helloTarget_parseTree.txt @@ -0,0 +1,13 @@ +compilationUnit[1,1..1,16] + body[1,1..1,15] + bodyText[1,1..1,15] + gStringBodyText[1,1..1,15] + jStringBodyText[1,1..1,1] + RawText[1,1](Hello, ) + gStringBodyTextGroovyElement[1,8..1,9] + dollarReference[1,8..1,9] + DollarReferenceStart[1,8]($) + GroovyCode[1,9](target) + jStringBodyText[1,15..1,15] + RawText[1,15](!) + EOF[1,16]() diff --git a/web-views/src/test/parser/trees/preambleWithClass_parseTree.txt b/web-views/src/test/parser/trees/preambleWithClass_parseTree.txt new file mode 100644 index 0000000..278847d --- /dev/null +++ b/web-views/src/test/parser/trees/preambleWithClass_parseTree.txt @@ -0,0 +1,6 @@ +compilationUnit[1,1..6,1] + preamble[1,1..4,2] + PreambleBreak[1,1](---\n) + GroovyCode[2,1](class Greeting {\n String myGreeting\n}) + PreambleBreak[4,2](\n---\n) + EOF[6,1]() diff --git a/web-views/src/test/resources/groowt/view/web/antlr/mergesGroovyTokens.gst b/web-views/src/test/resources/groowt/view/web/antlr/mergesGroovyTokens.gst new file mode 100644 index 0000000..b2c5e66 --- /dev/null +++ b/web-views/src/test/resources/groowt/view/web/antlr/mergesGroovyTokens.gst @@ -0,0 +1,3 @@ +--- +println 'Hello, World!' // comment +--- diff --git a/web-views/src/testFixtures/java/groowt/view/web/ast/AstBuilderTests.java b/web-views/src/testFixtures/java/groowt/view/web/ast/AstBuilderTests.java new file mode 100644 index 0000000..7cea76d --- /dev/null +++ b/web-views/src/testFixtures/java/groowt/view/web/ast/AstBuilderTests.java @@ -0,0 +1,91 @@ +package groowt.view.web.ast; + +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.node.Node; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.Collection; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +public abstract class AstBuilderTests { + + protected record BuildResult(Node node, TokenList tokenList) {} + + private static final Pattern withoutExtension = Pattern.compile("(?.*)\\..+"); + + protected static String getNameWithoutExtension(File file) { + final var m = withoutExtension.matcher(file.getName()); + if (m.matches()) { + return m.group("name"); + } else { + throw new IllegalArgumentException("Cannot get name without extension for " + file); + } + } + + protected static String readFile(File file) { + try (final var fis = new FileInputStream(file)) { + return new String(fis.readAllBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private final Path sourceFileDir; + private final String sourceFileGlob; + private final File astTreeDir; + private final String astFileSuffixAndExt; + + public AstBuilderTests(Path sourceFileDir, String sourceFileGlob, File astTreeDir, String astFileSuffixAndExt) { + this.sourceFileDir = sourceFileDir; + this.sourceFileGlob = sourceFileGlob; + this.astTreeDir = astTreeDir; + this.astFileSuffixAndExt = astFileSuffixAndExt; + } + + protected abstract BuildResult buildFromSource(String source); + + protected abstract String format(BuildResult buildResult); + + protected void doSourceFileTest(String source, String expected) { + final BuildResult buildResult = this.buildFromSource(source); + final var actual = this.format(buildResult); + assertEquals(expected, actual); + } + + @TestFactory + public Collection getSourceFileTests() { + final var fs = FileSystems.getDefault(); + final PathMatcher matcher = fs.getPathMatcher( + "glob:" + this.sourceFileDir.toString() + File.separator + this.sourceFileGlob + ); + try (final Stream paths = Files.walk(this.sourceFileDir)) { + return paths.filter(matcher::matches) + .map(Path::toFile) + .filter(File::isFile) + .map(file -> { + final var name = getNameWithoutExtension(file); + final var expectedAstFile = new File(this.astTreeDir, name + this.astFileSuffixAndExt); + final var source = readFile(file); + final var expected = readFile(expectedAstFile); + return DynamicTest.dynamicTest( + name, + () -> this.doSourceFileTest(source, expected) + ); + }).toList(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/web-views/src/testFixtures/java/groowt/view/web/ast/NodeFactoryTests.java b/web-views/src/testFixtures/java/groowt/view/web/ast/NodeFactoryTests.java new file mode 100644 index 0000000..219f16e --- /dev/null +++ b/web-views/src/testFixtures/java/groowt/view/web/ast/NodeFactoryTests.java @@ -0,0 +1,223 @@ +package groowt.view.web.ast; + +import groowt.view.web.ast.extension.GStringNodeExtension; +import groowt.view.web.ast.node.*; +import groowt.view.web.util.TokenRange; +import org.antlr.v4.runtime.Token; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public abstract class NodeFactoryTests { + + private final Function getNodeFactory; + private NodeFactory nodeFactory; + private Token startAndEndToken; + + public NodeFactoryTests(Function getNodeFactory) { + this.getNodeFactory = getNodeFactory; + } + + protected Token getStartAndEndToken() { + if (this.startAndEndToken == null) { + this.startAndEndToken = mock(Token.class); + } + return this.startAndEndToken; + } + + protected TokenRange getTokenRange() { + return TokenRange.of(this.getStartAndEndToken()); + } + + @BeforeEach + public void beforeEach() { + this.nodeFactory = this.getNodeFactory.apply(this); + } + + @Test + public void compilationUnitNode(@Mock PreambleNode preambleNode, @Mock BodyNode bodyNode) { + assertNotNull(this.nodeFactory.compilationUnitNode(this.getTokenRange(), preambleNode, bodyNode)); + } + + @Test + public void compilationUnitNodePreambleOnly(@Mock PreambleNode preambleNode) { + assertNotNull(this.nodeFactory.compilationUnitNode(this.getTokenRange(), preambleNode, null)); + } + + @Test + public void compilationUnitNodeBodyOnly(@Mock BodyNode bodyNode) { + assertNotNull(this.nodeFactory.compilationUnitNode(this.getTokenRange(), null, bodyNode)); + } + + @Test + public void compilationUnitNodeNullNull() { + assertNotNull(this.nodeFactory.compilationUnitNode(this.getTokenRange(), null, null)); + } + + @Test + public void preambleNode() { + assertNotNull(this.nodeFactory.preambleNode(this.getTokenRange(), 0)); + } + + @Test + public void bodyNode(@Mock BodyChildNode child, @Mock TreeNode childAsNode) { + when(child.asNode()).thenReturn(childAsNode); + assertNotNull(this.nodeFactory.bodyNode(this.getTokenRange(), List.of(child))); + } + + @Test + public void gStringBodyTextNode(@Mock LeafNode child) { + when(child.hasExtension(GStringNodeExtension.class)).thenReturn(true); + assertNotNull(this.nodeFactory.gStringBodyTextNode(this.getTokenRange(), List.of(child))); + } + + @Test + public void jStringBodyTextNode() { + assertNotNull(this.nodeFactory.jStringBodyTextNode(this.getTokenRange(), "Hello!")); + } + + @Test + public void typedComponentNode(@Mock ComponentArgsNode componentArgsNode, @Mock BodyNode bodyNode) { + assertNotNull(this.nodeFactory.typedComponentNode(this.getTokenRange(), componentArgsNode, bodyNode)); + } + + @Test + public void typedComponentNodeBodyNull(@Mock ComponentArgsNode componentArgsNode) { + assertNotNull(this.nodeFactory.typedComponentNode(this.getTokenRange(), componentArgsNode, null)); + } + + @Test + public void fragmentComponentNode(@Mock BodyNode bodyNode) { + assertNotNull(this.nodeFactory.fragmentComponentNode(this.getTokenRange(), bodyNode)); + } + + @Test + public void fragmentComponentNodeBodyNull() { + assertNotNull(this.nodeFactory.fragmentComponentNode(this.getTokenRange(), null)); + } + + @Test + public void componentArgsNodeWithClassComponentType( + @Mock ClassComponentTypeNode componentTypeNode, + @Mock ComponentConstructorNode componentConstructorNode + ) { + assertNotNull(this.nodeFactory.componentArgsNode( + this.getTokenRange(), + componentTypeNode, + componentConstructorNode, + List.of() + )); + } + + @Test + public void componentArgsNodeWithStringComponentType( + @Mock StringComponentTypeNode componentTypeNode, + @Mock ComponentConstructorNode componentConstructorNode + ) { + assertNotNull(this.nodeFactory.componentArgsNode( + this.getTokenRange(), + componentTypeNode, + componentConstructorNode, + List.of() + )); + } + + @Test + public void componentArgsNodeNullConstructorNodeWithClassComponentType( + @Mock ClassComponentTypeNode componentTypeNode + ) { + assertNotNull(this.nodeFactory.componentArgsNode( + this.getTokenRange(), componentTypeNode, null, List.of() + )); + } + + @Test + public void componentArgsNodeNullConstructorNodeWithStringComponentType( + @Mock StringComponentTypeNode componentTypeNode + ) { + assertNotNull(this.nodeFactory.componentArgsNode( + this.getTokenRange(), componentTypeNode, null, List.of() + )); + } + + @Test + public void classComponentTypeNode() { + assertNotNull(this.nodeFactory.classComponentTypeNode(this.getTokenRange())); + } + + @Test + public void stringComponentTypeNode() { + assertNotNull(this.nodeFactory.stringComponentTypeNode(this.getTokenRange(), 0)); + } + + @Test + public void componentConstructorNode() { + assertNotNull(this.nodeFactory.componentConstructorNode(this.getTokenRange(), 0)); + } + + @Test + public void keyValueAttrNode(@Mock KeyNode keyNode, @Mock ValueNode valueNode, @Mock TreeNode valueNodeAsNode) { + when(valueNode.asNode()).thenReturn(valueNodeAsNode); + assertNotNull(this.nodeFactory.keyValueAttrNode(this.getTokenRange(), keyNode, valueNode)); + } + + @Test + public void booleanValueAttrNode(@Mock KeyNode keyNode) { + assertNotNull(this.nodeFactory.booleanValueAttrNode(this.getTokenRange(), keyNode)); + } + + @Test + public void keyNode() { + assertNotNull(this.nodeFactory.keyNode(this.getTokenRange(), 0)); + } + + @Test + public void gStringValueNode() { + assertNotNull(this.nodeFactory.gStringValueNode(this.getTokenRange(), 0)); + } + + @Test + public void jStringValueNode() { + assertNotNull(this.nodeFactory.jStringValueNode(this.getTokenRange(), "")); + } + + @Test + public void closureValueNode() { + assertNotNull(this.nodeFactory.closureValueNode(this.getTokenRange(), 0)); + } + + @Test + public void componentValueNodeWithTypedComponentNode(@Mock TypedComponentNode node) { + assertNotNull(this.nodeFactory.componentValueNode(this.getTokenRange(), node)); + } + + @Test + public void componentValueNodeWithFragmentComponentNode(@Mock FragmentComponentNode node) { + assertNotNull(this.nodeFactory.componentValueNode(this.getTokenRange(), node)); + } + + @Test + public void plainScriptletNode() { + assertNotNull(this.nodeFactory.plainScriptletNode(this.getTokenRange(), 0)); + } + + @Test + public void dollarScriptletNode() { + assertNotNull(this.nodeFactory.dollarScriptletNode(this.getTokenRange(), 0)); + } + + @Test + public void dollarReferenceNode() { + assertNotNull(this.nodeFactory.dollarReferenceNode(this.getTokenRange(), 0)); + } + +} 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 new file mode 100644 index 0000000..1abcf72 --- /dev/null +++ b/web-views/src/testFixtures/java/groowt/view/web/transpiler/BodyTranspilerTests.java @@ -0,0 +1,80 @@ +package groowt.view.web.transpiler; + +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.BodyNode; +import groowt.view.web.ast.node.CompilationUnitNode; +import groowt.view.web.transpile.BodyTranspiler; +import groowt.view.web.transpile.OutStatementFactory; +import groowt.view.web.transpile.TranspilerUtil; +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 static org.junit.jupiter.api.Assertions.*; + +public abstract class BodyTranspilerTests { + + protected abstract BodyTranspiler getBodyTranspiler(); + protected abstract OutStatementFactory getOutStatementFactory(); + + protected record BuildResult(BodyNode bodyNode, TokenList tokenList) {} + + protected BuildResult build(String source) { + final var parseResult = ParserUtil.parseCompilationUnit(source); + final var tokenList = new TokenList(parseResult.getTokenStream()); + final var b = new DefaultAstBuilder(new DefaultNodeFactory(tokenList)); + final var cuNode = (CompilationUnitNode) b.build(parseResult.getCompilationUnitContext()); + final var bodyNode = cuNode.getBodyNode(); + if (bodyNode == null) { + fail("No BodyNode was built for source: " + source); + } + return new BuildResult(bodyNode, tokenList); + } + + @Test + public void smokeScreen() { + assertDoesNotThrow(() -> { + getBodyTranspiler(); + }); + } + + @Test + public void simpleGStringOutStatement() { + final var source = "Hello, $target!"; + final var buildResult = this.build(source); + final var transpiler = this.getBodyTranspiler(); + final var outStatementFactory = this.getOutStatementFactory(); + final BlockStatement blockStatement = transpiler.transpileBody( + buildResult.bodyNode(), + (ignored, expression) -> outStatementFactory.create(expression), + TranspilerUtil.TranspilerState.withDefaultRootScope() + ); + assertEquals(1, blockStatement.getStatements().size()); + } + + @Test + public void simpleJStringOutStatement() { + final var source = "Hello, World!"; + final var buildResult = this.build(source); + final var transpiler = this.getBodyTranspiler(); + final var outStatementFactory = this.getOutStatementFactory(); + final BlockStatement blockStatement = transpiler.transpileBody( + buildResult.bodyNode(), + (ignored, expression) -> outStatementFactory.create(expression), + TranspilerUtil.TranspilerState.withDefaultRootScope() + ); + assertEquals(1, blockStatement.getStatements().size()); + final var s0 = (ExpressionStatement) blockStatement.getStatements().getFirst(); + final var binaryExpression = (MethodCallExpression) s0.getExpression(); + final var args = (TupleExpression) binaryExpression.getArguments(); + final var first = (ConstantExpression) args.getExpression(0); + assertEquals("Hello, World!", first.getValue()); + } + +} diff --git a/web-views/src/testFixtures/java/groowt/view/web/transpiler/GStringTranspilerTests.java b/web-views/src/testFixtures/java/groowt/view/web/transpiler/GStringTranspilerTests.java new file mode 100644 index 0000000..ac3d870 --- /dev/null +++ b/web-views/src/testFixtures/java/groowt/view/web/transpiler/GStringTranspilerTests.java @@ -0,0 +1,42 @@ +package groowt.view.web.transpiler; + +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.ast.node.GStringBodyTextNode; +import groowt.view.web.transpile.GStringTranspiler; +import org.codehaus.groovy.ast.expr.GStringExpression; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public abstract class GStringTranspilerTests { + + protected abstract GStringTranspiler getGStringTranspiler(); + + @Test + public void smokeScreen() { + assertDoesNotThrow(() -> { + getGStringTranspiler(); + }); + } + + @Test + public void gStringExpressionWithDollarReference() { + final var source = "Hello, $target!"; + final var parseResult = ParserUtil.parseCompilationUnit(source); + final var tokenList = new TokenList(parseResult.getTokenStream()); + final var nodeFactory = new DefaultNodeFactory(tokenList); + final var astBuilder = new DefaultAstBuilder(nodeFactory); + final var cuNode = (CompilationUnitNode) astBuilder.build(parseResult.getCompilationUnitContext()); + final var bodyNode = cuNode.getBodyNode(); + assertNotNull(bodyNode); + final var gStringBodyTextNode = bodyNode.getAt(0, GStringBodyTextNode.class); + final var transpiler = this.getGStringTranspiler(); + final GStringExpression gStringExpression = transpiler.createGStringExpression(gStringBodyTextNode); + assertEquals("Hello, $target!", gStringExpression.getText()); + } + +} 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 new file mode 100644 index 0000000..5989a68 --- /dev/null +++ b/web-views/src/testFixtures/java/groowt/view/web/transpiler/GroovyTranspilerTests.java @@ -0,0 +1,72 @@ +package groowt.view.web.transpiler; + +import groovy.lang.Tuple2; +import groowt.view.component.ComponentContext; +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.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.*; + +@ExtendWith(MockitoExtension.class) +public abstract class GroovyTranspilerTests { + + protected final GroovyTranspiler transpiler; + private final CompilationUnit groovyCompilationUnit; + + public GroovyTranspilerTests(Tuple2 params) { + this.transpiler = params.getV1(); + this.groovyCompilationUnit = params.getV2(); + } + + @Test + public void smokeScreen() {} + + private void doTranspile( + String source, + String ownerComponentName + ) { + 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()) + ); + + assertDoesNotThrow(() -> { + this.groovyCompilationUnit.compile(CompilePhase.CLASS_GENERATION.getPhaseNumber()); + }); + } + + /** + * Absolute woot! 4/30/24 + */ + @Test + public void helloTarget(@Mock ComponentContext componentContext) { + this.doTranspile( + "Hello, $target!", + "HelloTarget" + ); + } + + @Test + public void withComponent() { + this.doTranspile("", "WithComponent"); + } + +} diff --git a/web-views/src/testFixtures/java/groowt/view/web/transpiler/PreambleTranspilerTests.java b/web-views/src/testFixtures/java/groowt/view/web/transpiler/PreambleTranspilerTests.java new file mode 100644 index 0000000..a0271b8 --- /dev/null +++ b/web-views/src/testFixtures/java/groowt/view/web/transpiler/PreambleTranspilerTests.java @@ -0,0 +1,17 @@ +package groowt.view.web.transpiler; + +import groowt.view.web.transpile.PreambleTranspiler; +import org.junit.jupiter.api.Test; + +public abstract class PreambleTranspilerTests { + + protected final PreambleTranspiler preambleTranspiler; + + public PreambleTranspilerTests(PreambleTranspiler preambleTranspiler) { + this.preambleTranspiler = preambleTranspiler; + } + + @Test + public void smokeScreen() {} + +} diff --git a/web-views/src/testFixtures/java/groowt/view/web/transpiler/TranspilerTestsUtil.java b/web-views/src/testFixtures/java/groowt/view/web/transpiler/TranspilerTestsUtil.java new file mode 100644 index 0000000..348d91e --- /dev/null +++ b/web-views/src/testFixtures/java/groowt/view/web/transpiler/TranspilerTestsUtil.java @@ -0,0 +1,7 @@ +package groowt.view.web.transpiler; + +public final class TranspilerTestsUtil { + + private TranspilerTestsUtil() {} + +} diff --git a/web-views/src/tools/binTemplate.gst b/web-views/src/tools/binTemplate.gst new file mode 100644 index 0000000..9c878c3 --- /dev/null +++ b/web-views/src/tools/binTemplate.gst @@ -0,0 +1,3 @@ +#/usr/bin/env bash + +java -cp build/libs/web-tools.jar:build/libs/web.jar $mainClassName "\$@" diff --git a/web-views/src/tools/groovy/groowt/view/web/tools/AbstractTreeFileMaker.groovy b/web-views/src/tools/groovy/groowt/view/web/tools/AbstractTreeFileMaker.groovy new file mode 100644 index 0000000..542b4e2 --- /dev/null +++ b/web-views/src/tools/groovy/groowt/view/web/tools/AbstractTreeFileMaker.groovy @@ -0,0 +1,43 @@ +package groowt.view.web.tools + +import java.util.regex.Pattern + +abstract class AbstractTreeFileMaker implements SourceFileProcessor { + + private static final Pattern withoutExtension = ~/(?.*)\.(?.+)/ + + private final Scanner scanner = new Scanner(System.in) + + boolean dryRun + String suffix + String extension + File outputDirectory + boolean autoYes + boolean verbose + + protected String getNameWithoutExtension(File file) { + def m = withoutExtension.matcher(file.name) + if (m.matches()) { + return m.group('name') + } else { + throw new IllegalArgumentException("Could not determine file name without extension for ${file}") + } + } + + protected boolean getYesNoInput(String prompt, boolean force = false) { + if (this.autoYes && !force) { + return true + } else { + print prompt + ' ' + while (true) { + if (this.scanner.hasNextLine()) { + def input = this.scanner.nextLine() + if (input in ['y', 'n']) { + return input == 'y' + } + } + } + } + } + +} diff --git a/web-views/src/tools/groovy/groowt/view/web/tools/AstFileMaker.groovy b/web-views/src/tools/groovy/groowt/view/web/tools/AstFileMaker.groovy new file mode 100644 index 0000000..10c0f5b --- /dev/null +++ b/web-views/src/tools/groovy/groowt/view/web/tools/AstFileMaker.groovy @@ -0,0 +1,135 @@ +package groowt.view.web.tools + +import groovy.transform.InheritConstructors +import groovy.transform.MapConstructor +import groowt.view.web.analysis.MismatchedComponentTypeAnalyzer +import groowt.view.web.antlr.AntlrUtil +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.NodeUtil +import groowt.view.web.ast.node.Node +import org.jetbrains.annotations.Nullable + +import static groowt.view.web.antlr.WebViewComponentsParser.CompilationUnitContext + +@InheritConstructors +final class AstFileMaker extends AbstractTreeFileMaker { + + protected sealed interface BuildResult permits BuildSuccess, BuildFailure {} + + @MapConstructor + protected static class BuildSuccess implements BuildResult { + Node node + TokenList tokenList + } + + @MapConstructor + protected static class BuildFailure implements BuildResult { + @Nullable RuntimeException exception + @Nullable CompilationUnitContext compilationUnitContext + @Nullable String message + } + + private void writeFormatted(String name, String formatted) { + this.outputDirectory.mkdirs() + def outFile = new File(this.outputDirectory, name + this.suffix + this.extension) + if (outFile.exists()) { + if (this.getYesNoInput("$outFile already exists. Write over? (y/n)")) { + println "Writing to $outFile..." + outFile.write(formatted) + } else { + println "Skipping writing to $outFile." + } + } else { + println "Writing to $outFile..." + outFile.write(formatted) + } + } + + private boolean onSuccess(String name, BuildSuccess buildSuccess) { + def formatted = NodeUtil.formatAst(buildSuccess.node, buildSuccess.tokenList) + if (!this.autoYes) { + println "Please review the following AST:" + println formatted + } + if (this.getYesNoInput('Would you like to write to disk? (y/n)')) { + this.writeFormatted(name, formatted) + return true + } else { + return !this.getYesNoInput('Do you wish to redo this file? (y/n)') + } + } + + private boolean onFailure(String name, BuildFailure buildFailure) { + if (buildFailure.exception != null) { + println 'There was an exception during parsing/ast-building:' + buildFailure.exception.printStackTrace() + } else if (buildFailure.message != null) { + println buildFailure.message + } + if (this.getYesNoInput('Would you like to try again? (y/n)', true)) { + println "Re-processing $name..." + return false + } else { + println "Skipping $name after failure." + return true + } + } + + private BuildResult build(File sourceFile) { + try { + def parseResult = ParserUtil.parseCompilationUnit(sourceFile) + + def cuContext = parseResult.compilationUnitContext + + def errorCollector = AntlrUtil.findErrorNodes(cuContext) + if (!errorCollector.isEmpty()) { + def message = 'There were parsing/lexing errors: \n' + + errorCollector.all.collect { + ParserUtil.formatTree(parseResult.parser, it, true) + '\n' + } + return new BuildFailure( + compilationUnitContext: cuContext, + message: message + ) + } + + def mismatchedTypeAnalyzer = new MismatchedComponentTypeAnalyzer() + def mismatchedTypeErrors = mismatchedTypeAnalyzer.analyze(cuContext) + + if (!mismatchedTypeErrors.isEmpty()) { + def message = 'There were mismatched type errors: \n' + mismatchedTypeErrors.collect { + it.message() + }.join('\n') + return new BuildFailure( + compilationUnitContext: cuContext, + message: message + ) + } + + def tokenList = new TokenList(parseResult.tokenStream) + def astBuilder = new DefaultAstBuilder(new DefaultNodeFactory(tokenList)) + return new BuildSuccess(node: astBuilder.build(cuContext), tokenList: tokenList) + } catch (RuntimeException exception) { + return new BuildFailure(exception: exception) + } + } + + @Override + void process(File sourceFile) { + def name = this.getNameWithoutExtension(sourceFile) + println "Processing $name" + boolean doneYet = false + while (!doneYet) { + def buildResult = this.build(sourceFile) + if (buildResult instanceof BuildSuccess) { + doneYet = this.onSuccess(name, buildResult) + } else { + doneYet = this.onFailure(name, buildResult as BuildFailure) + } + } + } + +} diff --git a/web-views/src/tools/groovy/groowt/view/web/tools/AstFileMakerCli.groovy b/web-views/src/tools/groovy/groowt/view/web/tools/AstFileMakerCli.groovy new file mode 100644 index 0000000..aa766ef --- /dev/null +++ b/web-views/src/tools/groovy/groowt/view/web/tools/AstFileMakerCli.groovy @@ -0,0 +1,29 @@ +package groowt.view.web.tools + +import picocli.CommandLine + +@CommandLine.Command( + name = 'astFileMaker', + mixinStandardHelpOptions = true, + description = 'Create ast file(s) from given source file(s).' +) +class AstFileMakerCli extends SourceFileProcessorSpec { + + static void main(String[] args) { + System.exit(new CommandLine(new AstFileMakerCli()).execute(args)) + } + + AstFileMakerCli() { + super({ SourceFileProcessorSpec spec -> + new AstFileMaker( + dryRun: spec.dryRun, + suffix: spec.suffix.orElse('_ast'), + extension: spec.extension, + outputDirectory: spec.outputDirectory.orElse(new File('src/test/ast/trees')), + autoYes: spec.autoYes, + verbose: spec.verbose + ) + }, 'src/test/ast') + } + +} 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 new file mode 100644 index 0000000..2f900d4 --- /dev/null +++ b/web-views/src/tools/groovy/groowt/view/web/tools/ConvertToGroovy.groovy @@ -0,0 +1,99 @@ +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 picocli.CommandLine + +import java.util.concurrent.Callable + +@CommandLine.Command( + name = 'convertToGroovy', + description = 'Convert a given wvc source file to groovy source code.', + mixinStandardHelpOptions = true +) +class ConvertToGroovy implements Callable { + + static void main(String[] args) { + System.exit(new CommandLine(new ConvertToGroovy()).execute(args)) + } + + @CommandLine.Parameters(arity = '1..*', description = 'The source files to convert.') + List targets + + @CommandLine.Option(names = ['-p', '--package'], description = 'The default package name for the targets.') + String defaultPackageName + + @CommandLine.Option(names = ['-o', '--out'], description = 'Write source files to disk instead of printing them.') + boolean writeOut + + // Default is Phases.CLASS_GENERATION (7) + @CommandLine.Option( + names = ['-t', '--compilePhase'], + defaultValue = '7', + description = 'The groovy compile phase to target.' + ) + int compilePhase + + @CommandLine.Option( + names = ['-d', '--classesDir'], + description = 'If the GroovyCompiler outputs classes, where to write them.' + ) + File classesDir + + @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.classesDir ?: new File(target.parentFile, 'classes') + } + def gcu = new CompilationUnit(config) + + def w = new StringWriter() + def astVisitor = new AstNodeToScriptVisitor(w) + gcu.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) + + if (this.writeOut) { + def outFile = new File(target.parentFile, name + '.groovy') + outFile.write(w.toString()) + } else { + println w.toString() + } + return true + } catch (Exception e) { + e.printStackTrace() + return false + } + } + return success ? 0 : 1 + } + +} diff --git a/web-views/src/tools/groovy/groowt/view/web/tools/ParseTreeFileMaker.groovy b/web-views/src/tools/groovy/groowt/view/web/tools/ParseTreeFileMaker.groovy new file mode 100644 index 0000000..22a89f5 --- /dev/null +++ b/web-views/src/tools/groovy/groowt/view/web/tools/ParseTreeFileMaker.groovy @@ -0,0 +1,88 @@ +package groowt.view.web.tools + +import groovy.transform.InheritConstructors +import groowt.view.web.antlr.* +import groowt.view.web.antlr.AntlrUtil.ParseErrorCollector +import groowt.view.web.antlr.WebViewComponentsParser.CompilationUnitContext +import org.antlr.v4.runtime.CharStreams + +@InheritConstructors +final class ParseTreeFileMaker extends AbstractTreeFileMaker { + + private void writeFormatted(String name, WebViewComponentsParser parser, CompilationUnitContext cu) { + this.outputDirectory.mkdirs() + def formatted = ParserUtil.formatTree(parser, cu, false) + def out = new File(this.outputDirectory, name + this.suffix + this.extension) + if (out.exists()) { + if (this.getYesNoInput("${out} already exists. Write over? (y/n)")) { + println "Writing to $out..." + out.write(formatted) + } else { + println "Skipping writing to $out." + } + } else { + println "Writing to $out..." + out.write(formatted) + } + } + + /** + * @return true if done now, false if not done yet + */ + private boolean onSuccess(String name, WebViewComponentsParser parser, CompilationUnitContext cu) { + if (!this.autoYes) { + println 'Please preview the formatted tree:' + println ParserUtil.formatTree(parser, cu, true) + } + if (this.getYesNoInput('Write to disk? (y/n)')) { + this.writeFormatted(name, parser, cu) + return true + } else { + return !this.getYesNoInput('Do you wish to redo this file? (y/n)') + } + } + + /** + * @return true if done now, false if not done yet + */ + private boolean onErrors(String name, WebViewComponentsParser parser, CompilationUnitContext cu, ParseErrorCollector errors) { + def errorCount = errors.errorCount + def isOne = errorCount == 1 + def formatted = ParserUtil.formatTree(parser, cu, true) + println "There ${isOne ? 'was' : 'were'} ${errorCount} error${isOne ? '' : 's'} during parsing:" + println formatted + if (this.getYesNoInput('Do you wish to try again? (y/n)', true)) { + println "trying $name again..." + return false + } else { + println "skipping $name..." + return true + } + } + + private Tuple3 parse(File sourceFile) { + def input = CharStreams.fromFileName(sourceFile.toString()) + def lexer = new WebViewComponentsLexer(input) + def tokenStream = new WebViewComponentsTokenStream(lexer) + def parser = new WebViewComponentsParser(tokenStream) + def cu = parser.compilationUnit() + def errors = AntlrUtil.findErrorNodes(cu) + new Tuple3<>(parser, cu, errors) + } + + @Override + void process(File sourceFile) { + def name = this.getNameWithoutExtension(sourceFile) + println "processing: $name" + boolean doneYet = false + while (!doneYet) { + def (parser, cu, errors) = this.parse(sourceFile) + if (errors.isEmpty()) { + doneYet = this.onSuccess(name, parser, cu) + } else { + doneYet = this.onErrors(name, parser, cu, errors) + } + } + } + +} diff --git a/web-views/src/tools/groovy/groowt/view/web/tools/ParseTreeFileMakerCli.groovy b/web-views/src/tools/groovy/groowt/view/web/tools/ParseTreeFileMakerCli.groovy new file mode 100644 index 0000000..6040656 --- /dev/null +++ b/web-views/src/tools/groovy/groowt/view/web/tools/ParseTreeFileMakerCli.groovy @@ -0,0 +1,30 @@ +package groowt.view.web.tools + +import picocli.CommandLine +import picocli.CommandLine.Command + +@Command( + name = 'parseTreeFileMaker', + mixinStandardHelpOptions = true, + description = 'Create parse tree file(s) from given source file(s).' +) +class ParseTreeFileMakerCli extends SourceFileProcessorSpec { + + static void main(String[] args) { + System.exit(new CommandLine(new ParseTreeFileMakerCli()).execute(args)) + } + + ParseTreeFileMakerCli() { + super({ SourceFileProcessorSpec spec -> + new ParseTreeFileMaker( + dryRun: spec.dryRun, + suffix: spec.suffix.orElse('_parseTree'), + extension: spec.extension, + outputDirectory: spec.outputDirectory.orElse(new File('src/test/parser/trees')), + autoYes: spec.autoYes, + verbose: spec.verbose + ) + }, 'src/test/parser') + } + +} diff --git a/web-views/src/tools/groovy/groowt/view/web/tools/SourceFileProcessor.groovy b/web-views/src/tools/groovy/groowt/view/web/tools/SourceFileProcessor.groovy new file mode 100644 index 0000000..cdf1c45 --- /dev/null +++ b/web-views/src/tools/groovy/groowt/view/web/tools/SourceFileProcessor.groovy @@ -0,0 +1,6 @@ +package groowt.view.web.tools + +@FunctionalInterface +interface SourceFileProcessor { + void process(File sourceFile) +} diff --git a/web-views/src/tools/groovy/groowt/view/web/tools/SourceFileProcessorSpec.groovy b/web-views/src/tools/groovy/groowt/view/web/tools/SourceFileProcessorSpec.groovy new file mode 100644 index 0000000..01f5685 --- /dev/null +++ b/web-views/src/tools/groovy/groowt/view/web/tools/SourceFileProcessorSpec.groovy @@ -0,0 +1,100 @@ +package groowt.view.web.tools + +import picocli.CommandLine + +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.Path +import java.util.concurrent.Callable +import java.util.function.Function + +abstract class SourceFileProcessorSpec implements Callable { + + @CommandLine.Parameters( + arity = '1..*', + defaultValue = '*.wvc', + description = 'The web view component source file(s) to process, as globs relative to the source directory.' + ) + List targets + + @CommandLine.Option( + names = ['--dryRun'], + defaultValue = 'false', + description = 'Do not actually write the tree files to disk.' + ) + boolean dryRun + + @CommandLine.Option( + names = ['-s', '--suffix'], + description = 'The suffix to append to the written tree file name(s).' + ) + Optional suffix + + @CommandLine.Option( + names = ['-e', '--extension'], + defaultValue = '.txt', + description = 'The extension to append to the written tree file(s).' + ) + String extension + + @CommandLine.Option( + names = ['-d', '--sourceDirectory'], + description = 'The directory where the source files are stored.' + ) + Optional srcDir + + @CommandLine.Option( + names = ['-o', '--outputDirectory'], + description = 'The directory to which the output files should be written.' + ) + Optional outputDirectory + + @CommandLine.Option( + names = ['-y', '--yes'], + description = 'Whether to automatically write all formatted trees to disk, including cases where old data will be written over.' + ) + boolean autoYes + + @CommandLine.Option( + names = ['-v', '--verbose'], + description = 'Whether to output verbose information during processing.' + ) + boolean verbose + + private final Function getSourceFileProcessor + private final String defaultSourceDir + + SourceFileProcessorSpec( + Function getSourceFileProcessor, + String defaultSourceDir + ) { + this.getSourceFileProcessor = getSourceFileProcessor + this.defaultSourceDir = defaultSourceDir + } + + @Override + Integer call() throws Exception { + def fs = FileSystems.default + def resolvedSrcDir = this.srcDir.orElse(this.defaultSourceDir) + def srcDirPath = Path.of(resolvedSrcDir) + def globBase = 'glob:' + resolvedSrcDir + def sourceFiles = this.targets.collectMany { + def matcher = fs.getPathMatcher(globBase + File.separator + it) + Files.walk(srcDirPath).filter { matcher.matches(it) } + .map { it.toFile() } + .filter { it.isFile() } + .toList() + } + + if (sourceFiles.isEmpty()) { + System.err.println('There are no matching source files for the given targets.') + return 1 + } + + def processor = this.getSourceFileProcessor.apply(this) + sourceFiles.each(processor.&process) + + return 0 + } + +} diff --git a/web-views/src/tools/groovy/groowt/view/web/tools/inspectNodes.groovy b/web-views/src/tools/groovy/groowt/view/web/tools/inspectNodes.groovy new file mode 100644 index 0000000..2e67e0b --- /dev/null +++ b/web-views/src/tools/groovy/groowt/view/web/tools/inspectNodes.groovy @@ -0,0 +1,47 @@ +package groowt.view.web.tools + +import groowt.view.web.transpile.util.GroovyUtil +import org.codehaus.groovy.ast.ImportNode + +import static groowt.view.web.transpile.util.GroovyUtil.formatGroovy + +def src = ''' +import some.Thing +def myVar = 3 +class Helper { } +''' + +def quickConvertResult = GroovyUtil.convert(src, 'MyTemplate') +println formatGroovy(quickConvertResult.blockStatement()) + +quickConvertResult. classNodes().each { println formatGroovy(it) } + +quickConvertResult. moduleNode().with { + println "Module: ${it}" + def indentTimes = 1 + def getIndent = { ' '.repeat(indentTimes) } + it.properties.each { key, value -> + if (key instanceof String && key in ['imports', 'starImports']) { + def importNodes = it[key] as List + if (importNodes.size() > 0) { + println getIndent() + "$key: " + indentTimes++ + importNodes.each { println getIndent() + it.text } + indentTimes-- + return + } + } else if (key instanceof String && key in ['staticImports', 'staticStarImports']) { + def staticImports = it[key] as Map + if (staticImports.size() > 0) { + println getIndent() + "$key: " + indentTimes++ + staticImports.each { alias, importNode -> + println getIndent() + "$alias: $importNode" + } + indentTimes-- + return + } + } + println getIndent() +"$key: $value" + } +} diff --git a/web-views/src/tools/kotlin/groowt/view/web/tools/LexerTool.kt b/web-views/src/tools/kotlin/groowt/view/web/tools/LexerTool.kt new file mode 100644 index 0000000..83434fe --- /dev/null +++ b/web-views/src/tools/kotlin/groowt/view/web/tools/LexerTool.kt @@ -0,0 +1,12 @@ +@file:JvmName("LexerTool") +package groowt.view.web.tools + +import groowt.view.web.antlr.formatToken +import groowt.view.web.antlr.runLexerAllTokens + +fun main(args: Array) { + val options = processArgs(args) + configureLog(options.logLevel) + val input = getInput(options.source) + runLexerAllTokens(input).forEachIndexed { i, t -> println("$i: ${formatToken(t)}") } +} diff --git a/web-views/src/tools/kotlin/groowt/view/web/tools/ParserTool.kt b/web-views/src/tools/kotlin/groowt/view/web/tools/ParserTool.kt new file mode 100644 index 0000000..3a17b6e --- /dev/null +++ b/web-views/src/tools/kotlin/groowt/view/web/tools/ParserTool.kt @@ -0,0 +1,18 @@ +@file:JvmName("ParserTool") +package groowt.view.web.tools + +import groowt.view.web.antlr.WebViewComponentsLexer +import groowt.view.web.antlr.WebViewComponentsTokenStream +import groowt.view.web.antlr.formatTree +import groowt.view.web.antlr.parseCompilationUnit + +fun main(args: Array) { + val options = processArgs(args) + configureLog(options.logLevel) + val input = getInput(options.source) + val lexer = WebViewComponentsLexer(input) + val tokenStream = WebViewComponentsTokenStream(lexer) + parseCompilationUnit(tokenStream) { cu, parser -> + println(formatTree(parser, cu)) + } +} diff --git a/web-views/src/tools/kotlin/groowt/view/web/tools/ToolUtil.kt b/web-views/src/tools/kotlin/groowt/view/web/tools/ToolUtil.kt new file mode 100644 index 0000000..c75ece0 --- /dev/null +++ b/web-views/src/tools/kotlin/groowt/view/web/tools/ToolUtil.kt @@ -0,0 +1,80 @@ +@file:JvmName("ToolUtil") +package groowt.view.web.tools + +import org.antlr.v4.runtime.CharStream +import org.antlr.v4.runtime.CharStreams +import org.apache.logging.log4j.Level +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.core.config.Configurator +import java.lang.invoke.MethodHandles +import java.net.URI + +private fun getLogConfiguration(): URI = + MethodHandles.lookup().lookupClass().getResource("/log4j2.xml")?.toURI() + ?: throw RuntimeException("cannot find resource /log4j2.xml") + +internal fun configureLog(levelName: String) { + val level = Level.toLevel(levelName) + if (level == null) { + LogManager.getRootLogger().warn("Unknown log level: $levelName") + } else { + Configurator.reconfigure(getLogConfiguration()) + Configurator.setLevel(MethodHandles.lookup().lookupClass().packageName, level) + } +} + +internal sealed interface Source + +internal class FileSource(private val fileName: String) : Source { + fun get() = this.fileName +} + +internal class StringSource(private val s: String) : Source { + fun get() = this.s +} + +internal data class CliOptions(val source: Source, val logLevel: String) + +internal fun processArgs(args: Array): CliOptions { + var source: Source? = null + var logLevel: String? = null + val argsIter = args.iterator() + while (argsIter.hasNext()) { + when (val current = argsIter.next()) { + "--file" -> { + val fileName = if (argsIter.hasNext()) { + argsIter.next() + } else { + throw IllegalArgumentException("--file must be followed by a file name!") + } + source = FileSource(fileName) + } + "--source" -> { + val rawSource = if (argsIter.hasNext()) { + argsIter.next() + } else { + throw IllegalArgumentException("--source must be followed by a string source!") + } + source = StringSource(rawSource) + } + else -> { + if (current.startsWith("--logLevel")) { + logLevel = current.substringAfter("=") + } else { + throw IllegalArgumentException("Unknown option: $current") + } + } + } + } + + if (source == null) { + throw IllegalArgumentException("must provide a source with either --file or --source") + } + + return CliOptions(source, logLevel ?: "info") +} + +internal fun getInput(source: Source): CharStream = when (source) { + is FileSource -> CharStreams.fromFileName(source.get()) + is StringSource -> CharStreams.fromString(source.get()) +}