Refactoring for more flexible class hierarchy.
This commit is contained in:
parent
6af7e4fd82
commit
6788b10068
@ -0,0 +1,49 @@
|
||||
package groowt.view.component;
|
||||
|
||||
import groowt.view.component.TemplateSource.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
|
||||
public abstract class AbstractComponentTemplateCompiler implements ComponentTemplateCompiler {
|
||||
|
||||
protected abstract ComponentTemplate compile(
|
||||
TemplateSource templateSource,
|
||||
Class<? extends ViewComponent> forClass,
|
||||
Reader actualSource
|
||||
);
|
||||
|
||||
@Override
|
||||
public ComponentTemplate compile(Class<? extends ViewComponent> forClass, TemplateSource source) {
|
||||
return switch (source) {
|
||||
case FileSource(File file) -> {
|
||||
try {
|
||||
yield this.compile(source, forClass, new FileReader(file));
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
case StringSource(String rawSource) ->
|
||||
this.compile(source, forClass, new StringReader(rawSource));
|
||||
case InputStreamSource(InputStream inputStream) ->
|
||||
this.compile(source, forClass, new InputStreamReader(inputStream));
|
||||
case URISource(URI uri) -> {
|
||||
try {
|
||||
yield this.compile(source, forClass, new InputStreamReader(uri.toURL().openStream()));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
case URLSource(URL url) -> {
|
||||
try {
|
||||
yield this.compile(source, forClass, new InputStreamReader(url.openStream()));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
case ReaderSource(Reader reader) -> this.compile(source, forClass, reader);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@ -5,9 +5,11 @@ import groovy.lang.Closure;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
public abstract class AbstractViewComponent implements ViewComponent {
|
||||
|
||||
private ComponentContext context;
|
||||
private ComponentTemplate template;
|
||||
|
||||
public AbstractViewComponent() {}
|
||||
@ -24,6 +26,27 @@ public abstract class AbstractViewComponent implements ViewComponent {
|
||||
}
|
||||
}
|
||||
|
||||
protected AbstractViewComponent(TemplateSource source, Function<String, ComponentTemplateCompiler> getCompiler) {
|
||||
final Class<? extends AbstractViewComponent> selfClass = this.getSelfClass();
|
||||
this.template = getCompiler.apply(selfClass.getPackageName()).compile(selfClass, source);
|
||||
}
|
||||
|
||||
protected AbstractViewComponent(TemplateSource source, ComponentTemplateCompiler compiler) {
|
||||
this.template = compiler.compile(this.getSelfClass(), source);
|
||||
}
|
||||
|
||||
protected abstract Class<? extends AbstractViewComponent> getSelfClass();
|
||||
|
||||
@Override
|
||||
public void setContext(ComponentContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComponentContext getContext() {
|
||||
return Objects.requireNonNull(this.context);
|
||||
}
|
||||
|
||||
protected ComponentTemplate getTemplate() {
|
||||
return Objects.requireNonNull(template);
|
||||
}
|
||||
@ -40,6 +63,11 @@ public abstract class AbstractViewComponent implements ViewComponent {
|
||||
this.getContext().afterComponentRender(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @implSpec If overriding, <strong>please</strong> call
|
||||
* {@link #beforeRender()}and {@link #afterRender()} before
|
||||
* and after the actual rendering is done, respectively.
|
||||
*/
|
||||
@Override
|
||||
public void renderTo(Writer out) throws IOException {
|
||||
final Closure<?> closure = this.template.getRenderer();
|
||||
|
@ -1,27 +1,26 @@
|
||||
package groowt.view.component;
|
||||
|
||||
import java.io.Reader;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public abstract class CachingComponentTemplateCompiler implements ComponentTemplateCompiler {
|
||||
public abstract class CachingComponentTemplateCompiler extends AbstractComponentTemplateCompiler {
|
||||
|
||||
private final Map<Class<? extends ViewComponent>, ComponentTemplate> cache = new HashMap<>();
|
||||
|
||||
protected final void putInCache(Class<? extends ViewComponent> forClass, ComponentTemplate template) {
|
||||
this.cache.put(forClass, template);
|
||||
}
|
||||
|
||||
protected final ComponentTemplate getFromCache(Class<? extends ViewComponent> forClass) {
|
||||
return Objects.requireNonNull(this.cache.get(forClass));
|
||||
}
|
||||
|
||||
protected final ComponentTemplate getFromCacheOrElse(
|
||||
@Override
|
||||
protected final ComponentTemplate compile(
|
||||
TemplateSource source,
|
||||
Class<? extends ViewComponent> forClass,
|
||||
Supplier<? extends ComponentTemplate> onEmpty
|
||||
Reader sourceReader
|
||||
) {
|
||||
return this.cache.computeIfAbsent(forClass, ignored -> onEmpty.get());
|
||||
return this.cache.computeIfAbsent(forClass, ignored -> this.doCompile(source, forClass, sourceReader));
|
||||
}
|
||||
|
||||
protected abstract ComponentTemplate doCompile(
|
||||
TemplateSource source,
|
||||
Class<? extends ViewComponent> forClass,
|
||||
Reader sourceReader
|
||||
);
|
||||
|
||||
}
|
||||
|
@ -4,50 +4,79 @@ import groovy.lang.Closure;
|
||||
|
||||
import static groowt.view.component.ComponentFactoryUtil.flatten;
|
||||
|
||||
final class ClosureComponentFactory<T extends ViewComponent> extends ComponentFactoryBase<T> {
|
||||
final class ClosureComponentFactory<T extends ViewComponent> implements ComponentFactory<T> {
|
||||
|
||||
private enum Type {
|
||||
ALL,
|
||||
NAME_AND_CONTEXT, NAME_AND_ARGS, CONTEXT_AND_ARGS,
|
||||
NAME_ONLY, CONTEXT_ONLY, ARGS_ONLY,
|
||||
NONE
|
||||
}
|
||||
|
||||
private final Closure<T> closure;
|
||||
private final Class<?> firstParamType;
|
||||
private final Type type;
|
||||
|
||||
public ClosureComponentFactory(Closure<T> 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."
|
||||
);
|
||||
@SuppressWarnings("unchecked")
|
||||
public ClosureComponentFactory(Closure<? extends T> closure) {
|
||||
this.closure = (Closure<T>) closure;
|
||||
final var paramTypes = this.closure.getParameterTypes();
|
||||
if (paramTypes.length == 0) {
|
||||
this.type = Type.NONE;
|
||||
} else if (paramTypes.length == 1) {
|
||||
final var paramType = paramTypes[0];
|
||||
if (paramType == String.class || paramType == Class.class) {
|
||||
this.type = Type.NAME_ONLY;
|
||||
} else if (ComponentContext.class.isAssignableFrom(paramType)) {
|
||||
this.type = Type.CONTEXT_ONLY;
|
||||
} else {
|
||||
this.type = Type.ARGS_ONLY;
|
||||
}
|
||||
} else {
|
||||
final var firstParamType = paramTypes[0];
|
||||
final var secondParamType = paramTypes[1];
|
||||
if (firstParamType == String.class || firstParamType == Class.class) {
|
||||
if (ComponentContext.class.isAssignableFrom(secondParamType)) {
|
||||
if (paramTypes.length > 2) {
|
||||
this.type = Type.ALL;
|
||||
} else {
|
||||
this.type = Type.NAME_AND_CONTEXT;
|
||||
}
|
||||
} else {
|
||||
this.type = Type.NAME_AND_ARGS;
|
||||
}
|
||||
} else if (ComponentContext.class.isAssignableFrom(firstParamType)) {
|
||||
this.type = Type.CONTEXT_AND_ARGS;
|
||||
} else {
|
||||
this.type = Type.ARGS_ONLY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private T flatCall(Object... args) {
|
||||
return this.closure.call(flatten(args));
|
||||
}
|
||||
|
||||
private T objTypeCreate(Object type, ComponentContext componentContext, Object... args) {
|
||||
return switch (this.type) {
|
||||
case ALL -> this.flatCall(type, componentContext, args);
|
||||
case NAME_AND_CONTEXT -> this.closure.call(type, componentContext);
|
||||
case NAME_AND_ARGS -> this.flatCall(type, args);
|
||||
case CONTEXT_AND_ARGS -> this.flatCall(componentContext, args);
|
||||
case NAME_ONLY -> this.closure.call(type);
|
||||
case CONTEXT_ONLY -> this.closure.call(componentContext);
|
||||
case ARGS_ONLY -> this.closure.call(args);
|
||||
case NONE -> this.closure.call();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public T create(String type, ComponentContext componentContext, Object... args) {
|
||||
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));
|
||||
return this.objTypeCreate(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));
|
||||
return this.objTypeCreate(type, componentContext, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,8 +27,9 @@ public class ComponentCreateException extends RuntimeException {
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "Exception in " + this.template.getClass() + " while creating " + this.componentType.getClass() +
|
||||
" at line " + this.line + ", column " + this.column + ".";
|
||||
return "Exception in " + this.template.getClass().getName() + " while creating "
|
||||
+ this.componentType.getClass().getName() + " at line " + this.line
|
||||
+ ", column " + this.column + ".";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,28 +1,13 @@
|
||||
package groowt.view.component;
|
||||
|
||||
import groovy.lang.Closure;
|
||||
import groovy.lang.GroovyObject;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@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<? extends T> closure) {
|
||||
return new ClosureComponentFactory<>(closure);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
package groowt.view.component;
|
||||
|
||||
import groovy.lang.Closure;
|
||||
|
||||
public interface ComponentScope {
|
||||
|
||||
void add(String name, ComponentFactory<?> factory);
|
||||
@ -24,11 +26,6 @@ public interface ComponentScope {
|
||||
return (ComponentFactory<T>) this.get(clazz.getName());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
default <T extends ViewComponent> ComponentFactory<T> getAs(String name, Class<T> viewComponentType) {
|
||||
return (ComponentFactory<T>) this.get(name);
|
||||
}
|
||||
|
||||
default void remove(Class<? extends ViewComponent> clazz) {
|
||||
this.remove(clazz.getName());
|
||||
}
|
||||
@ -38,4 +35,12 @@ public interface ComponentScope {
|
||||
return (ComponentFactory<T>) this.factoryMissing(clazz.getName());
|
||||
}
|
||||
|
||||
default void add(String name, Closure<? extends ViewComponent> closure) {
|
||||
this.add(name, ComponentFactory.ofClosure(closure));
|
||||
}
|
||||
|
||||
default <T extends ViewComponent> void add(Class<T> type, Closure<? extends T> closure) {
|
||||
this.add(type, ComponentFactory.ofClosure(closure));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,16 +1,5 @@
|
||||
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<? extends ViewComponent> forClass, File templateFile);
|
||||
ComponentTemplate compile(Class<? extends ViewComponent> forClass, String template);
|
||||
ComponentTemplate compile(Class<? extends ViewComponent> forClass, URI templateURI);
|
||||
ComponentTemplate compile(Class<? extends ViewComponent> forClass, URL templateURL);
|
||||
ComponentTemplate compile(Class<? extends ViewComponent> forClass, InputStream inputStream);
|
||||
ComponentTemplate compile(Class<? extends ViewComponent> forClass, Reader reader);
|
||||
ComponentTemplate compile(Class<? extends ViewComponent> forClass, TemplateSource source);
|
||||
}
|
||||
|
@ -0,0 +1,55 @@
|
||||
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 sealed interface TemplateSource {
|
||||
|
||||
static TemplateSource of(String template) {
|
||||
return new StringSource(template);
|
||||
}
|
||||
|
||||
static TemplateSource of(File templateFile) {
|
||||
return new FileSource(templateFile);
|
||||
}
|
||||
|
||||
static TemplateSource of(URI templateURI) {
|
||||
return new URISource(templateURI);
|
||||
}
|
||||
|
||||
static TemplateSource of(URL url) {
|
||||
return new URLSource(url);
|
||||
}
|
||||
|
||||
static TemplateSource of(InputStream templateInputStream) {
|
||||
return new InputStreamSource(templateInputStream);
|
||||
}
|
||||
|
||||
static TemplateSource of(Reader templateReader) {
|
||||
return new ReaderSource(templateReader);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resourceName An <strong>absolute</strong> path resource name.
|
||||
* @return A template source
|
||||
*/
|
||||
static TemplateSource fromResource(String resourceName) {
|
||||
return of(TemplateSource.class.getClassLoader().getResource(resourceName));
|
||||
}
|
||||
|
||||
record StringSource(String template) implements TemplateSource {}
|
||||
|
||||
record FileSource(File templateFile) implements TemplateSource {}
|
||||
|
||||
record URISource(URI templateURI) implements TemplateSource {}
|
||||
|
||||
record URLSource(URL templateURL) implements TemplateSource {}
|
||||
|
||||
record InputStreamSource(InputStream templateInputStream) implements TemplateSource {}
|
||||
|
||||
record ReaderSource(Reader templateReader) implements TemplateSource {}
|
||||
|
||||
}
|
@ -7,6 +7,7 @@ import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface View {
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,46 @@
|
||||
package groowt.view.web
|
||||
|
||||
import groowt.view.component.AbstractViewComponent
|
||||
import groowt.view.component.ComponentTemplate
|
||||
import groowt.view.component.TemplateSource
|
||||
|
||||
class DefaultWebViewComponent extends AbstractWebViewComponent {
|
||||
|
||||
DefaultWebViewComponent() {}
|
||||
|
||||
DefaultWebViewComponent(ComponentTemplate template) {
|
||||
super(template)
|
||||
}
|
||||
|
||||
DefaultWebViewComponent(Class<? extends ComponentTemplate> templateType) {
|
||||
super(templateType)
|
||||
}
|
||||
|
||||
DefaultWebViewComponent(TemplateSource source) {
|
||||
super(source)
|
||||
}
|
||||
|
||||
DefaultWebViewComponent(TemplateSource source, WebViewComponentTemplateCompiler compiler) {
|
||||
super(source, compiler)
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience constructor which creates a {@link TemplateSource}
|
||||
* from the given {@code source} parameter and passes it to super. See
|
||||
* {@link TemplateSource} for possible types.
|
||||
*
|
||||
* @param source the object passed to {@link TemplateSource#of}
|
||||
*
|
||||
* @see TemplateSource
|
||||
*/
|
||||
@SuppressWarnings('GroovyAssignabilityCheck')
|
||||
DefaultWebViewComponent(Object source) {
|
||||
super(TemplateSource.of(source))
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final Class<? extends AbstractViewComponent> getSelfClass() {
|
||||
this.class
|
||||
}
|
||||
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
package groowt.view.web;
|
||||
|
||||
import groovy.lang.Closure;
|
||||
import groovy.lang.GroovyClassLoader;
|
||||
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 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<? extends DefaultWebViewComponent> 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<WebViewChildRenderer> children;
|
||||
|
||||
public DefaultWebViewComponent() {}
|
||||
|
||||
public DefaultWebViewComponent(ComponentTemplate template) {
|
||||
super(template);
|
||||
}
|
||||
|
||||
public DefaultWebViewComponent(Class<? extends ComponentTemplate> 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<WebViewChildRenderer> getChildRenderers() {
|
||||
if (this.children == null) {
|
||||
this.children = new ArrayList<>();
|
||||
}
|
||||
return this.children;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasChildren() {
|
||||
return !this.getChildRenderers().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChildRenderers(List<WebViewChildRenderer> children) {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderChildren() {
|
||||
for (final var childRenderer : this.getChildRenderers()) {
|
||||
try {
|
||||
if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) {
|
||||
this.getContext().beforeComponentRender(childComponentRenderer.getComponent());
|
||||
}
|
||||
childRenderer.render(this);
|
||||
} catch (Exception e) {
|
||||
throw new ChildRenderException(e);
|
||||
} finally {
|
||||
if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) {
|
||||
this.getContext().afterComponentRender(childComponentRenderer.getComponent());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderTo(Writer out) throws IOException {
|
||||
final var webWriter = new WebViewComponentWriter(out);
|
||||
final Closure<?> renderer = this.getTemplate().getRenderer();
|
||||
renderer.setDelegate(this);
|
||||
renderer.setResolveStrategy(Closure.DELEGATE_FIRST);
|
||||
renderer.call(this.getContext(), webWriter);
|
||||
}
|
||||
|
||||
}
|
@ -4,22 +4,23 @@ import groowt.view.component.ComponentScope
|
||||
import groowt.view.component.DefaultComponentContext
|
||||
import groowt.view.component.ViewComponent
|
||||
import groowt.view.web.lib.Fragment
|
||||
import groowt.view.web.runtime.WebViewComponentChildCollector
|
||||
import groowt.view.web.runtime.DefaultWebViewComponentChildCollection
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
class DefaultWebViewComponentContext extends DefaultComponentContext {
|
||||
class DefaultWebViewComponentContext extends DefaultComponentContext implements WebViewComponentContext {
|
||||
|
||||
@Override
|
||||
protected ComponentScope getNewDefaultScope() {
|
||||
new WebViewScope()
|
||||
}
|
||||
|
||||
@Override
|
||||
@ApiStatus.Internal
|
||||
ViewComponent createFragment(Closure<?> childCollector) {
|
||||
def collector = new WebViewComponentChildCollector()
|
||||
childCollector.call(collector)
|
||||
def childCollection = new DefaultWebViewComponentChildCollection()
|
||||
childCollector.call(childCollection)
|
||||
def fragment = new Fragment()
|
||||
fragment.childRenderers = collector.children
|
||||
fragment.childRenderers = childCollection.children
|
||||
fragment
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,24 @@
|
||||
package groowt.view.web
|
||||
|
||||
import groovy.transform.stc.ClosureParams
|
||||
import groovy.transform.stc.FromString
|
||||
import groowt.view.component.ComponentFactory
|
||||
|
||||
import java.util.function.Function
|
||||
|
||||
final class WebViewComponentFactories {
|
||||
|
||||
static <T extends WebViewComponent> ComponentFactory<T> withAttr(
|
||||
@ClosureParams(value = FromString, options = 'java.util.Map<String, Object>')
|
||||
Closure<T> closure
|
||||
) {
|
||||
ComponentFactory.ofClosure { Map<String, Object> attr -> closure(attr) }
|
||||
}
|
||||
|
||||
static <T extends WebViewComponent> ComponentFactory<T> withAttr(Function<Map<String, Object>, T> tFunction) {
|
||||
ComponentFactory.ofClosure { Map<String, Object> attr -> tFunction.apply(attr) }
|
||||
}
|
||||
|
||||
private WebViewComponentFactories() {}
|
||||
|
||||
}
|
@ -2,15 +2,14 @@ package groowt.view.web
|
||||
|
||||
import groowt.view.component.ComponentFactory
|
||||
import groowt.view.component.DefaultComponentScope
|
||||
import groowt.view.web.lib.Echo
|
||||
import groowt.view.web.lib.Echo.EchoFactory
|
||||
|
||||
class WebViewScope extends DefaultComponentScope {
|
||||
|
||||
private final EchoFactory echoFactory = new EchoFactory()
|
||||
|
||||
@Override
|
||||
ComponentFactory factoryMissing(String typeName) {
|
||||
echoFactory
|
||||
Echo.FACTORY
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,8 +22,10 @@ public abstract class DelegatingWebViewComponent extends DefaultWebViewComponent
|
||||
protected abstract View getDelegate();
|
||||
|
||||
@Override
|
||||
public void renderTo(Writer out) throws IOException {
|
||||
public final void renderTo(Writer out) throws IOException {
|
||||
this.beforeRender();
|
||||
this.getDelegate().renderTo(out);
|
||||
this.afterRender();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,14 +1,20 @@
|
||||
package groowt.view.web.lib
|
||||
|
||||
import groowt.view.StandardGStringTemplateView
|
||||
import groowt.view.View
|
||||
import groowt.view.component.ComponentContext
|
||||
import groowt.view.component.ComponentFactory
|
||||
import groowt.view.component.ComponentRenderException
|
||||
import groowt.view.web.WebViewChildComponentRenderer
|
||||
|
||||
class Echo extends DelegatingWebViewComponent {
|
||||
|
||||
static final class EchoFactory implements ComponentFactory<Echo> {
|
||||
static final ComponentFactory<Echo> FACTORY = new EchoFactory()
|
||||
|
||||
private static final class EchoFactory implements ComponentFactory<Echo> {
|
||||
|
||||
Echo doCreate(String typeName) {
|
||||
doCreate(typeName, [:], true)
|
||||
}
|
||||
|
||||
Echo doCreate(String typeName, boolean selfClose) {
|
||||
doCreate(typeName, [:], selfClose)
|
||||
@ -19,8 +25,7 @@ class Echo extends DelegatingWebViewComponent {
|
||||
}
|
||||
|
||||
Echo doCreate(String typeName, Map<String, Object> attr, boolean selfClose) {
|
||||
def echo = new Echo(attr, typeName, selfClose)
|
||||
echo
|
||||
new Echo(attr, typeName, selfClose)
|
||||
}
|
||||
|
||||
Echo doCreate(
|
||||
@ -56,26 +61,43 @@ class Echo extends DelegatingWebViewComponent {
|
||||
|
||||
@Override
|
||||
protected View getDelegate() {
|
||||
return new StandardGStringTemplateView(
|
||||
src: Echo.getResource('EchoTemplate.gst'),
|
||||
parent: this
|
||||
)
|
||||
if (this.selfClose && this.hasChildren()) {
|
||||
throw new ComponentRenderException('Cannot have selfClose set to true and have children.')
|
||||
}
|
||||
return {
|
||||
it << '<'
|
||||
it << this.name
|
||||
if (!this.attr.isEmpty()) {
|
||||
it << ' '
|
||||
formatAttr(it)
|
||||
}
|
||||
if (this.selfClose) {
|
||||
it << ' /'
|
||||
}
|
||||
it << '>'
|
||||
if (this.hasChildren()) {
|
||||
this.renderChildren() // TODO: fix this
|
||||
}
|
||||
if (this.hasChildren() || !this.selfClose) {
|
||||
it << '</'
|
||||
it << this.name
|
||||
it << '>'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String formatAttr() {
|
||||
def sb = new StringBuilder()
|
||||
protected void formatAttr(Writer writer) {
|
||||
def iter = this.attr.iterator()
|
||||
while (iter.hasNext()) {
|
||||
def entry = iter.next()
|
||||
sb << entry.key
|
||||
sb << '="'
|
||||
sb << entry.value
|
||||
sb << '"'
|
||||
writer << entry.key
|
||||
writer << '="'
|
||||
writer << entry.value
|
||||
writer << '"'
|
||||
if (iter.hasNext()) {
|
||||
sb << ' '
|
||||
writer << ' '
|
||||
}
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ package groowt.view.web.lib
|
||||
|
||||
import groowt.view.web.DefaultWebViewComponent
|
||||
|
||||
class Fragment extends DefaultWebViewComponent {
|
||||
final class Fragment extends DefaultWebViewComponent {
|
||||
|
||||
@Override
|
||||
void renderTo(Writer out) throws IOException {
|
||||
|
@ -0,0 +1,88 @@
|
||||
package groowt.view.web;
|
||||
|
||||
import groovy.lang.Closure;
|
||||
import groowt.view.component.AbstractViewComponent;
|
||||
import groowt.view.component.ComponentTemplate;
|
||||
import groowt.view.component.TemplateSource;
|
||||
import groowt.view.web.runtime.DefaultWebViewComponentWriter;
|
||||
import groowt.view.web.runtime.WebViewComponentWriter;
|
||||
import org.codehaus.groovy.control.CompilerConfiguration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class AbstractWebViewComponent extends AbstractViewComponent implements WebViewComponent {
|
||||
|
||||
private List<WebViewChildRenderer> childRenderers;
|
||||
|
||||
public AbstractWebViewComponent() {}
|
||||
|
||||
public AbstractWebViewComponent(ComponentTemplate template) {
|
||||
super(template);
|
||||
}
|
||||
|
||||
public AbstractWebViewComponent(Class<? extends ComponentTemplate> templateClass) {
|
||||
super(templateClass);
|
||||
}
|
||||
|
||||
protected AbstractWebViewComponent(TemplateSource source) {
|
||||
super(source, packageName -> new DefaultWebViewComponentTemplateCompiler(
|
||||
CompilerConfiguration.DEFAULT,
|
||||
packageName
|
||||
));
|
||||
}
|
||||
|
||||
protected AbstractWebViewComponent(TemplateSource source, WebViewComponentTemplateCompiler compiler) {
|
||||
super(source, compiler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WebViewChildRenderer> getChildRenderers() {
|
||||
if (this.childRenderers == null) {
|
||||
this.childRenderers = new ArrayList<>();
|
||||
}
|
||||
return this.childRenderers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasChildren() {
|
||||
return !this.getChildRenderers().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChildRenderers(List<WebViewChildRenderer> children) {
|
||||
this.childRenderers = children;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderChildren() {
|
||||
for (final var childRenderer : this.getChildRenderers()) {
|
||||
try {
|
||||
if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) {
|
||||
this.getContext().beforeComponentRender(childComponentRenderer.getComponent());
|
||||
}
|
||||
childRenderer.render(this);
|
||||
} catch (Exception e) {
|
||||
throw new ChildRenderException(e);
|
||||
} finally {
|
||||
if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) {
|
||||
this.getContext().afterComponentRender(childComponentRenderer.getComponent());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderTo(Writer out) throws IOException {
|
||||
final WebViewComponentWriter webWriter = new DefaultWebViewComponentWriter(out);
|
||||
final Closure<?> renderer = this.getTemplate().getRenderer();
|
||||
renderer.setDelegate(this);
|
||||
renderer.setResolveStrategy(Closure.DELEGATE_FIRST);
|
||||
this.beforeRender();
|
||||
renderer.call(this.getContext(), webWriter);
|
||||
this.afterRender();
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
package groowt.view.web;
|
||||
|
||||
import groovy.lang.GroovyClassLoader;
|
||||
import groowt.view.component.CachingComponentTemplateCompiler;
|
||||
import groowt.view.component.ComponentTemplate;
|
||||
import groowt.view.component.ComponentTemplateCreateException;
|
||||
import groowt.view.component.ViewComponent;
|
||||
import groowt.view.component.*;
|
||||
import groowt.view.web.antlr.CompilationUnitParseResult;
|
||||
import groowt.view.web.antlr.ParserUtil;
|
||||
import groowt.view.web.antlr.TokenList;
|
||||
@ -21,15 +18,14 @@ import org.codehaus.groovy.tools.GroovyClass;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
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.net.URISyntaxException;
|
||||
import java.util.Objects;
|
||||
|
||||
public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplateCompiler {
|
||||
public class DefaultWebViewComponentTemplateCompiler extends CachingComponentTemplateCompiler
|
||||
implements WebViewComponentTemplateCompiler {
|
||||
|
||||
private final CompilerConfiguration configuration;
|
||||
private final String defaultPackageName;
|
||||
@ -37,15 +33,12 @@ public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplat
|
||||
|
||||
private GroovyClassLoader groovyClassLoader;
|
||||
|
||||
public DefaultWebComponentTemplateCompiler(
|
||||
CompilerConfiguration configuration,
|
||||
String defaultPackageName
|
||||
) {
|
||||
public DefaultWebViewComponentTemplateCompiler(CompilerConfiguration configuration, String defaultPackageName) {
|
||||
this(configuration, defaultPackageName, Phases.CLASS_GENERATION);
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public DefaultWebComponentTemplateCompiler(
|
||||
public DefaultWebViewComponentTemplateCompiler(
|
||||
CompilerConfiguration configuration,
|
||||
String defaultPackageName,
|
||||
int phase
|
||||
@ -70,11 +63,30 @@ public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplat
|
||||
this.groovyClassLoader = null;
|
||||
}
|
||||
|
||||
protected ComponentTemplate doCompile(@Nullable Class<? extends ViewComponent> forClass, Reader reader) {
|
||||
return this.doCompile(forClass, reader, null);
|
||||
@Override
|
||||
protected ComponentTemplate doCompile(
|
||||
@Nullable TemplateSource source,
|
||||
@Nullable Class<? extends ViewComponent> forClass,
|
||||
Reader sourceReader
|
||||
) {
|
||||
if (source instanceof TemplateSource.URISource uriSource) {
|
||||
return this.doCompile(forClass, sourceReader, uriSource.templateURI());
|
||||
} else if (source instanceof TemplateSource.URLSource urlSource) {
|
||||
try {
|
||||
return this.doCompile(forClass, sourceReader, urlSource.templateURL().toURI());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
return this.doCompile(forClass, sourceReader, null);
|
||||
}
|
||||
}
|
||||
|
||||
protected ComponentTemplate doCompile(@Nullable Class<? extends ViewComponent> forClass, Reader reader, @Nullable URI uri) {
|
||||
protected ComponentTemplate doCompile(
|
||||
@Nullable Class<? extends ViewComponent> forClass,
|
||||
Reader reader,
|
||||
@Nullable URI uri
|
||||
) {
|
||||
final CompilationUnitParseResult parseResult = ParserUtil.parseCompilationUnit(reader);
|
||||
|
||||
// TODO: analysis
|
||||
@ -153,56 +165,8 @@ public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplat
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComponentTemplate compile(Class<? extends ViewComponent> 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<? extends ViewComponent> forClass, String template) {
|
||||
return this.getFromCacheOrElse(forClass, () -> this.doCompile(forClass, new StringReader(template)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComponentTemplate compile(Class<? extends ViewComponent> 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<? extends ViewComponent> 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<? extends ViewComponent> forClass, InputStream inputStream) {
|
||||
return this.getFromCacheOrElse(forClass, () -> this.doCompile(forClass, new InputStreamReader(inputStream)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComponentTemplate compile(Class<? extends ViewComponent> forClass, Reader reader) {
|
||||
return this.getFromCacheOrElse(forClass, () -> this.doCompile(forClass, reader));
|
||||
}
|
||||
|
||||
public ComponentTemplate compileAnonymous(Reader reader) {
|
||||
return this.doCompile(null, reader);
|
||||
return this.doCompile(null, null, reader);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package groowt.view.web;
|
||||
|
||||
import groovy.lang.Closure;
|
||||
import groowt.view.component.ComponentContext;
|
||||
import groowt.view.component.ViewComponent;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
public interface WebViewComponentContext extends ComponentContext {
|
||||
|
||||
/**
|
||||
* For use only by compiled web view component templates.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
ViewComponent createFragment(Closure<?> childCollector);
|
||||
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package groowt.view.web;
|
||||
|
||||
import groowt.view.component.ComponentTemplate;
|
||||
import groowt.view.component.ComponentTemplateCompiler;
|
||||
|
||||
import java.io.Reader;
|
||||
|
||||
public interface WebViewComponentTemplateCompiler extends ComponentTemplateCompiler {
|
||||
ComponentTemplate compileAnonymous(Reader reader);
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
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 <strong>absolute</strong> 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 {}
|
||||
|
||||
}
|
@ -11,22 +11,26 @@ import groowt.view.web.WebViewChildRenderer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class WebViewComponentChildCollector {
|
||||
public class DefaultWebViewComponentChildCollection implements WebViewComponentChildCollection {
|
||||
|
||||
private final List<WebViewChildRenderer> children = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void add(String jString, Closure<Void> renderer) {
|
||||
this.children.add(new WebViewChildJStringRenderer(jString, renderer));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(GString gString, Closure<Void> renderer) {
|
||||
this.children.add(new WebViewChildGStringRenderer(gString, renderer));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(ViewComponent component, Closure<Void> renderer) {
|
||||
this.children.add(new WebViewChildComponentRenderer(component, renderer));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WebViewChildRenderer> getChildren() {
|
||||
return this.children;
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
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 DefaultWebViewComponentWriter implements WebViewComponentWriter {
|
||||
|
||||
private final Writer delegate;
|
||||
|
||||
public DefaultWebViewComponentWriter(Writer delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void append(String string) {
|
||||
try {
|
||||
this.delegate.append(string);
|
||||
} catch (IOException ioException) {
|
||||
throw new RuntimeException(ioException);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void append(GString gString) {
|
||||
try {
|
||||
gString.writeTo(this.delegate);
|
||||
} catch (IOException ioException) {
|
||||
throw new RuntimeException(ioException);
|
||||
} catch (Exception exception) {
|
||||
throw new ComponentRenderException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void append(Object object) {
|
||||
try {
|
||||
this.delegate.append(object.toString());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void leftShift(Object object) {
|
||||
switch (object) {
|
||||
case String s -> this.append(s);
|
||||
case GString gs -> this.append(gs);
|
||||
case ViewComponent viewComponent -> this.append(viewComponent);
|
||||
default -> this.append(object);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package groowt.view.web.runtime;
|
||||
|
||||
import groovy.lang.Closure;
|
||||
import groovy.lang.GString;
|
||||
import groowt.view.component.ViewComponent;
|
||||
import groowt.view.web.WebViewChildRenderer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface WebViewComponentChildCollection {
|
||||
|
||||
void add(String jString, Closure<Void> renderer);
|
||||
|
||||
void add(GString gString, Closure<Void> renderer);
|
||||
|
||||
void add(ViewComponent component, Closure<Void> renderer);
|
||||
|
||||
List<WebViewChildRenderer> getChildren();
|
||||
|
||||
}
|
@ -1,87 +1,14 @@
|
||||
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) {
|
||||
try {
|
||||
gString.writeTo(this.delegate);
|
||||
} catch (IOException ioException) {
|
||||
throw new RuntimeException(ioException);
|
||||
} catch (Exception exception) {
|
||||
throw new ComponentRenderException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public void leftShift(Object object) {
|
||||
switch (object) {
|
||||
case String s -> this.append(s);
|
||||
case GString gs -> this.append(gs);
|
||||
case ViewComponent viewComponent -> this.append(viewComponent);
|
||||
default -> this.append(object);
|
||||
}
|
||||
}
|
||||
|
||||
public interface WebViewComponentWriter {
|
||||
void append(String string);
|
||||
void append(GString gString);
|
||||
void append(GString gString, int line, int column);
|
||||
void append(ViewComponent viewComponent);
|
||||
void append(ViewComponent viewComponent, int line, int column);
|
||||
void append(Object object);
|
||||
void leftShift(Object object);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package groowt.view.web.transpile;
|
||||
import groovy.lang.Tuple2;
|
||||
import groowt.view.component.*;
|
||||
import groowt.view.web.ast.node.*;
|
||||
import groowt.view.web.runtime.WebViewComponentChildCollector;
|
||||
import groowt.view.web.runtime.WebViewComponentChildCollection;
|
||||
import groowt.view.web.transpile.util.GroovyUtil;
|
||||
import groowt.view.web.transpile.util.GroovyUtil.ConvertResult;
|
||||
import org.codehaus.groovy.ast.*;
|
||||
@ -22,7 +22,7 @@ import static groowt.view.web.transpile.TranspilerUtil.*;
|
||||
public class DefaultComponentTranspiler implements ComponentTranspiler {
|
||||
|
||||
private static final ClassNode VIEW_COMPONENT = ClassHelper.make(ViewComponent.class);
|
||||
private static final ClassNode CHILD_COLLECTOR = ClassHelper.make(WebViewComponentChildCollector.class);
|
||||
private static final ClassNode CHILD_COLLECTION = ClassHelper.make(WebViewComponentChildCollection.class);
|
||||
|
||||
private static final ClassNode EXCEPTION = ClassHelper.make(Exception.class);
|
||||
private static final ClassNode COMPONENT_CREATE = ClassHelper.make(ComponentCreateException.class);
|
||||
@ -182,7 +182,7 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
|
||||
String componentVariableName
|
||||
) {
|
||||
final Parameter childCollectorParam = new Parameter(
|
||||
CHILD_COLLECTOR,
|
||||
CHILD_COLLECTION,
|
||||
componentVariableName + "_childCollector"
|
||||
);
|
||||
|
||||
|
@ -0,0 +1 @@
|
||||
<$name
|
@ -1,98 +0,0 @@
|
||||
package groowt.view.web;
|
||||
|
||||
import groovy.lang.Closure;
|
||||
import groowt.view.component.*;
|
||||
import groowt.view.web.runtime.WebViewComponentWriter;
|
||||
import org.codehaus.groovy.control.CompilerConfiguration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class DefaultWebComponentTemplateCompilerTests {
|
||||
|
||||
private static ComponentTemplate doCompile(Class<? extends ViewComponent> componentClass, Reader source) {
|
||||
final var compiler = new DefaultWebComponentTemplateCompiler(
|
||||
CompilerConfiguration.DEFAULT,
|
||||
componentClass.getPackageName()
|
||||
);
|
||||
return compiler.compile(componentClass, source);
|
||||
}
|
||||
|
||||
private static ComponentTemplate doCompile(Class<? extends ViewComponent> componentClass, String source) {
|
||||
return doCompile(componentClass, new StringReader(source));
|
||||
}
|
||||
|
||||
private static final class Greeter extends DefaultWebViewComponent {
|
||||
|
||||
private final String target;
|
||||
|
||||
public Greeter(Map<String, Object> 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 ComponentFactoryBase<Greeter> {
|
||||
|
||||
public Greeter doCreate(Map<String, Object> attr) {
|
||||
return new Greeter(attr);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class UsingGreeter extends DefaultWebViewComponent {
|
||||
|
||||
public UsingGreeter(ComponentContext context) {
|
||||
super(doCompile(UsingGreeter.class, "<Greeter target='World' />"));
|
||||
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());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withPreambleImport() {
|
||||
final ComponentTemplate template = doCompile(DefaultWebViewComponent.class,
|
||||
"""
|
||||
---
|
||||
import groovy.transform.Field
|
||||
|
||||
@Field
|
||||
String greeting = 'Hello, World!'
|
||||
---
|
||||
$greeting
|
||||
""".stripIndent()
|
||||
);
|
||||
final var context = new DefaultComponentContext();
|
||||
context.pushDefaultScope();
|
||||
|
||||
final var sw = new StringWriter();
|
||||
final var out = new WebViewComponentWriter(sw);
|
||||
final Closure<?> renderer = template.getRenderer();
|
||||
renderer.call(context, out);
|
||||
|
||||
assertEquals("Hello, World!", sw.toString());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package groowt.view.web
|
||||
|
||||
import groowt.view.component.ComponentFactoryBase
|
||||
import groowt.view.web.lib.WithContext
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals
|
||||
|
||||
class DefaultWebViewComponentTests implements WithContext {
|
||||
|
||||
private static final class Greeter extends DefaultWebViewComponent {
|
||||
|
||||
private final String target
|
||||
|
||||
Greeter(Map<String, Object> attr) {
|
||||
super('Hello, $target!')
|
||||
this.target = Objects.requireNonNull(attr.get("target"))
|
||||
}
|
||||
|
||||
String getTarget() {
|
||||
return this.target
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class GreeterFactory extends ComponentFactoryBase<Greeter> {
|
||||
|
||||
Greeter doCreate(Map<String, Object> attr) {
|
||||
return new Greeter(attr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class UsingGreeter extends DefaultWebViewComponent {
|
||||
|
||||
UsingGreeter() {
|
||||
super("<Greeter target='World' />")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void withPreambleImport() {
|
||||
def c = new DefaultWebViewComponent(
|
||||
'''
|
||||
---
|
||||
import groovy.transform.Field
|
||||
|
||||
@Field
|
||||
String greeting = 'Hello, World!'
|
||||
---
|
||||
$greeting
|
||||
'''.stripIndent().trim()
|
||||
)
|
||||
c.context = this.context()
|
||||
assertEquals("Hello, World!", c.render())
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedGreeter() {
|
||||
def context = this.context {
|
||||
this.configureContext(it)
|
||||
currentScope.add('Greeter', new GreeterFactory())
|
||||
}
|
||||
def c = new DefaultWebViewComponent('<Greeter target="World" />')
|
||||
c.context = context
|
||||
assertEquals('Hello, World!', c.render())
|
||||
}
|
||||
|
||||
@Test
|
||||
void doubleNested() {
|
||||
def context = this.context {
|
||||
this.configureContext(it)
|
||||
currentScope.add('UsingGreeter') { new UsingGreeter() }
|
||||
currentScope.add('Greeter', new GreeterFactory())
|
||||
}
|
||||
def c = new DefaultWebViewComponent('<UsingGreeter />')
|
||||
c.context = context
|
||||
assertEquals('Hello, World!', c.render())
|
||||
}
|
||||
|
||||
}
|
@ -1,16 +1,31 @@
|
||||
package groowt.view.web.lib
|
||||
|
||||
import groowt.view.web.DefaultWebViewComponentContext
|
||||
import groowt.view.web.WebViewComponentContext
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class EchoTests extends AbstractWebViewComponentTests {
|
||||
|
||||
@Override
|
||||
void configureContext(WebViewComponentContext context) {
|
||||
super.configureContext(context)
|
||||
context.currentScope.add('Echo', Echo.FACTORY)
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeOnlySelfClose() {
|
||||
def context = new DefaultWebViewComponentContext()
|
||||
context.pushDefaultScope()
|
||||
context.currentScope.add('Echo', new Echo.EchoFactory())
|
||||
this.doTest('<Echo(true) />', '<Echo />', context)
|
||||
void selfClose() {
|
||||
this.doTest('<Echo />', '<Echo />')
|
||||
}
|
||||
|
||||
@Test
|
||||
void noSelfClose() {
|
||||
this.doTest('<Echo(false) />', '<Echo></Echo>')
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("Not possible to render children directly to a writer yet.")
|
||||
void withChildren() {
|
||||
this.doTest('<Echo>Hello, World!</Echo>', '<Echo>Hello, World!</Echo>')
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,9 +4,11 @@ import groowt.view.component.ComponentContext
|
||||
import groowt.view.component.ComponentFactory
|
||||
import groowt.view.web.DefaultWebViewComponent
|
||||
import groowt.view.web.DefaultWebViewComponentContext
|
||||
import groowt.view.web.WebViewTemplateComponentSource
|
||||
import groowt.view.component.TemplateSource
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
import static groowt.view.web.WebViewComponentFactories.withAttr
|
||||
|
||||
class FragmentTests extends AbstractWebViewComponentTests {
|
||||
|
||||
static class Greeter extends DefaultWebViewComponent {
|
||||
@ -14,7 +16,7 @@ class FragmentTests extends AbstractWebViewComponentTests {
|
||||
String greeting
|
||||
|
||||
Greeter(Map<String, Object> attr) {
|
||||
super(WebViewTemplateComponentSource.of('$greeting'))
|
||||
super(TemplateSource.of('$greeting'))
|
||||
greeting = attr.greeting
|
||||
}
|
||||
|
||||
@ -22,9 +24,7 @@ class FragmentTests extends AbstractWebViewComponentTests {
|
||||
|
||||
private final ComponentContext greeterContext = new DefaultWebViewComponentContext().tap {
|
||||
pushDefaultScope()
|
||||
def greeterFactory = ComponentFactory.ofClosure { type, componentContext, attr ->
|
||||
new Greeter(attr)
|
||||
}
|
||||
def greeterFactory = withAttr(Greeter.&new)
|
||||
currentScope.add('Greeter', greeterFactory)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,34 @@
|
||||
package groowt.view.web.lib
|
||||
|
||||
|
||||
import groowt.view.component.ComponentContext
|
||||
import groowt.view.web.DefaultWebViewComponentTemplateCompiler
|
||||
import groowt.view.web.WebViewComponentTemplateCompiler
|
||||
import groowt.view.web.runtime.DefaultWebViewComponentWriter
|
||||
import org.codehaus.groovy.control.CompilerConfiguration
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals
|
||||
|
||||
abstract class AbstractWebViewComponentTests implements WithContext {
|
||||
|
||||
protected WebViewComponentTemplateCompiler compiler() {
|
||||
new DefaultWebViewComponentTemplateCompiler(
|
||||
CompilerConfiguration.DEFAULT,
|
||||
this.class.packageName
|
||||
)
|
||||
}
|
||||
|
||||
protected void doTest(Reader source, String expected, ComponentContext context) {
|
||||
def template = this.compiler().compileAnonymous(source)
|
||||
def renderer = template.getRenderer()
|
||||
def sw = new StringWriter()
|
||||
def out = new DefaultWebViewComponentWriter(sw)
|
||||
renderer.call(context, out)
|
||||
assertEquals(expected, sw.toString())
|
||||
}
|
||||
|
||||
protected void doTest(String source, String expected, ComponentContext context = this.context()) {
|
||||
this.doTest(new StringReader(source), expected, context)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package groowt.view.web.lib
|
||||
|
||||
import groovy.transform.stc.ClosureParams
|
||||
import groovy.transform.stc.SimpleType
|
||||
import groowt.view.web.DefaultWebViewComponentContext
|
||||
import groowt.view.web.WebViewComponentContext
|
||||
|
||||
trait WithContext {
|
||||
|
||||
WebViewComponentContext context(
|
||||
@ClosureParams(value = SimpleType, options = 'groowt.view.web.WebViewComponentContext')
|
||||
@DelegatesTo(value = WebViewComponentContext)
|
||||
Closure configure
|
||||
) {
|
||||
new DefaultWebViewComponentContext().tap {
|
||||
pushDefaultScope()
|
||||
configure.delegate = it
|
||||
configure(it)
|
||||
}
|
||||
}
|
||||
|
||||
WebViewComponentContext context() {
|
||||
new DefaultWebViewComponentContext().tap {
|
||||
configureContext(it)
|
||||
}
|
||||
}
|
||||
|
||||
void configureContext(WebViewComponentContext context) {
|
||||
context.pushDefaultScope()
|
||||
}
|
||||
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package groowt.view.web.lib;
|
||||
|
||||
import groovy.lang.Closure;
|
||||
import groowt.view.component.ComponentContext;
|
||||
import groowt.view.component.ComponentTemplate;
|
||||
import groowt.view.web.DefaultWebComponentTemplateCompiler;
|
||||
import groowt.view.web.DefaultWebViewComponentContext;
|
||||
import groowt.view.web.runtime.WebViewComponentWriter;
|
||||
import org.codehaus.groovy.control.CompilerConfiguration;
|
||||
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public abstract class AbstractWebViewComponentTests {
|
||||
|
||||
protected void doTest(Reader source, String expected, ComponentContext context) {
|
||||
final var compiler = new DefaultWebComponentTemplateCompiler(
|
||||
CompilerConfiguration.DEFAULT, this.getClass().getPackageName()
|
||||
);
|
||||
final ComponentTemplate template = compiler.compileAnonymous(source);
|
||||
final Closure<?> renderer = template.getRenderer();
|
||||
final StringWriter sw = new StringWriter();
|
||||
final WebViewComponentWriter out = new WebViewComponentWriter(sw);
|
||||
renderer.call(context, out);
|
||||
assertEquals(expected, sw.toString().trim());
|
||||
}
|
||||
|
||||
protected void doTest(String source, String expected, ComponentContext context) {
|
||||
this.doTest(new StringReader(source), expected, context);
|
||||
}
|
||||
|
||||
protected void doTest(String source, String expected) {
|
||||
final var context = new DefaultWebViewComponentContext();
|
||||
context.pushDefaultScope();
|
||||
this.doTest(source, expected, context);
|
||||
}
|
||||
|
||||
}
|
@ -1,11 +1,7 @@
|
||||
package groowt.view.web.tools
|
||||
|
||||
import groowt.view.component.DefaultComponentContext
|
||||
import groowt.view.web.DefaultWebComponentTemplateCompiler
|
||||
import groowt.view.web.DefaultWebViewComponent
|
||||
import groowt.view.web.WebViewTemplateComponentSource
|
||||
import groowt.view.web.runtime.WebViewComponentWriter
|
||||
import org.codehaus.groovy.control.CompilerConfiguration
|
||||
import picocli.CommandLine
|
||||
import picocli.CommandLine.Command
|
||||
import picocli.CommandLine.Option
|
||||
@ -30,7 +26,7 @@ class RunTemplate implements Callable<Integer> {
|
||||
|
||||
@Override
|
||||
Integer call() throws Exception {
|
||||
def component = new DefaultWebViewComponent(WebViewTemplateComponentSource.of(this.template))
|
||||
def component = new DefaultWebViewComponent(this.template)
|
||||
|
||||
def context = new DefaultComponentContext()
|
||||
context.pushDefaultScope()
|
||||
|
Loading…
Reference in New Issue
Block a user