Added some convenience methods to ComponentScope and related factories, etc. Fixed RunTemplate.

This commit is contained in:
JesseBrault0709 2024-05-12 11:50:54 +02:00
parent e4e972ea7b
commit e22fc1622e
14 changed files with 112 additions and 175 deletions

View File

@ -49,8 +49,6 @@ public abstract class AbstractViewComponent implements ViewComponent {
this.template = instantiateTemplate(templateClass);
}
@Override
public void setContext(ComponentContext context) {
this.context = context;

View File

@ -1,5 +1,10 @@
package groowt.view.component.context;
import groovy.lang.Closure;
import groovy.lang.DelegatesTo;
import groovy.transform.stc.ClosureParams;
import groovy.transform.stc.FirstParam;
import groovy.transform.stc.SimpleType;
import groowt.view.component.ViewComponent;
import groowt.view.component.runtime.RenderContext;
import org.jetbrains.annotations.Nullable;
@ -25,6 +30,27 @@ public interface ComponentContext {
return scopeClass.cast(this.getRootScope());
}
default void configureRootScope(
@ClosureParams(value = SimpleType.class, options = "groowt.view.component.context.ComponentScope")
@DelegatesTo(ComponentScope.class)
Closure<?> configure
) {
final var rootScope = this.getRootScope();
configure.setDelegate(rootScope);
configure.call(rootScope);
}
default <S extends ComponentScope> void configureRootScope(
Class<S> scopeClass,
@ClosureParams(value = FirstParam.FirstGenericType.class)
@DelegatesTo(type = "S")
Closure<?> configure
) {
final var rootScope = this.getRootScope(scopeClass);
configure.setDelegate(rootScope);
configure.call(rootScope);
}
default ComponentScope getCurrentScope() {
final List<ComponentScope> scopeStack = this.getScopeStack();
if (scopeStack.isEmpty()) {
@ -37,6 +63,27 @@ public interface ComponentContext {
return scopeClass.cast(this.getCurrentScope());
}
default void configureCurrentScope(
@ClosureParams(value = SimpleType.class, options = "groowt.view.component.context.ComponentScope")
@DelegatesTo(ComponentScope.class)
Closure<?> configure
) {
final var currentScope = this.getCurrentScope();
configure.setDelegate(currentScope);
configure.call(currentScope);
}
default <S extends ComponentScope> void configureCurrentScope(
Class<S> scopeClass,
@ClosureParams(value = FirstParam.FirstGenericType.class)
@DelegatesTo(type = "S")
Closure<?> configure
) {
final var currentScope = this.getCurrentScope(scopeClass);
configure.setDelegate(currentScope);
configure.call(currentScope);
}
@Nullable ViewComponent getParent();
default <T extends ViewComponent> @Nullable T getParent(Class<T> parentClass) {

View File

@ -1,6 +1,7 @@
package groowt.view.component.context;
import groowt.view.component.ViewComponent;
import groowt.view.component.factory.ComponentFactories;
import groowt.view.component.factory.ComponentFactory;
public interface ComponentScope {
@ -11,6 +12,10 @@ public interface ComponentScope {
<T extends ViewComponent> void add(String typeName, Class<T> forClass, ComponentFactory<? extends T> factory);
default <T extends ViewComponent> void addWithNoArgConstructor(String typeName, Class<T> forClass) {
this.add(typeName, forClass, ComponentFactories.ofNoArgConstructor(forClass));
}
boolean contains(String typeName);
void remove(String typeName);
@ -34,6 +39,17 @@ public interface ComponentScope {
ComponentFactory<? extends T> factory
);
default <T extends ViewComponent> void addWithNoArgConstructor(Class<T> forClass) {
this.add(forClass, ComponentFactories.ofNoArgConstructor(forClass));
}
default <T extends ViewComponent> void addWithNoArgConstructor(
Class<T> publicType,
Class<? extends T> implementingClass
) {
this.add(publicType, implementingClass, ComponentFactories.ofNoArgConstructor(implementingClass));
}
boolean contains(Class<? extends ViewComponent> type);
<T extends ViewComponent> TypeAndFactory<T> get(Class<T> type);

View File

@ -22,6 +22,18 @@ public final class ComponentFactories {
return (typeName, componentContext, args) -> supplier.get();
}
public static <T extends ViewComponent> ComponentFactory<T> ofNoArgConstructor(
Class<? extends T> viewComponentClass
) {
return (typeName, componentContext, args) -> {
try {
return viewComponentClass.getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
private ComponentFactories() {}
}

View File

@ -1,107 +0,0 @@
package groowt.view.component.factory;
import groovy.lang.GroovyObjectSupport;
import groovy.lang.MetaClass;
import groovy.lang.MetaMethod;
import groovy.lang.MissingMethodException;
import groowt.view.component.context.ComponentContext;
import groowt.view.component.ViewComponent;
import java.util.HashMap;
import java.util.Map;
/**
* This class can be used to create custom implementations {@link ComponentFactory}.
*
* @implSpec All implementations must simply provide one or more {@code doCreate()} methods,
* which will be found by this class via the Groovy meta object protocol. The method(s) may
* have any of the following signatures:
* <ul>
* <li>{@code String | Class, ComponentContext, ... -> T}</li>
* <li>{@code ComponentContext, ... -> T}</li>
* <li>{@code String | Class, ... -> T}</li>
* <li>{@code ... -> }</li>
* </ul>
* where '{@code ...}' represents zero or more additional arguments.
*
* @implNote In most cases, the implementation does not need to consume the
* {@link ComponentContext} argument, as the compiled template is required to contain
* {@code component.setContext(context)} statements following the component
* creation call. However, if the component <strong>needs</strong> the context
* (for example, to do custom scope logic), then it is more than okay to consume it.
*
* @implNote In the case Web View Components, the first additional argument will be
* a {@link Map} containing the attributes of the component, followed by any additional
* component constructor args.
*
* @param <T> The type of the ViewComponent produced by this factory.
*/
public abstract class ComponentFactoryBase<T extends ViewComponent> extends GroovyObjectSupport
implements ComponentFactory<T> {
protected static final String DO_CREATE = "doCreate";
protected static MetaMethod findDoCreateMethod(MetaClass metaClass, Class<?>[] types) {
return metaClass.getMetaMethod(DO_CREATE, types);
}
protected final Map<Class<?>[], MetaMethod> cache = new HashMap<>();
protected MetaMethod findDoCreateMethod(Object[] allArgs) {
return this.cache.computeIfAbsent(ComponentFactoryUtil.asTypes(allArgs), types ->
findDoCreateMethod(this.getMetaClass(), types)
);
}
@SuppressWarnings("unchecked")
protected T findAndDoCreate(Object type, ComponentContext componentContext, Object[] args) {
final Object[] typeContextAndArgs = ComponentFactoryUtil.flatten(type, componentContext, args);
final MetaMethod typeContextAndArgsMethod = this.findDoCreateMethod(typeContextAndArgs);
if (typeContextAndArgsMethod != null) {
return (T) typeContextAndArgsMethod.invoke(this, typeContextAndArgs);
}
final Object[] typeAndContext = new Object[] { type, componentContext };
final MetaMethod typeAndContextMethod = this.findDoCreateMethod(typeAndContext);
if (typeAndContextMethod != null) {
return (T) typeAndContextMethod.invoke(this, typeAndContext);
}
final Object[] typeAndArgs = ComponentFactoryUtil.flatten(type, args);
final MetaMethod typeAndArgsMethod = this.findDoCreateMethod(typeAndArgs);
if (typeAndArgsMethod != null) {
return (T) typeAndArgsMethod.invoke(this, typeAndArgs);
}
final Object[] typeOnly = new Object[] { type };
final MetaMethod typeOnlyMethod = this.findDoCreateMethod(typeOnly);
if (typeOnlyMethod != null) {
return (T) typeOnlyMethod.invoke(this, typeOnly);
}
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(String typeName, ComponentContext componentContext, Object... args) {
throw new UnsupportedOperationException();
}
// TODO: this needs to be updated.
@Override
public T create(String alias, Class<?> type, ComponentContext componentContext, Object... args) {
throw new UnsupportedOperationException();
}
}

View File

@ -20,7 +20,7 @@ public abstract class AbstractRenderContext implements RenderContext {
this.writer = writer;
}
public ComponentContext getComponentContext() {
protected ComponentContext getComponentContext() {
return this.componentContext;
}

View File

@ -15,11 +15,10 @@ public interface ComponentWriter {
void append(String string);
void append(GString gString);
void append(GString gString, int line, int column);
void append(ViewComponent viewComponent);
void append(ViewComponent viewComponent, int line, int column);
void append(Object object);
@SuppressWarnings("unused")
default void leftShift(Object object) {
switch (object) {
case String s -> this.append(s);

View File

@ -57,33 +57,14 @@ public class DefaultComponentWriter implements ComponentWriter {
}
}
@Override
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);
}
}
private void doComponentRender(ViewComponent viewComponent) throws IOException {
this.getRenderContext().pushComponent(viewComponent);
this.getComponentContext().pushDefaultScope();
viewComponent.renderTo(this.delegate);
this.getComponentContext().popScope();
this.getRenderContext().popComponent(viewComponent);
}
@Override
public void append(ViewComponent viewComponent) {
try {
this.doComponentRender(viewComponent);
this.getRenderContext().pushComponent(viewComponent);
this.getComponentContext().pushDefaultScope();
viewComponent.renderTo(this.delegate);
this.getComponentContext().popScope();
this.getRenderContext().popComponent(viewComponent);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
} catch (ComponentRenderException componentRenderException) {
@ -93,19 +74,6 @@ public class DefaultComponentWriter implements ComponentWriter {
}
}
@Override
public void append(ViewComponent viewComponent, int line, int column) {
try {
this.doComponentRender(viewComponent);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
} catch (ComponentRenderException componentRenderException) {
throw componentRenderException;
} catch (Exception exception) {
throw new ComponentRenderException(viewComponent, line, column, exception);
}
}
@Override
public void append(Object object) {
switch (object) {

View File

@ -1,4 +1,4 @@
---
import groowt.view.web.lib.Echo
---
<Echo greeting="Hello!"><p>${context}</p></Echo>
<Echo greeting=${cliGreeting}><p>$greeting</p></Echo>

View File

@ -7,7 +7,7 @@ import org.codehaus.groovy.runtime.InvokerHelper
import static groowt.view.web.WebViewComponentFactories.withAttr
class DefaultWebViewComponentScope extends DefaultComponentScope {
class DefaultWebViewComponentScope extends DefaultComponentScope implements WebViewComponentScope {
static DefaultWebViewComponentScope getDefaultRootScope() {
new DefaultWebViewComponentScope().tap {
@ -15,6 +15,7 @@ class DefaultWebViewComponentScope extends DefaultComponentScope {
}
}
@Override
<T extends WebViewComponent> void addWithAttr(Class<T> componentClass) {
add(componentClass, withAttr(componentClass) { attr ->
InvokerHelper.invokeConstructorOf(componentClass, attr) as T

View File

@ -0,0 +1,7 @@
package groowt.view.web;
import groowt.view.component.context.ComponentScope;
public interface WebViewComponentScope extends ComponentScope {
<T extends WebViewComponent> void addWithAttr(Class<T> componentClass);
}

View File

@ -1,6 +1,5 @@
package groowt.view.web
import groowt.view.component.factory.ComponentFactories
import groowt.view.web.lib.AbstractWebViewComponentTests
import org.junit.jupiter.api.Test
@ -44,8 +43,8 @@ class BaseWebViewComponentTests extends AbstractWebViewComponentTests {
@Test
void nestedGreeter() {
def context = this.context {
getRootScope(DefaultWebViewComponentScope).with {
def context = this.context() {
configureRootScope(WebViewComponentScope) {
addWithAttr(Greeter)
}
}
@ -55,9 +54,9 @@ class BaseWebViewComponentTests extends AbstractWebViewComponentTests {
@Test
void doubleNested() {
def context = this.context {
getRootScope(DefaultWebViewComponentScope).with {
configureRootScope(WebViewComponentScope) {
addWithAttr(Greeter)
add(UsingGreeter, ComponentFactories.ofSupplier { new UsingGreeter() })
addWithNoArgConstructor(UsingGreeter)
}
}
this.doTest('<BaseWebViewComponentTests.UsingGreeter />', 'Hello, World!', context)

View File

@ -1,8 +1,8 @@
package groowt.view.web.lib
import groowt.view.web.BaseWebViewComponent
import groowt.view.web.DefaultWebViewComponentScope
import groowt.view.web.WebViewComponentContext
import groowt.view.web.WebViewComponentScope
import org.junit.jupiter.api.Test
class FragmentTests extends AbstractWebViewComponentTests {
@ -20,7 +20,7 @@ class FragmentTests extends AbstractWebViewComponentTests {
@Override
void configureContext(WebViewComponentContext context) {
context.getRootScope(DefaultWebViewComponentScope).with {
context.configureRootScope(WebViewComponentScope) {
addWithAttr(Greeter)
}
}

View File

@ -1,10 +1,10 @@
package groowt.view.web.tools
import groowt.view.component.ComponentTemplate
import groowt.view.component.compiler.SimpleComponentTemplateClassFactory
import groowt.view.component.compiler.source.ComponentTemplateSource
import groowt.view.component.context.DefaultComponentContext
import groowt.view.component.runtime.DefaultComponentWriter
import groowt.view.web.BaseWebViewComponent
import groowt.view.web.DefaultWebViewComponentContext
import groowt.view.web.compiler.AnonymousWebViewComponent
import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit
import picocli.CommandLine
@ -27,16 +27,15 @@ class RunTemplate implements Callable<Integer> {
names = ['-A', '--attr', '--attribute'],
description = 'Attribute(s) to pass to the template.'
)
Map<String, String> properties
Map<String, String> attr
static class PropertiesComponent extends BaseWebViewComponent {
static class RunnableTemplate extends BaseWebViewComponent {
private final Map<String, String> properties
private final Map<String, String> cliAttr
@SuppressWarnings('GroovyAssignabilityCheck')
PropertiesComponent(Map<String, Object> attr) {
super('${renderChildren()}')
this.properties = attr.properties ?: [:]
RunnableTemplate(Class<? extends ComponentTemplate> templateClass, Map<String, String> cliAttr) {
super(templateClass)
this.cliAttr = cliAttr
}
@Override
@ -44,7 +43,7 @@ class RunTemplate implements Callable<Integer> {
try {
return super.getProperty(propertyName)
} catch (Exception ignored) {
return this.properties.get(propertyName)
return this.cliAttr.get(propertyName)
}
}
@ -61,14 +60,12 @@ class RunTemplate implements Callable<Integer> {
def compileResult = compileUnit.compile()
def templateLoader = new SimpleComponentTemplateClassFactory()
def templateClass = templateLoader.getTemplateClass(compileResult)
def template = templateClass.getConstructor().newInstance()
def context = new DefaultComponentContext()
context.pushDefaultScope()
def runnableTemplate = new RunnableTemplate(templateClass, this.attr)
def componentContext = new DefaultWebViewComponentContext()
runnableTemplate.context = componentContext
def componentWriter = new DefaultComponentWriter(new OutputStreamWriter(System.out))
template.renderer.call(context, componentWriter)
println runnableTemplate.render()
return 0
}