diff --git a/view-components/src/main/java/groowt/view/component/ClosureComponentFactory.java b/view-components/src/main/java/groowt/view/component/ClosureComponentFactory.java index 537e399..2428788 100644 --- a/view-components/src/main/java/groowt/view/component/ClosureComponentFactory.java +++ b/view-components/src/main/java/groowt/view/component/ClosureComponentFactory.java @@ -2,18 +2,52 @@ package groowt.view.component; import groovy.lang.Closure; -final class ClosureComponentFactory extends AbstractComponentFactory { +import static groowt.view.component.ComponentFactoryUtil.flatten; + +final class ClosureComponentFactory extends ComponentFactoryBase { private final Closure closure; + private final Class firstParamType; public ClosureComponentFactory(Closure closure) { this.closure = closure; + if (this.closure.getParameterTypes().length < 2) { + throw new IllegalArgumentException( + "Closures for " + getClass().getName() + " require at least two parameters" + ); + } + this.firstParamType = this.closure.getParameterTypes()[0]; + if (this.firstParamType != Object.class + && !(this.firstParamType == String.class || this.firstParamType == Class.class)) { + throw new IllegalArgumentException( + "The first closure parameter must be any of type Object (i.e, dynamic), String, or Class" + ); + } + final var secondParamType = this.closure.getParameterTypes()[1]; + if (secondParamType != Object.class && !ComponentContext.class.isAssignableFrom(secondParamType)) { + throw new IllegalArgumentException( + "The second closure parameter must be of type Object (i.e., dynamic) or " + + "ComponentContext or a subclass thereof." + ); + } } @Override - public T create(Object type, ComponentContext componentContext, Object... args) { - final Object[] flattened = ComponentFactoryUtil.flatten(type, componentContext, args); - return this.closure.call(flattened); + public T create(String type, ComponentContext componentContext, Object... args) { + if (this.firstParamType != Object.class && this.firstParamType != String.class) { + throw new IllegalArgumentException("Cannot call this ClosureComponentFactory " + + "with a String component type argument."); + } + return this.closure.call(flatten(type, componentContext, args)); + } + + @Override + public T create(Class type, ComponentContext componentContext, Object... args) { + if (this.firstParamType != Object.class && this.firstParamType != Class.class) { + throw new IllegalArgumentException("Cannot call this ClosureComponentFactory " + + "with a Class component type argument."); + } + return this.closure.call(flatten(type, componentContext, args)); } } diff --git a/view-components/src/main/java/groowt/view/component/ComponentContext.java b/view-components/src/main/java/groowt/view/component/ComponentContext.java index 477e255..a5a3a0f 100644 --- a/view-components/src/main/java/groowt/view/component/ComponentContext.java +++ b/view-components/src/main/java/groowt/view/component/ComponentContext.java @@ -10,20 +10,36 @@ import java.util.function.Predicate; public interface ComponentContext { + /** + * For use only by compiled templates. + */ @ApiStatus.Internal interface Resolved { String getTypeName(); ComponentFactory getComponentFactory(); } + /** + * For use only by compiled templates. + */ @ApiStatus.Internal Resolved resolve(String component); + /** + * For use only by compiled templates. + */ @ApiStatus.Internal ViewComponent create(Resolved resolved, Object... args); + /** + * For use only by compiled templates. + */ + @ApiStatus.Internal void beforeComponentRender(ViewComponent component); + /** + * For use only by compiled templates. + */ @ApiStatus.Internal void afterComponentRender(ViewComponent component); diff --git a/view-components/src/main/java/groowt/view/component/ComponentCreateException.java b/view-components/src/main/java/groowt/view/component/ComponentCreateException.java index 3e0db81..3b8bfc2 100644 --- a/view-components/src/main/java/groowt/view/component/ComponentCreateException.java +++ b/view-components/src/main/java/groowt/view/component/ComponentCreateException.java @@ -1,13 +1,25 @@ package groowt.view.component; +/** + * An exception which signals that a component of the given type + * could not be created in the given template. + */ public class ComponentCreateException extends RuntimeException { + private final Object componentType; private final ComponentTemplate template; private final int line; private final int column; - public ComponentCreateException(ComponentTemplate template, int line, int column, Throwable cause) { + public ComponentCreateException( + Object componentType, + ComponentTemplate template, + int line, + int column, + Throwable cause + ) { super(cause); + this.componentType = componentType; this.template = template; this.line = line; this.column = column; @@ -15,8 +27,8 @@ public class ComponentCreateException extends RuntimeException { @Override public String getMessage() { - return "Exception while rendering " + this.template.getClass() - + " at line " + this.line + ", column " + this.column + "."; + return "Exception in " + this.template.getClass() + " while creating " + this.componentType.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 index 21080ad..99fd2f7 100644 --- a/view-components/src/main/java/groowt/view/component/ComponentFactory.java +++ b/view-components/src/main/java/groowt/view/component/ComponentFactory.java @@ -5,8 +5,23 @@ import groovy.lang.GroovyObject; import java.util.function.Supplier; -public interface ComponentFactory extends GroovyObject { +@FunctionalInterface +public interface ComponentFactory { + /** + * @param closure A closure with the following signature: + *

+ * {@code Object componentType, ComponentContext context, ... -> T } + *

+ * where '{@code ...}' represents any additional parameters (or none). + *

+ * The first two parameters are not optional and must be present + * or else this method will not work. The first parameter may be either + * a {@link String} or a {@link Class}. + * + * @return A factory which will create type {@code T}. + * @param The desired {@link ViewComponent} type. + */ static ComponentFactory ofClosure(Closure closure) { return new ClosureComponentFactory<>(closure); } @@ -15,6 +30,10 @@ public interface ComponentFactory extends GroovyObject return new SupplierComponentFactory<>(supplier); } - T create(Object type, ComponentContext componentContext, Object... args); + T create(String typeName, ComponentContext componentContext, Object... args); + + default T create(Class type, ComponentContext componentContext, Object... args) { + return this.create(type.getName(), componentContext, args); + } } diff --git a/view-components/src/main/java/groowt/view/component/AbstractComponentFactory.java b/view-components/src/main/java/groowt/view/component/ComponentFactoryBase.java similarity index 52% rename from view-components/src/main/java/groowt/view/component/AbstractComponentFactory.java rename to view-components/src/main/java/groowt/view/component/ComponentFactoryBase.java index 5abc9ce..d41aa27 100644 --- a/view-components/src/main/java/groowt/view/component/AbstractComponentFactory.java +++ b/view-components/src/main/java/groowt/view/component/ComponentFactoryBase.java @@ -1,19 +1,53 @@ package groowt.view.component; import groovy.lang.GroovyObjectSupport; +import groovy.lang.MetaClass; import groovy.lang.MetaMethod; import groovy.lang.MissingMethodException; -import java.util.*; +import java.util.HashMap; +import java.util.Map; -public abstract class AbstractComponentFactory extends GroovyObjectSupport +/** + * 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: + *

    + *
  • {@code String | Class, ComponentContext, ... -> T}
  • + *
  • {@code ComponentContext, ... -> T}
  • + *
  • {@code String | Class, ... -> T}
  • + *
  • {@code ... -> }
  • + *
+ * 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 needs 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 The type of the ViewComponent produced by this factory. + */ +public abstract class ComponentFactoryBase extends GroovyObjectSupport implements ComponentFactory { + protected static final String DO_CREATE = "doCreate"; + + protected static MetaMethod findDoCreateMethod(MetaClass metaClass, Class[] types) { + return metaClass.getMetaMethod(DO_CREATE, types); + } + protected final Map[], MetaMethod> cache = new HashMap<>(); protected MetaMethod findDoCreateMethod(Object[] allArgs) { return this.cache.computeIfAbsent(ComponentFactoryUtil.asTypes(allArgs), types -> - ComponentFactoryUtil.findDoCreateMethod(this.getMetaClass(), types) + findDoCreateMethod(this.getMetaClass(), types) ); } @@ -54,15 +88,16 @@ public abstract class AbstractComponentFactory extends return (T) argsOnlyMethod.invoke(this, args); } - throw new MissingMethodException( - ComponentFactoryUtil.DO_CREATE, - this.getClass(), - args - ); + throw new MissingMethodException(DO_CREATE, this.getClass(), args); } @Override - public T create(Object type, ComponentContext componentContext, Object... args) { + public T create(String type, ComponentContext componentContext, Object... args) { + return this.findAndDoCreate(type, componentContext, args); + } + + @Override + public T create(Class type, ComponentContext componentContext, Object... args) { return this.findAndDoCreate(type, componentContext, args); } diff --git a/view-components/src/main/java/groowt/view/component/ComponentFactoryUtil.java b/view-components/src/main/java/groowt/view/component/ComponentFactoryUtil.java index da504e7..7520667 100644 --- a/view-components/src/main/java/groowt/view/component/ComponentFactoryUtil.java +++ b/view-components/src/main/java/groowt/view/component/ComponentFactoryUtil.java @@ -1,7 +1,9 @@ package groowt.view.component; +import groovy.lang.GroovyObject; import groovy.lang.MetaClass; import groovy.lang.MetaMethod; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Arrays; @@ -9,7 +11,6 @@ import java.util.List; public final class ComponentFactoryUtil { - public static final String DO_CREATE = "doCreate"; public static final Class[] EMPTY_CLASSES = new Class[0]; public static Object[] flatten(Object... args) { @@ -18,7 +19,7 @@ public final class ComponentFactoryUtil { } else { final List result = new ArrayList<>(args.length); for (final var arg : args) { - if (arg instanceof Object[] arr) { + if (arg instanceof Object[] arr && arr.length > 0) { result.addAll(Arrays.asList(arr)); } else { result.add(arg); @@ -44,10 +45,6 @@ public final class ComponentFactoryUtil { return result; } - public static MetaMethod findDoCreateMethod(MetaClass metaClass, Class[] types) { - return metaClass.getMetaMethod(DO_CREATE, types); - } - private ComponentFactoryUtil() {} } diff --git a/view-components/src/main/java/groowt/view/component/ComponentRenderException.java b/view-components/src/main/java/groowt/view/component/ComponentRenderException.java index 3810ab7..f03a866 100644 --- a/view-components/src/main/java/groowt/view/component/ComponentRenderException.java +++ b/view-components/src/main/java/groowt/view/component/ComponentRenderException.java @@ -1,5 +1,8 @@ package groowt.view.component; +/** + * An exception which represents an error during rendering of a component. + */ public class ComponentRenderException extends RuntimeException { private static String formatMessage(int line, int column) { diff --git a/view-components/src/main/java/groowt/view/component/ComponentTemplateCreateException.java b/view-components/src/main/java/groowt/view/component/ComponentTemplateCreateException.java index 4226662..eff73b4 100644 --- a/view-components/src/main/java/groowt/view/component/ComponentTemplateCreateException.java +++ b/view-components/src/main/java/groowt/view/component/ComponentTemplateCreateException.java @@ -2,6 +2,9 @@ package groowt.view.component; import java.io.Reader; +/** + * Represents an exception thrown while attempting to instantiate a ComponentTemplate during compilation. + */ public class ComponentTemplateCreateException extends RuntimeException { private final Class forClass; diff --git a/view-components/src/main/java/groowt/view/component/MissingComponentException.java b/view-components/src/main/java/groowt/view/component/MissingComponentException.java index 527567a..e32a0de 100644 --- a/view-components/src/main/java/groowt/view/component/MissingComponentException.java +++ b/view-components/src/main/java/groowt/view/component/MissingComponentException.java @@ -1,5 +1,9 @@ package groowt.view.component; +/** + * An exception which represents that a component type could not be + * found by the {@link ComponentContext}. + */ public abstract class MissingComponentException extends RuntimeException { private final ComponentTemplate template; diff --git a/view-components/src/main/java/groowt/view/component/SupplierComponentFactory.java b/view-components/src/main/java/groowt/view/component/SupplierComponentFactory.java index 534c82c..a975ef8 100644 --- a/view-components/src/main/java/groowt/view/component/SupplierComponentFactory.java +++ b/view-components/src/main/java/groowt/view/component/SupplierComponentFactory.java @@ -2,7 +2,7 @@ package groowt.view.component; import java.util.function.Supplier; -final class SupplierComponentFactory extends AbstractComponentFactory { +final class SupplierComponentFactory extends ComponentFactoryBase { private final Supplier tSupplier; diff --git a/view-components/src/main/java/groowt/view/component/ViewComponent.java b/view-components/src/main/java/groowt/view/component/ViewComponent.java index cfbd7a0..66c8941 100644 --- a/view-components/src/main/java/groowt/view/component/ViewComponent.java +++ b/view-components/src/main/java/groowt/view/component/ViewComponent.java @@ -9,9 +9,24 @@ public interface ViewComponent extends View { return this.getClass().getName(); } - @ApiStatus.Internal + /** + * Note: compiled templates are required to automatically + * call this method after the component is constructed. One + * only needs to use this if doing custom rendering logic + * where the component is not rendered inside a compiled + * template. + */ void setContext(ComponentContext context); + /** + * Note: compiled templates call the + * related {@link #setContext} method after + * the component is instantiated. If one needs access to the + * {@link ComponentContext} in the component constructor, + * ask for it in the constructor parameters. + * + * @return the component context + */ ComponentContext getContext(); } diff --git a/web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy b/web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy index 9f4f34d..9f015c2 100644 --- a/web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy +++ b/web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy @@ -2,7 +2,6 @@ package groowt.view.web.lib import groowt.view.StandardGStringTemplateView import groowt.view.View -import groowt.view.component.AbstractComponentFactory import groowt.view.component.ComponentContext import groowt.view.component.ComponentFactory import groowt.view.web.WebViewChildComponentRenderer @@ -34,10 +33,7 @@ class Echo extends DelegatingWebViewComponent { } @Override - Echo create(Object type, ComponentContext componentContext, Object... args) { - if (!type instanceof String) { - throw new IllegalArgumentException(' can only be used with String types.') - } + Echo create(String type, ComponentContext componentContext, Object... args) { if (args == null || args.length < 1) { throw new IllegalArgumentException( ' must have at least one attribute. ' + @@ -47,6 +43,11 @@ class Echo extends DelegatingWebViewComponent { this.invokeMethod('doCreate', type as String, componentContext, *args) as Echo } + @Override + Echo create(Class type, ComponentContext componentContext, Object... args) { + throw new UnsupportedOperationException(' can only be used with String types.') + } + } String typeName diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java index f7eccc0..7d09c20 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java @@ -341,11 +341,22 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { final Parameter exceptionParam = new Parameter(EXCEPTION, exceptionName); final VariableExpression exceptionVar = new VariableExpression(exceptionName); + final ConstantExpression componentTypeExpression = switch (componentNode) { + case TypedComponentNode typedComponentNode -> switch (typedComponentNode.getArgs().getType()) { + case StringComponentTypeNode stringComponentTypeNode -> + makeStringLiteral(stringComponentTypeNode.getIdentifier()); + case ClassComponentTypeNode classComponentTypeNode -> + makeStringLiteral(classComponentTypeNode.getFullyQualifiedName()); + }; + case FragmentComponentNode ignored -> makeStringLiteral(FRAGMENT_FQN); + }; + final var lineAndColumn = lineAndColumn(componentNode.getTokenRange().getStartPosition()); final ConstructorCallExpression cce = new ConstructorCallExpression( COMPONENT_CREATE, new ArgumentListExpression(List.of( + componentTypeExpression, VariableExpression.THIS_EXPRESSION, lineAndColumn.getV1(), lineAndColumn.getV2(), diff --git a/web-views/src/test/groovy/groowt/view/web/DefaultWebComponentTemplateCompilerTests.java b/web-views/src/test/groovy/groowt/view/web/DefaultWebComponentTemplateCompilerTests.java index 11626c0..e1f0635 100644 --- a/web-views/src/test/groovy/groowt/view/web/DefaultWebComponentTemplateCompilerTests.java +++ b/web-views/src/test/groovy/groowt/view/web/DefaultWebComponentTemplateCompilerTests.java @@ -43,7 +43,7 @@ public class DefaultWebComponentTemplateCompilerTests { } - private static final class GreeterFactory extends AbstractComponentFactory { + private static final class GreeterFactory extends ComponentFactoryBase { public Greeter doCreate(Map attr) { return new Greeter(attr);