Refactored and added documentation for view-components.
This commit is contained in:
parent
ea4c29f1d7
commit
d05b4f4c0f
@ -2,18 +2,52 @@ package groowt.view.component;
|
|||||||
|
|
||||||
import groovy.lang.Closure;
|
import groovy.lang.Closure;
|
||||||
|
|
||||||
final class ClosureComponentFactory<T extends ViewComponent> extends AbstractComponentFactory<T> {
|
import static groowt.view.component.ComponentFactoryUtil.flatten;
|
||||||
|
|
||||||
|
final class ClosureComponentFactory<T extends ViewComponent> extends ComponentFactoryBase<T> {
|
||||||
|
|
||||||
private final Closure<T> closure;
|
private final Closure<T> closure;
|
||||||
|
private final Class<?> firstParamType;
|
||||||
|
|
||||||
public ClosureComponentFactory(Closure<T> closure) {
|
public ClosureComponentFactory(Closure<T> closure) {
|
||||||
this.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
|
@Override
|
||||||
public T create(Object type, ComponentContext componentContext, Object... args) {
|
public T create(String type, ComponentContext componentContext, Object... args) {
|
||||||
final Object[] flattened = ComponentFactoryUtil.flatten(type, componentContext, args);
|
if (this.firstParamType != Object.class && this.firstParamType != String.class) {
|
||||||
return this.closure.call(flattened);
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,20 +10,36 @@ import java.util.function.Predicate;
|
|||||||
|
|
||||||
public interface ComponentContext {
|
public interface ComponentContext {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For use only by compiled templates.
|
||||||
|
*/
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
interface Resolved {
|
interface Resolved {
|
||||||
String getTypeName();
|
String getTypeName();
|
||||||
ComponentFactory<?> getComponentFactory();
|
ComponentFactory<?> getComponentFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For use only by compiled templates.
|
||||||
|
*/
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
Resolved resolve(String component);
|
Resolved resolve(String component);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For use only by compiled templates.
|
||||||
|
*/
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
ViewComponent create(Resolved resolved, Object... args);
|
ViewComponent create(Resolved resolved, Object... args);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For use only by compiled templates.
|
||||||
|
*/
|
||||||
|
@ApiStatus.Internal
|
||||||
void beforeComponentRender(ViewComponent component);
|
void beforeComponentRender(ViewComponent component);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For use only by compiled templates.
|
||||||
|
*/
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
void afterComponentRender(ViewComponent component);
|
void afterComponentRender(ViewComponent component);
|
||||||
|
|
||||||
|
@ -1,13 +1,25 @@
|
|||||||
package groowt.view.component;
|
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 {
|
public class ComponentCreateException extends RuntimeException {
|
||||||
|
|
||||||
|
private final Object componentType;
|
||||||
private final ComponentTemplate template;
|
private final ComponentTemplate template;
|
||||||
private final int line;
|
private final int line;
|
||||||
private final int column;
|
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);
|
super(cause);
|
||||||
|
this.componentType = componentType;
|
||||||
this.template = template;
|
this.template = template;
|
||||||
this.line = line;
|
this.line = line;
|
||||||
this.column = column;
|
this.column = column;
|
||||||
@ -15,8 +27,8 @@ public class ComponentCreateException extends RuntimeException {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getMessage() {
|
public String getMessage() {
|
||||||
return "Exception while rendering " + this.template.getClass()
|
return "Exception in " + this.template.getClass() + " while creating " + this.componentType.getClass() +
|
||||||
+ " at line " + this.line + ", column " + this.column + ".";
|
" at line " + this.line + ", column " + this.column + ".";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,23 @@ import groovy.lang.GroovyObject;
|
|||||||
|
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public interface ComponentFactory<T extends ViewComponent> extends GroovyObject {
|
@FunctionalInterface
|
||||||
|
public interface ComponentFactory<T extends ViewComponent> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param closure A closure with the following signature:
|
||||||
|
* <p>
|
||||||
|
* {@code Object componentType, ComponentContext context, ... -> T }
|
||||||
|
* <p>
|
||||||
|
* where '{@code ...}' represents any additional parameters (or none).
|
||||||
|
* <p>
|
||||||
|
* 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 <T> The desired {@link ViewComponent} type.
|
||||||
|
*/
|
||||||
static <T extends ViewComponent> ComponentFactory<T> ofClosure(Closure<T> closure) {
|
static <T extends ViewComponent> ComponentFactory<T> ofClosure(Closure<T> closure) {
|
||||||
return new ClosureComponentFactory<>(closure);
|
return new ClosureComponentFactory<>(closure);
|
||||||
}
|
}
|
||||||
@ -15,6 +30,10 @@ public interface ComponentFactory<T extends ViewComponent> extends GroovyObject
|
|||||||
return new SupplierComponentFactory<>(supplier);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,53 @@
|
|||||||
package groowt.view.component;
|
package groowt.view.component;
|
||||||
|
|
||||||
import groovy.lang.GroovyObjectSupport;
|
import groovy.lang.GroovyObjectSupport;
|
||||||
|
import groovy.lang.MetaClass;
|
||||||
import groovy.lang.MetaMethod;
|
import groovy.lang.MetaMethod;
|
||||||
import groovy.lang.MissingMethodException;
|
import groovy.lang.MissingMethodException;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public abstract class AbstractComponentFactory<T extends ViewComponent> 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:
|
||||||
|
* <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> {
|
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 final Map<Class<?>[], MetaMethod> cache = new HashMap<>();
|
||||||
|
|
||||||
protected MetaMethod findDoCreateMethod(Object[] allArgs) {
|
protected MetaMethod findDoCreateMethod(Object[] allArgs) {
|
||||||
return this.cache.computeIfAbsent(ComponentFactoryUtil.asTypes(allArgs), types ->
|
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<T extends ViewComponent> extends
|
|||||||
return (T) argsOnlyMethod.invoke(this, args);
|
return (T) argsOnlyMethod.invoke(this, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new MissingMethodException(
|
throw new MissingMethodException(DO_CREATE, this.getClass(), args);
|
||||||
ComponentFactoryUtil.DO_CREATE,
|
|
||||||
this.getClass(),
|
|
||||||
args
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
return this.findAndDoCreate(type, componentContext, args);
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
|||||||
package groowt.view.component;
|
package groowt.view.component;
|
||||||
|
|
||||||
|
import groovy.lang.GroovyObject;
|
||||||
import groovy.lang.MetaClass;
|
import groovy.lang.MetaClass;
|
||||||
import groovy.lang.MetaMethod;
|
import groovy.lang.MetaMethod;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -9,7 +11,6 @@ import java.util.List;
|
|||||||
|
|
||||||
public final class ComponentFactoryUtil {
|
public final class ComponentFactoryUtil {
|
||||||
|
|
||||||
public static final String DO_CREATE = "doCreate";
|
|
||||||
public static final Class<?>[] EMPTY_CLASSES = new Class[0];
|
public static final Class<?>[] EMPTY_CLASSES = new Class[0];
|
||||||
|
|
||||||
public static Object[] flatten(Object... args) {
|
public static Object[] flatten(Object... args) {
|
||||||
@ -18,7 +19,7 @@ public final class ComponentFactoryUtil {
|
|||||||
} else {
|
} else {
|
||||||
final List<Object> result = new ArrayList<>(args.length);
|
final List<Object> result = new ArrayList<>(args.length);
|
||||||
for (final var arg : args) {
|
for (final var arg : args) {
|
||||||
if (arg instanceof Object[] arr) {
|
if (arg instanceof Object[] arr && arr.length > 0) {
|
||||||
result.addAll(Arrays.asList(arr));
|
result.addAll(Arrays.asList(arr));
|
||||||
} else {
|
} else {
|
||||||
result.add(arg);
|
result.add(arg);
|
||||||
@ -44,10 +45,6 @@ public final class ComponentFactoryUtil {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MetaMethod findDoCreateMethod(MetaClass metaClass, Class<?>[] types) {
|
|
||||||
return metaClass.getMetaMethod(DO_CREATE, types);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ComponentFactoryUtil() {}
|
private ComponentFactoryUtil() {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package groowt.view.component;
|
package groowt.view.component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception which represents an error during rendering of a component.
|
||||||
|
*/
|
||||||
public class ComponentRenderException extends RuntimeException {
|
public class ComponentRenderException extends RuntimeException {
|
||||||
|
|
||||||
private static String formatMessage(int line, int column) {
|
private static String formatMessage(int line, int column) {
|
||||||
|
@ -2,6 +2,9 @@ package groowt.view.component;
|
|||||||
|
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an exception thrown while attempting to instantiate a ComponentTemplate during compilation.
|
||||||
|
*/
|
||||||
public class ComponentTemplateCreateException extends RuntimeException {
|
public class ComponentTemplateCreateException extends RuntimeException {
|
||||||
|
|
||||||
private final Class<? extends ViewComponent> forClass;
|
private final Class<? extends ViewComponent> forClass;
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
package groowt.view.component;
|
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 {
|
public abstract class MissingComponentException extends RuntimeException {
|
||||||
|
|
||||||
private final ComponentTemplate template;
|
private final ComponentTemplate template;
|
||||||
|
@ -2,7 +2,7 @@ package groowt.view.component;
|
|||||||
|
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
final class SupplierComponentFactory<T extends ViewComponent> extends AbstractComponentFactory<T> {
|
final class SupplierComponentFactory<T extends ViewComponent> extends ComponentFactoryBase<T> {
|
||||||
|
|
||||||
private final Supplier<T> tSupplier;
|
private final Supplier<T> tSupplier;
|
||||||
|
|
||||||
|
@ -9,9 +9,24 @@ public interface ViewComponent extends View {
|
|||||||
return this.getClass().getName();
|
return this.getClass().getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiStatus.Internal
|
/**
|
||||||
|
* <em>Note:</em> 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);
|
void setContext(ComponentContext context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <em>Note:</em> compiled templates call the
|
||||||
|
* related {@link #setContext} method <strong>after</strong>
|
||||||
|
* 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();
|
ComponentContext getContext();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package groowt.view.web.lib
|
|||||||
|
|
||||||
import groowt.view.StandardGStringTemplateView
|
import groowt.view.StandardGStringTemplateView
|
||||||
import groowt.view.View
|
import groowt.view.View
|
||||||
import groowt.view.component.AbstractComponentFactory
|
|
||||||
import groowt.view.component.ComponentContext
|
import groowt.view.component.ComponentContext
|
||||||
import groowt.view.component.ComponentFactory
|
import groowt.view.component.ComponentFactory
|
||||||
import groowt.view.web.WebViewChildComponentRenderer
|
import groowt.view.web.WebViewChildComponentRenderer
|
||||||
@ -34,10 +33,7 @@ class Echo extends DelegatingWebViewComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
Echo create(Object type, ComponentContext componentContext, Object... args) {
|
Echo create(String type, ComponentContext componentContext, Object... args) {
|
||||||
if (!type instanceof String) {
|
|
||||||
throw new IllegalArgumentException('<Echo> can only be used with String types.')
|
|
||||||
}
|
|
||||||
if (args == null || args.length < 1) {
|
if (args == null || args.length < 1) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
'<Echo> must have at least one attribute. ' +
|
'<Echo> must have at least one attribute. ' +
|
||||||
@ -47,6 +43,11 @@ class Echo extends DelegatingWebViewComponent {
|
|||||||
this.invokeMethod('doCreate', type as String, componentContext, *args) as Echo
|
this.invokeMethod('doCreate', type as String, componentContext, *args) as Echo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Echo create(Class<?> type, ComponentContext componentContext, Object... args) {
|
||||||
|
throw new UnsupportedOperationException('<Echo> can only be used with String types.')
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String typeName
|
String typeName
|
||||||
|
@ -341,11 +341,22 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
|
|||||||
final Parameter exceptionParam = new Parameter(EXCEPTION, exceptionName);
|
final Parameter exceptionParam = new Parameter(EXCEPTION, exceptionName);
|
||||||
final VariableExpression exceptionVar = new VariableExpression(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 var lineAndColumn = lineAndColumn(componentNode.getTokenRange().getStartPosition());
|
||||||
|
|
||||||
final ConstructorCallExpression cce = new ConstructorCallExpression(
|
final ConstructorCallExpression cce = new ConstructorCallExpression(
|
||||||
COMPONENT_CREATE,
|
COMPONENT_CREATE,
|
||||||
new ArgumentListExpression(List.of(
|
new ArgumentListExpression(List.of(
|
||||||
|
componentTypeExpression,
|
||||||
VariableExpression.THIS_EXPRESSION,
|
VariableExpression.THIS_EXPRESSION,
|
||||||
lineAndColumn.getV1(),
|
lineAndColumn.getV1(),
|
||||||
lineAndColumn.getV2(),
|
lineAndColumn.getV2(),
|
||||||
|
@ -43,7 +43,7 @@ public class DefaultWebComponentTemplateCompilerTests {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class GreeterFactory extends AbstractComponentFactory<Greeter> {
|
private static final class GreeterFactory extends ComponentFactoryBase<Greeter> {
|
||||||
|
|
||||||
public Greeter doCreate(Map<String, Object> attr) {
|
public Greeter doCreate(Map<String, Object> attr) {
|
||||||
return new Greeter(attr);
|
return new Greeter(attr);
|
||||||
|
Loading…
Reference in New Issue
Block a user