Major refactoring of compiler, transpiler, and general api.

This commit is contained in:
JesseBrault0709 2024-05-08 13:51:49 +02:00
parent 2d4e085bb3
commit 3a171b8736
132 changed files with 3879 additions and 2343 deletions

View File

@ -1,10 +1,8 @@
package groowt.view.component; package groowt.view.component;
import groovy.lang.Closure; import groovy.lang.Closure;
import groowt.view.component.compiler.ComponentTemplateCompileErrorException; import groowt.view.component.compiler.*;
import groowt.view.component.compiler.ComponentTemplateCompiler;
import groowt.view.component.context.ComponentContext; import groowt.view.component.context.ComponentContext;
import groowt.view.component.factory.ComponentTemplateSource;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
@ -13,44 +11,45 @@ import java.util.function.Function;
public abstract class AbstractViewComponent implements ViewComponent { public abstract class AbstractViewComponent implements ViewComponent {
private ComponentContext context; private static final ComponentTemplateClassFactory templateClassFactory = new SimpleComponentTemplateClassFactory();
private ComponentTemplate template;
public AbstractViewComponent() {} private static ComponentTemplate instantiateTemplate(Class<? extends ComponentTemplate> templateClass) {
public AbstractViewComponent(ComponentTemplate template) {
this.template = Objects.requireNonNull(template);
}
public AbstractViewComponent(Class<? extends ComponentTemplate> templateClass) {
try { try {
this.template = templateClass.getConstructor().newInstance(); return templateClass.getConstructor().newInstance();
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
protected AbstractViewComponent(ComponentTemplateSource source, ComponentTemplateCompiler compiler) { private final ComponentTemplate template;
try { private ComponentContext context;
this.template = compiler.compileAndGet(this.getSelfClass(), source);
} catch (ComponentTemplateCompileErrorException e) { public AbstractViewComponent() {
throw new RuntimeException(e); this.template = null;
}
} }
protected AbstractViewComponent( public AbstractViewComponent(ComponentTemplate template) {
ComponentTemplateSource source, this.template = template;
Function<? super Class<? extends AbstractViewComponent>, ? extends ComponentTemplateCompiler> compilerFunction }
public AbstractViewComponent(Class<? extends ComponentTemplate> templateClass) {
this.template = instantiateTemplate(templateClass);
}
public AbstractViewComponent(
Function<? super Class<? extends AbstractViewComponent>, ComponentTemplateCompileUnit> compileUnitFunction
) { ) {
final var compiler = compilerFunction.apply(this.getSelfClass()); final ComponentTemplateCompileResult compileResult;
try { try {
this.template = compiler.compileAndGet(this.getSelfClass(), source); compileResult = compileUnitFunction.apply(this.getClass()).compile();
} catch (ComponentTemplateCompileErrorException e) { } catch (ComponentTemplateCompileException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
final var templateClass = templateClassFactory.getTemplateClass(compileResult);
this.template = instantiateTemplate(templateClass);
} }
protected abstract Class<? extends AbstractViewComponent> getSelfClass();
@Override @Override
public void setContext(ComponentContext context) { public void setContext(ComponentContext context) {
@ -66,26 +65,20 @@ public abstract class AbstractViewComponent implements ViewComponent {
return Objects.requireNonNull(template); return Objects.requireNonNull(template);
} }
protected void setTemplate(ComponentTemplate template) { protected void beforeRender() {}
this.template = Objects.requireNonNull(template);
}
protected void beforeRender() { protected void afterRender() {}
this.getContext().beforeComponentRender(this);
}
protected void afterRender() {
this.getContext().afterComponentRender(this);
}
/** /**
* @implSpec If overriding, <strong>please</strong> call * @implSpec If overriding, <strong>please</strong> call
* {@link #beforeRender()}and {@link #afterRender()} before * {@link #beforeRender()} and {@link #afterRender()} before
* and after the actual rendering is done, respectively. * and after the actual rendering is done, respectively;
* this way, components can still do their before/after
* logic even if this method is overwritten.
*/ */
@Override @Override
public void renderTo(Writer out) throws IOException { public void renderTo(Writer out) throws IOException {
final Closure<?> closure = this.template.getRenderer(); final Closure<?> closure = this.getTemplate().getRenderer();
closure.setDelegate(this); closure.setDelegate(this);
closure.setResolveStrategy(Closure.DELEGATE_FIRST); closure.setResolveStrategy(Closure.DELEGATE_FIRST);
this.beforeRender(); this.beforeRender();

View File

@ -5,10 +5,6 @@ import groowt.view.component.context.ComponentContext;
public interface ViewComponent extends View { public interface ViewComponent extends View {
default String getTypeName() {
return this.getClass().getName();
}
/** /**
* <em>Note:</em> compiled templates are required to automatically * <em>Note:</em> compiled templates are required to automatically
* call this method after the component is constructed. One * call this method after the component is constructed. One

View File

@ -0,0 +1,31 @@
package groowt.view.component;
public class ViewComponentBugError extends RuntimeException {
public ViewComponentBugError(String message) {
super(message);
}
public ViewComponentBugError(String message, Throwable cause) {
super(message, cause);
}
public ViewComponentBugError(Throwable cause) {
super(cause);
}
public ViewComponentBugError(
String message,
Throwable cause,
boolean enableSuppression,
boolean writableStackTrace
) {
super(message, cause, enableSuppression, writableStackTrace);
}
@Override
public String getMessage() {
return "BUG! Please file an issue report at the github repository. " + super.getMessage();
}
}

View File

@ -0,0 +1,30 @@
package groowt.view.component.compiler;
import groowt.view.component.ViewComponent;
import groowt.view.component.compiler.source.ComponentTemplateSource;
public abstract class AbstractComponentTemplateCompileUnit implements
ComponentTemplateCompileUnit {
private final Class<? extends ViewComponent> forClass;
private final ComponentTemplateSource source;
public AbstractComponentTemplateCompileUnit(
Class<? extends ViewComponent> forClass,
ComponentTemplateSource source
) {
this.forClass = forClass;
this.source = source;
}
@Override
public Class<? extends ViewComponent> getForClass() {
return this.forClass;
}
@Override
public ComponentTemplateSource getSource() {
return this.source;
}
}

View File

@ -1,41 +0,0 @@
package groowt.view.component.compiler;
import groovy.lang.GroovyClassLoader;
import groowt.view.component.ComponentTemplate;
import groowt.view.component.ViewComponent;
import groowt.view.component.factory.ComponentTemplateSource;
import java.io.IOException;
import java.io.Reader;
public abstract class AbstractComponentTemplateCompiler implements ComponentTemplateCompiler {
private final GroovyClassLoader groovyClassLoader;
public AbstractComponentTemplateCompiler(GroovyClassLoader groovyClassLoader) {
this.groovyClassLoader = groovyClassLoader;
}
protected abstract ComponentTemplateCompileResult compile(
ComponentTemplateSource componentTemplateSource,
Class<? extends ViewComponent> forClass,
Reader actualSource
) throws ComponentTemplateCompileErrorException;
@Override
public ComponentTemplateCompileResult compile(Class<? extends ViewComponent> forClass, ComponentTemplateSource source)
throws ComponentTemplateCompileErrorException {
try (final Reader reader = ComponentTemplateSource.toReader(source)) {
return this.compile(source, forClass, reader);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public ComponentTemplate compileAndGet(Class<? extends ViewComponent> forClass, ComponentTemplateSource source)
throws ComponentTemplateCompileErrorException {
return this.compileAndGet(this.groovyClassLoader, forClass, source);
}
}

View File

@ -1,104 +1,59 @@
package groowt.view.component.compiler; package groowt.view.component.compiler;
import groovy.lang.GroovyClassLoader;
import groowt.view.component.ComponentTemplate;
import groowt.view.component.ViewComponent; import groowt.view.component.ViewComponent;
import groowt.view.component.factory.ComponentTemplateSource;
import org.codehaus.groovy.tools.GroovyClass;
import org.jetbrains.annotations.Nullable;
import java.io.Reader;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
public abstract class CachingComponentTemplateCompiler extends AbstractComponentTemplateCompiler { public abstract class CachingComponentTemplateCompiler<U extends ComponentTemplateCompileUnit>
implements ComponentTemplateCompiler<U> {
private record CachedTemplate( private final Map<Class<? extends ViewComponent>, ComponentTemplateCompileResult> cache = new HashMap<>();
ComponentTemplateCompileResult compileResult,
@Nullable ComponentTemplate template
) {}
private final Map<Class<? extends ViewComponent>, CachedTemplate> cache = new HashMap<>(); // private ComponentTemplate instantiate(
// GroovyClassLoader groovyClassLoader,
public CachingComponentTemplateCompiler(GroovyClassLoader groovyClassLoader) { // CompileResult compileResult
super(groovyClassLoader); // ) {
} // for (final var groovyClass : compileResult.otherClasses()) {
// // Try to find it. If we can't, we need to load it via the groovy loader
private ComponentTemplate instantiate( // try {
GroovyClassLoader groovyClassLoader, // Class.forName(groovyClass.getName(), true, groovyClassLoader);
ComponentTemplateCompileResult compileResult // } catch (ClassNotFoundException ignored) {
) { // groovyClassLoader.defineClass(groovyClass.getName(), groovyClass.getBytes());
for (final var groovyClass : compileResult.otherClasses()) { // } catch (LinkageError ignored) {
// Try to find it. If we can't, we need to load it via the groovy loader // // no-op, because we already have it
try { // }
Class.forName(groovyClass.getName(), true, groovyClassLoader); // }
} catch (ClassNotFoundException ignored) { // final GroovyClass templateGroovyClass = compileResult.templateClass();
groovyClassLoader.defineClass(groovyClass.getName(), groovyClass.getBytes()); // Class<?> templateClass;
} catch (LinkageError ignored) { // // Try to find it. If we can't, we need to load it via the groovy loader
// no-op, because we already have it // try {
} // templateClass = Class.forName(templateGroovyClass.getName(), true, groovyClassLoader);
} // } catch (ClassNotFoundException ignored) {
final GroovyClass templateGroovyClass = compileResult.templateClass(); // templateClass = groovyClassLoader.defineClass(
Class<?> templateClass; // templateGroovyClass.getName(),
// Try to find it. If we can't, we need to load it via the groovy loader // templateGroovyClass.getBytes()
try { // );
templateClass = Class.forName(templateGroovyClass.getName(), true, groovyClassLoader); // }
} catch (ClassNotFoundException ignored) { // try {
templateClass = groovyClassLoader.defineClass( // return (ComponentTemplate) templateClass.getConstructor().newInstance();
templateGroovyClass.getName(), // } catch (Exception e) {
templateGroovyClass.getBytes() // throw new RuntimeException("Unable to instantiate ComponentTemplate " + templateClass.getName(), e);
); // }
} // }
try {
return (ComponentTemplate) templateClass.getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("Unable to instantiate ComponentTemplate " + templateClass.getName(), e);
}
}
@Override @Override
public final ComponentTemplate compileAndGet( public final ComponentTemplateCompileResult compile(U compileUnit)
GroovyClassLoader groovyClassLoader, throws ComponentTemplateCompileException {
Class<? extends ViewComponent> forClass, if (this.cache.containsKey(compileUnit.getForClass())) {
ComponentTemplateSource source return this.cache.get(compileUnit.getForClass());
) throws ComponentTemplateCompileErrorException {
if (this.cache.containsKey(forClass)) {
final var cached = this.cache.get(forClass);
if (cached.template() == null) {
final ComponentTemplate template = this.instantiate(groovyClassLoader, cached.compileResult());
this.cache.put(forClass, new CachedTemplate(cached.compileResult(), template));
return template;
} else { } else {
return cached.template(); final ComponentTemplateCompileResult compileResult = this.doCompile(compileUnit);
} this.cache.put(compileUnit.getForClass(), compileResult);
} else {
final ComponentTemplateCompileResult compileResult = this.compile(forClass, source);
final ComponentTemplate template = this.instantiate(groovyClassLoader, compileResult);
this.cache.put(forClass, new CachedTemplate(compileResult, template));
return template;
}
}
@Override
protected final ComponentTemplateCompileResult compile(
ComponentTemplateSource componentTemplateSource,
Class<? extends ViewComponent> forClass,
Reader actualSource
) throws ComponentTemplateCompileErrorException {
if (this.cache.containsKey(forClass)) {
return this.cache.get(forClass).compileResult();
} else {
final ComponentTemplateCompileResult compileResult =
this.doCompile(componentTemplateSource, forClass, actualSource);
this.cache.put(forClass, new CachedTemplate(compileResult, null));
return compileResult; return compileResult;
} }
} }
protected abstract ComponentTemplateCompileResult doCompile( protected abstract ComponentTemplateCompileResult doCompile(U compileUnit) throws ComponentTemplateCompileException;
ComponentTemplateSource source,
Class<? extends ViewComponent> forClass,
Reader reader
) throws ComponentTemplateCompileErrorException;
} }

View File

@ -0,0 +1,7 @@
package groowt.view.component.compiler;
import groowt.view.component.ComponentTemplate;
public interface ComponentTemplateClassFactory {
Class<? extends ComponentTemplate> getTemplateClass(ComponentTemplateCompileResult compileResult);
}

View File

@ -1,61 +0,0 @@
package groowt.view.component.compiler;
import groowt.view.component.ViewComponent;
/**
* Represents an exception thrown while attempting to instantiate a ComponentTemplate during compilation.
*/
public class ComponentTemplateCompileErrorException extends Exception {
private final Class<? extends ViewComponent> forClass;
private final Object templateSource;
public ComponentTemplateCompileErrorException(
String message,
Class<? extends ViewComponent> forClass,
Object templateSource
) {
super(message);
this.forClass = forClass;
this.templateSource = templateSource;
}
public ComponentTemplateCompileErrorException(
String message,
Throwable cause,
Class<? extends ViewComponent> forClass,
Object templateSource
) {
super(message, cause);
this.forClass = forClass;
this.templateSource = templateSource;
}
public ComponentTemplateCompileErrorException(
Throwable cause,
Class<? extends ViewComponent> forClass,
Object templateSource
) {
super(cause);
this.forClass = forClass;
this.templateSource = templateSource;
}
public ComponentTemplateCompileErrorException(
Class<? extends ViewComponent> forClass,
Object templateSource
) {
super("Compile error in " + templateSource + " for " + forClass.getName());
this.forClass = forClass;
this.templateSource = templateSource;
}
public Class<? extends ViewComponent> getForClass() {
return this.forClass;
}
public Object getTemplateSource() {
return this.templateSource;
}
}

View File

@ -0,0 +1,37 @@
package groowt.view.component.compiler;
import org.jetbrains.annotations.Nullable;
public class ComponentTemplateCompileException extends Exception {
private final ComponentTemplateCompileUnit compileUnit;
public ComponentTemplateCompileException(ComponentTemplateCompileUnit compileUnit, String message) {
super(message);
this.compileUnit = compileUnit;
}
public ComponentTemplateCompileException(
ComponentTemplateCompileUnit compileUnit,
String message,
Throwable cause
) {
super(message, cause);
this.compileUnit = compileUnit;
}
@Override
public String getMessage() {
final var sb = new StringBuilder("Error in ").append(compileUnit.getSource().getDescription());
final @Nullable String position = this.getPosition();
if (position != null) {
sb.append(" at ").append(position);
}
return sb.append(": ").append(super.getMessage()).toString();
}
protected @Nullable String getPosition() {
return null;
}
}

View File

@ -0,0 +1,10 @@
package groowt.view.component.compiler;
import org.codehaus.groovy.tools.GroovyClass;
import java.util.Set;
public interface ComponentTemplateCompileResult {
GroovyClass getTemplateClass();
Set<GroovyClass> getOtherClasses();
}

View File

@ -0,0 +1,18 @@
package groowt.view.component.compiler;
import groowt.view.component.ViewComponent;
import groowt.view.component.compiler.source.ComponentTemplateSource;
public interface ComponentTemplateCompileUnit {
Class<? extends ViewComponent> getForClass();
String getDefaultPackageName();
ComponentTemplateSource getSource();
ComponentTemplateCompileResult compile(ComponentTemplateCompilerConfiguration configuration)
throws ComponentTemplateCompileException;
default ComponentTemplateCompileResult compile() throws ComponentTemplateCompileException {
return this.compile(new DefaultComponentTemplateCompilerConfiguration());
}
}

View File

@ -1,34 +1,7 @@
package groowt.view.component.compiler; package groowt.view.component.compiler;
import groovy.lang.GroovyClassLoader; public interface ComponentTemplateCompiler<U extends ComponentTemplateCompileUnit> {
import groowt.view.component.ComponentTemplate;
import groowt.view.component.ViewComponent;
import groowt.view.component.factory.ComponentTemplateSource;
import org.codehaus.groovy.tools.GroovyClass;
import java.io.IOException; ComponentTemplateCompileResult compile(U compileUnit) throws ComponentTemplateCompileException;
import java.util.List;
public interface ComponentTemplateCompiler {
record ComponentTemplateCompileResult(GroovyClass templateClass, List<GroovyClass> otherClasses) {}
ComponentTemplateCompileResult compile(Class<? extends ViewComponent> forClass, ComponentTemplateSource source)
throws ComponentTemplateCompileErrorException;
ComponentTemplate compileAndGet(
GroovyClassLoader groovyClassLoader,
Class<? extends ViewComponent> forClass,
ComponentTemplateSource source
) throws ComponentTemplateCompileErrorException;
default ComponentTemplate compileAndGet(Class<? extends ViewComponent> forClass, ComponentTemplateSource source)
throws ComponentTemplateCompileErrorException {
try (final GroovyClassLoader groovyClassLoader = new GroovyClassLoader(this.getClass().getClassLoader())) {
return this.compileAndGet(groovyClassLoader, forClass, source);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
}
}
} }

View File

@ -0,0 +1,11 @@
package groowt.view.component.compiler;
import groovy.lang.GroovyClassLoader;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.CompilerConfiguration;
public interface ComponentTemplateCompilerConfiguration {
GroovyClassLoader getGroovyClassLoader();
CompilerConfiguration getGroovyCompilerConfiguration();
CompilePhase getToCompilePhase();
}

View File

@ -0,0 +1,44 @@
package groowt.view.component.compiler;
import groovy.lang.GroovyClassLoader;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.CompilerConfiguration;
public class DefaultComponentTemplateCompilerConfiguration implements ComponentTemplateCompilerConfiguration {
private GroovyClassLoader groovyClassLoader;
private CompilerConfiguration groovyCompilerConfiguration;
private CompilePhase toCompilePhase;
@Override
public GroovyClassLoader getGroovyClassLoader() {
return this.groovyClassLoader != null
? this.groovyClassLoader
: new GroovyClassLoader(this.getClass().getClassLoader());
}
public void setGroovyClassLoader(GroovyClassLoader groovyClassLoader) {
this.groovyClassLoader = groovyClassLoader;
}
@Override
public CompilerConfiguration getGroovyCompilerConfiguration() {
return this.groovyCompilerConfiguration != null
? this.groovyCompilerConfiguration
: CompilerConfiguration.DEFAULT;
}
public void setGroovyCompilerConfiguration(CompilerConfiguration groovyCompilerConfiguration) {
this.groovyCompilerConfiguration = groovyCompilerConfiguration;
}
@Override
public CompilePhase getToCompilePhase() {
return this.toCompilePhase != null ? this.toCompilePhase : CompilePhase.CLASS_GENERATION;
}
public void setToCompilePhase(CompilePhase toCompilePhase) {
this.toCompilePhase = toCompilePhase;
}
}

View File

@ -0,0 +1,108 @@
package groowt.view.component.compiler;
import groowt.view.component.ComponentTemplate;
import org.codehaus.groovy.tools.GroovyClass;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import static java.nio.file.StandardOpenOption.CREATE_NEW;
import static java.nio.file.StandardOpenOption.WRITE;
public final class SimpleComponentTemplateClassFactory implements ComponentTemplateClassFactory {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private static String[] classNameToPackageDirParts(String fullClassName) {
final String[] allParts = fullClassName.split("\\.");
if (allParts.length == 0) {
throw new RuntimeException("Did not expect allParts.length to be zero.");
} else if (allParts.length == 1) {
return EMPTY_STRING_ARRAY;
} else {
final var result = new String[allParts.length - 1];
System.arraycopy(allParts, 0, result, 0, allParts.length - 1);
return result;
}
}
private static Path resolvePackageDir(Path rootDir, String[] packageDirParts) {
return Path.of(rootDir.toString(), packageDirParts);
}
private static String isolateClassName(String fullClassName) {
final String[] parts = fullClassName.split("\\.");
if (parts.length == 0) {
throw new RuntimeException("Did not expect parts.length to be zero");
}
return parts[parts.length - 1];
}
private final Map<String, Class<? extends ComponentTemplate>> cache = new HashMap<>();
private final ClassLoader classLoader;
private final Path tempClassesDir;
public SimpleComponentTemplateClassFactory() {
try {
this.tempClassesDir = Files.createTempDirectory("view-component-classes-");
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
this.classLoader = new URLClassLoader(
"SimpleComponentTemplateClassFactoryClassLoader",
new URL[] { this.tempClassesDir.toUri().toURL() },
this.getClass().getClassLoader()
);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
private void writeClassToDisk(GroovyClass groovyClass) {
final var className = groovyClass.getName();
final var packageDirParts = classNameToPackageDirParts(className);
final var packageDir = resolvePackageDir(this.tempClassesDir, packageDirParts);
try {
Files.createDirectories(packageDir);
} catch (IOException e) {
throw new RuntimeException(e);
}
final var classFile = Path.of(packageDir.toString(), isolateClassName(className) + ".class");
try {
Files.write(classFile, groovyClass.getBytes(), CREATE_NEW, WRITE);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public Class<? extends ComponentTemplate> getTemplateClass(ComponentTemplateCompileResult compileResult) {
final String templateClassName = compileResult.getTemplateClass().getName();
if (this.cache.containsKey(templateClassName)) {
return this.cache.get(templateClassName);
} else {
// write classes to disk
this.writeClassToDisk(compileResult.getTemplateClass());
compileResult.getOtherClasses().forEach(this::writeClassToDisk);
// load the template class
try {
//noinspection unchecked
final var templateClass = (Class<? extends ComponentTemplate>) this.classLoader.loadClass(
templateClassName
);
this.cache.put(templateClassName, templateClass);
return templateClass;
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@ -0,0 +1,50 @@
package groowt.view.component.compiler;
import org.codehaus.groovy.tools.GroovyClass;
import java.util.HashSet;
import java.util.Set;
public class SimpleComponentTemplateCompileResult implements ComponentTemplateCompileResult {
private final GroovyClass templateClass;
private final Set<GroovyClass> otherClasses;
public SimpleComponentTemplateCompileResult(
GroovyClass templateClass,
Set<GroovyClass> otherClasses
) {
this.templateClass = templateClass;
this.otherClasses = otherClasses;
}
@Override
public GroovyClass getTemplateClass() {
return this.templateClass;
}
@Override
public Set<GroovyClass> getOtherClasses() {
return new HashSet<>(this.otherClasses);
}
@Override
public int hashCode() {
return this.templateClass.getName().hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj instanceof ComponentTemplateCompileResult other) {
return this.templateClass.getName().equals(other.getTemplateClass().getName());
}
return false;
}
@Override
public String toString() {
return "sctCompileResult(" + this.templateClass.getName() + ")";
}
}

View File

@ -0,0 +1,53 @@
package groowt.view.component.compiler.source;
import java.io.File;
import java.io.InputStream;
import java.io.Reader;
import java.net.URI;
import java.net.URL;
import java.util.List;
public interface ComponentTemplateSource {
static ComponentTemplateSource of(String template) {
return new StringSource(template, null);
}
static ComponentTemplateSource of(String template, String name) {
return new StringSource(template, name);
}
static ComponentTemplateSource of(File templateFile) {
return new FileSource(templateFile);
}
static ComponentTemplateSource of(URI templateURI) {
return new URISource(templateURI);
}
static ComponentTemplateSource of(URL url) {
return new URLSource(url);
}
static ComponentTemplateSource of(InputStream templateInputStream) {
return new InputStreamSource(templateInputStream, null);
}
static ComponentTemplateSource of(InputStream templateInputStream, String description) {
return new InputStreamSource(templateInputStream, description);
}
static ComponentTemplateSource of(Reader templateReader) {
return new ReaderSource(templateReader, null);
}
static ComponentTemplateSource of(Reader templateReader, String description) {
return new ReaderSource(templateReader, description);
}
Reader toReader() throws Exception;
String getDescription();
boolean canReopen();
List<String> getLines();
}

View File

@ -0,0 +1,50 @@
package groowt.view.component.compiler.source;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.net.URI;
import java.util.List;
public class FileSource implements ComponentTemplateSource {
private final File templateFile;
private List<String> lines;
public FileSource(File templateFile) {
this.templateFile = templateFile;
}
@Override
public Reader toReader() throws Exception {
return new FileReader(this.templateFile);
}
@Override
public String getDescription() {
return this.templateFile.toString();
}
@Override
public boolean canReopen() {
return true;
}
@Override
public List<String> getLines() {
if (this.lines == null) {
try (final var fis = new FileInputStream(this.templateFile)) {
final var allSource = new String(fis.readAllBytes());
this.lines = allSource.lines().toList();
} catch (IOException ioException) {
throw new RuntimeException(ioException);
}
}
return this.lines;
}
public @Nullable URI getURI() {
return templateFile.toURI();
}
}

View File

@ -0,0 +1,40 @@
package groowt.view.component.compiler.source;
import org.jetbrains.annotations.Nullable;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.List;
public class InputStreamSource implements ComponentTemplateSource {
private final InputStream templateInputStream;
private final @Nullable String description;
public InputStreamSource(InputStream templateInputStream, @Nullable String description) {
this.templateInputStream = templateInputStream;
this.description = description;
}
@Override
public Reader toReader() {
return new InputStreamReader(this.templateInputStream);
}
@Override
public String getDescription() {
return this.description != null ? this.description : "<anonymous InputStream source>";
}
@Override
public boolean canReopen() {
return false;
}
@Override
public List<String> getLines() {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,38 @@
package groowt.view.component.compiler.source;
import org.jetbrains.annotations.Nullable;
import java.io.Reader;
import java.util.List;
public class ReaderSource implements ComponentTemplateSource {
private final Reader reader;
private final @Nullable String description;
public ReaderSource(Reader reader, @Nullable String description) {
this.reader = reader;
this.description = description;
}
@Override
public Reader toReader() {
return this.reader;
}
@Override
public String getDescription() {
return this.description != null ? this.description : "<anonymous Reader source>";
}
@Override
public boolean canReopen() {
return false;
}
@Override
public List<String> getLines() {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,39 @@
package groowt.view.component.compiler.source;
import org.jetbrains.annotations.Nullable;
import java.io.Reader;
import java.io.StringReader;
import java.util.List;
public class StringSource implements ComponentTemplateSource {
private final String template;
private final @Nullable String name;
public StringSource(String template, @Nullable String name) {
this.template = template;
this.name = name;
}
@Override
public Reader toReader() {
return new StringReader(this.template);
}
@Override
public String getDescription() {
return this.name != null ? this.name : "<anonymous string source>";
}
@Override
public boolean canReopen() {
return true;
}
@Override
public List<String> getLines() {
return this.template.lines().toList();
}
}

View File

@ -0,0 +1,50 @@
package groowt.view.component.compiler.source;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.util.List;
public class URISource implements ComponentTemplateSource {
private final URI templateURI;
private List<String> lines;
public URISource(URI templateURI) {
this.templateURI = templateURI;
}
@Override
public Reader toReader() throws Exception {
return new InputStreamReader(this.templateURI.toURL().openStream());
}
@Override
public String getDescription() {
return this.templateURI.toString();
}
@Override
public boolean canReopen() {
return true;
}
public URI getURI() {
return this.templateURI;
}
@Override
public List<String> getLines() {
if (this.lines == null) {
try (final var inputStream = this.templateURI.toURL().openStream()) {
final String allSource = new String(inputStream.readAllBytes());
this.lines = allSource.lines().toList();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return this.lines;
}
}

View File

@ -0,0 +1,58 @@
package groowt.view.component.compiler.source;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
public class URLSource implements ComponentTemplateSource {
private final URL url;
private List<String> lines;
public URLSource(URL url) {
this.url = url;
}
@Override
public Reader toReader() throws Exception {
return new InputStreamReader(this.url.openStream());
}
@Override
public String getDescription() {
return this.url.toString();
}
@Override
public boolean canReopen() {
return true;
}
public @Nullable URI getURI() {
try {
return this.url.toURI();
} catch (URISyntaxException e) {
return null;
}
}
@Override
public List<String> getLines() {
if (this.lines == null) {
try (final var inputStream = this.url.openStream()) {
final String allSource = new String(inputStream.readAllBytes());
this.lines = allSource.lines().toList();
} catch (IOException ioException) {
throw new RuntimeException(ioException);
}
}
return this.lines;
}
}

View File

@ -1,61 +1,27 @@
package groowt.view.component.context; package groowt.view.component.context;
import groowt.view.component.ViewComponent; import groowt.view.component.ViewComponent;
import groowt.view.component.factory.ComponentFactory;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Deque;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.function.Predicate; import java.util.function.Predicate;
public interface ComponentContext { public interface ComponentContext {
/** List<ComponentScope> getScopeStack();
* 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);
Deque<ComponentScope> getScopeStack();
void pushScope(ComponentScope scope); void pushScope(ComponentScope scope);
void pushDefaultScope(); void pushDefaultScope();
void popScope(); void popScope();
ComponentScope getRootScope();
default ComponentScope getCurrentScope() { default ComponentScope getCurrentScope() {
return Objects.requireNonNull(this.getScopeStack().peek(), "There is no current scope."); final List<ComponentScope> scopeStack = this.getScopeStack();
if (scopeStack.isEmpty()) {
throw new NullPointerException("There is no current scope.");
}
return scopeStack.getFirst();
} }
Deque<ViewComponent> getComponentStack();
@Nullable ViewComponent getParent(); @Nullable ViewComponent getParent();
@Nullable <T extends ViewComponent> T getParent(Class<T> parentClass); @Nullable <T extends ViewComponent> T getParent(Class<T> parentClass);
@ -66,11 +32,7 @@ public interface ComponentContext {
Class<T> ancestorClass, Class<T> ancestorClass,
Predicate<? super ViewComponent> matching Predicate<? super ViewComponent> matching
) { ) {
return ancestorClass.cast(matching.and(ancestorClass::isInstance)); return ancestorClass.cast(this.findNearestAncestor(matching.and(ancestorClass::isInstance)));
}
default @Nullable ViewComponent findNearestAncestorByTypeName(String typeName) {
return this.findNearestAncestor(component -> component.getTypeName().equals(typeName));
} }
List<ViewComponent> getAllAncestors(); List<ViewComponent> getAllAncestors();

View File

@ -1,37 +0,0 @@
package groowt.view.component.context;
import groowt.view.component.ComponentTemplate;
/**
* 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(
Object componentType,
ComponentTemplate template,
int line,
int column,
Throwable cause
) {
super(cause);
this.componentType = componentType;
this.template = template;
this.line = line;
this.column = column;
}
@Override
public String getMessage() {
return "Exception in " + this.template.getClass().getName() + " while creating "
+ this.componentType.getClass().getName() + " at line " + this.line
+ ", column " + this.column + ".";
}
}

View File

@ -0,0 +1,141 @@
package groowt.view.component.context;
import groowt.view.component.ComponentTemplate;
import groowt.view.component.ViewComponent;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public class ComponentResolveException extends Exception {
private final String typeNameOrAlias;
private @Nullable String message;
private @Nullable Class<? extends ViewComponent> type;
private @Nullable ComponentTemplate template;
private int line;
private int column;
public ComponentResolveException(String typeName) {
this.typeNameOrAlias = Objects.requireNonNull(typeName);
}
public ComponentResolveException(String typeName, @Nullable Class<? extends ViewComponent> type) {
this.typeNameOrAlias = Objects.requireNonNull(typeName);
this.type = type;
}
public ComponentResolveException(
@Nullable String message,
String typeName,
@Nullable Class<? extends ViewComponent> type
) {
this.typeNameOrAlias = typeName;
this.message = message;
this.type = type;
}
public ComponentResolveException(String typeName, Throwable cause) {
super(cause);
this.typeNameOrAlias = Objects.requireNonNull(typeName);
}
public ComponentResolveException(String typeName, @Nullable Class<? extends ViewComponent> type, Throwable cause) {
super(cause);
this.typeNameOrAlias = Objects.requireNonNull(typeName);
this.type = type;
}
public ComponentResolveException(
@NotNull ComponentTemplate template,
Throwable cause,
String typeName,
int line,
int column
) {
super(cause);
this.template = template;
this.typeNameOrAlias = typeName;
this.line = line;
this.column = column;
}
public ComponentResolveException(
@NotNull ComponentTemplate template,
Throwable cause,
String alias,
@NotNull Class<? extends ViewComponent> type,
int line,
int column
) {
super(cause);
this.template = Objects.requireNonNull(template);
this.typeNameOrAlias = alias;
this.type = type;
this.line = line;
this.column = column;
}
public void setMessage(@Nullable String message) {
this.message = message;
}
public String getTypeNameOrAlias() {
return this.typeNameOrAlias;
}
public @Nullable Class<? extends ViewComponent> getType() {
return this.type;
}
@ApiStatus.Internal
public void setType(@Nullable Class<? extends ViewComponent> type) {
this.type = type;
}
public @NotNull ComponentTemplate getTemplate() {
return Objects.requireNonNull(this.template);
}
@ApiStatus.Internal
public void setTemplate(ComponentTemplate template) {
this.template = Objects.requireNonNull(template);
}
public int getLine() {
return this.line;
}
@ApiStatus.Internal
public void setLine(int line) {
this.line = line;
}
public int getColumn() {
return this.column;
}
@ApiStatus.Internal
public void setColumn(int column) {
this.column = column;
}
@Override
public String getMessage() {
final var sb = new StringBuilder("Exception");
if (this.template != null) {
sb.append(" in ").append(this.template.getClass().getName());
}
sb.append(" while resolving component ").append(this.typeNameOrAlias);
if (this.type != null) {
sb.append(" of type ").append(this.type.getName());
}
sb.append(" at line ").append(this.line).append(", column ").append(this.column).append(".");
if (this.message != null) {
sb.append(" ").append(this.message);
} // else, assume caused by is not null
return sb.toString();
}
}

View File

@ -1,48 +1,52 @@
package groowt.view.component.context; package groowt.view.component.context;
import groovy.lang.Closure;
import groowt.view.component.ViewComponent; import groowt.view.component.ViewComponent;
import groowt.view.component.factory.ComponentFactory; import groowt.view.component.factory.ComponentFactory;
public interface ComponentScope { public interface ComponentScope {
void add(String name, ComponentFactory<?> factory); record TypeAndFactory<T extends ViewComponent>(Class<? extends T> type, ComponentFactory<? extends T> factory) {}
boolean contains(String name);
void remove(String name);
ComponentFactory<?> get(String name);
default ComponentFactory<?> factoryMissing(String typeName) { //---- string types
throw new NoFactoryMissingException(this.getClass().getName() + " does not support factoryMissing()");
<T extends ViewComponent> void add(String typeName, Class<T> forClass, ComponentFactory<? extends T> factory);
boolean contains(String typeName);
void remove(String typeName);
TypeAndFactory<?> get(String typeName);
default TypeAndFactory<?> factoryMissing(String typeName) throws ComponentResolveException {
throw new FactoryMissingUnsupportedException(
this.getClass().getName() + " does not support factoryMissing() for string types.",
typeName
);
} }
default <T extends ViewComponent> void add(Class<T> clazz, ComponentFactory<T> factory) { //---- class types
this.add(clazz.getName(), factory);
}
default boolean contains(Class<? extends ViewComponent> clazz) { <T extends ViewComponent> void add(Class<T> forClass, ComponentFactory<? extends T> factory);
return this.contains(clazz.getName());
}
@SuppressWarnings("unchecked") <T extends ViewComponent> void add(
default <T extends ViewComponent> ComponentFactory<T> get(Class<T> clazz) { Class<T> publicType,
return (ComponentFactory<T>) this.get(clazz.getName()); Class<? extends T> implementingType,
} ComponentFactory<? extends T> factory
);
default void remove(Class<? extends ViewComponent> clazz) { boolean contains(Class<? extends ViewComponent> type);
this.remove(clazz.getName());
}
@SuppressWarnings("unchecked") <T extends ViewComponent> TypeAndFactory<T> get(Class<T> type);
default <T extends ViewComponent> ComponentFactory<T> factoryMissing(Class<T> clazz) {
return (ComponentFactory<T>) this.factoryMissing(clazz.getName());
}
default void add(String name, Closure<? extends ViewComponent> closure) { void remove(Class<? extends ViewComponent> type);
this.add(name, ComponentFactory.ofClosure(closure));
}
default <T extends ViewComponent> void add(Class<T> type, Closure<? extends T> closure) { default <T extends ViewComponent> TypeAndFactory<?> factoryMissing(String typeName, Class<T> type)
this.add(type, ComponentFactory.ofClosure(closure)); throws ComponentResolveException {
throw new FactoryMissingUnsupportedException(
this.getClass().getName() + " does not support factoryMissing() for class component types.",
typeName,
type
);
} }
} }

View File

@ -1,98 +1,35 @@
package groowt.view.component.context; package groowt.view.component.context;
import groowt.view.component.ViewComponent; import groowt.view.component.ViewComponent;
import groowt.view.component.factory.ComponentFactory; import groowt.view.component.runtime.RenderContext;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.function.Predicate; import java.util.function.Predicate;
public class DefaultComponentContext implements ComponentContext { public class DefaultComponentContext implements ComponentContext {
protected static class DefaultResolved implements ComponentContext.Resolved { private final LinkedList<ComponentScope> scopeStack = new LinkedList<>();
private RenderContext renderContext;
private final String typeName; @ApiStatus.Internal
private final ComponentFactory<?> factory; public RenderContext getRenderContext() {
return Objects.requireNonNull(
public DefaultResolved(String typeName, ComponentFactory<?> factory) { this.renderContext,
this.typeName = typeName; "The renderContext is null. Did this method get called from outside of a rendering context?"
this.factory = factory;
}
@Override
public String getTypeName() {
return this.typeName;
}
@Override
public ComponentFactory<?> getComponentFactory() {
return this.factory;
}
}
private final Deque<ComponentScope> scopeStack = new LinkedList<>();
private final Deque<ViewComponent> componentStack = new LinkedList<>();
@Override
public Resolved resolve(String component) {
if (scopeStack.isEmpty()) {
throw new IllegalStateException("There are no scopes on the scopeStack.");
}
final var getStack = new LinkedList<>(this.scopeStack);
while (!getStack.isEmpty()) {
final ComponentScope scope = getStack.pop();
if (scope.contains(component)) {
return new DefaultResolved(component, scope.get(component));
}
}
final var missingStack = new LinkedList<>(this.scopeStack);
NoFactoryMissingException first = null;
while (!missingStack.isEmpty()) {
final ComponentScope scope = missingStack.pop();
try {
return new DefaultResolved(component, scope.factoryMissing(component));
} catch (NoFactoryMissingException e) {
if (first == null) {
first = e;
}
}
}
if (first == null) {
throw new IllegalStateException("First FactoryMissingException is still null.");
}
throw first;
}
@Override
public ViewComponent create(Resolved resolved, Object... args) {
return resolved.getComponentFactory().create(
resolved.getTypeName(), this, args
); );
} }
@Override @ApiStatus.Internal
public void beforeComponentRender(ViewComponent component) { public void setRenderContext(RenderContext renderContext) {
this.componentStack.push(component); this.renderContext = Objects.requireNonNull(renderContext);
} }
@Override @Override
public void afterComponentRender(ViewComponent component) { public List<ComponentScope> getScopeStack() {
final var popped = this.componentStack.pop();
if (!popped.equals(component)) {
throw new IllegalStateException("Popped component does not equal arg to afterComponent()");
}
}
@Override
public Deque<ComponentScope> getScopeStack() {
return new LinkedList<>(this.scopeStack); return new LinkedList<>(this.scopeStack);
} }
@ -116,18 +53,15 @@ public class DefaultComponentContext implements ComponentContext {
} }
@Override @Override
public Deque<ViewComponent> getComponentStack() { public ComponentScope getRootScope() {
return new LinkedList<>(this.componentStack); return this.scopeStack.getLast();
} }
@Override @Override
public @Nullable ViewComponent getParent() { public @Nullable ViewComponent getParent() {
if (this.componentStack.size() > 1) { final List<ViewComponent> componentStack = this.getRenderContext().getComponentStack();
final var child = this.componentStack.pop(); if (componentStack.size() > 1) {
final var parent = this.componentStack.pop(); return componentStack.get(1);
this.componentStack.push(parent);
this.componentStack.push(child);
return parent;
} }
return null; return null;
} }
@ -139,35 +73,21 @@ public class DefaultComponentContext implements ComponentContext {
@Override @Override
public @Nullable ViewComponent findNearestAncestor(Predicate<? super ViewComponent> matching) { public @Nullable ViewComponent findNearestAncestor(Predicate<? super ViewComponent> matching) {
if (this.componentStack.size() > 1) { final List<ViewComponent> componentStack = this.getRenderContext().getComponentStack();
final Deque<ViewComponent> tmp = new LinkedList<>(); if (componentStack.size() > 1) {
tmp.push(this.componentStack.pop()); // child for (final var ancestor : componentStack.subList(1, componentStack.size() -1)) {
ViewComponent result = null;
while (result == null && !this.componentStack.isEmpty()) {
final var ancestor = this.componentStack.pop();
tmp.push(ancestor);
if (matching.test(ancestor)) { if (matching.test(ancestor)) {
result = ancestor; return ancestor;
} }
} }
while (!tmp.isEmpty()) {
this.componentStack.push(tmp.pop());
}
return result;
} }
return null; return null;
} }
@Override @Override
public List<ViewComponent> getAllAncestors() { public List<ViewComponent> getAllAncestors() {
if (this.componentStack.size() > 1) { final List<ViewComponent> componentStack = this.getRenderContext().getComponentStack();
final var child = this.componentStack.pop(); return componentStack.subList(1, componentStack.size());
final List<ViewComponent> result = new ArrayList<>(this.componentStack);
this.componentStack.push(child);
return result;
} else {
return List.of();
}
} }
} }

View File

@ -1,32 +1,65 @@
package groowt.view.component.context; package groowt.view.component.context;
import groowt.view.component.ViewComponent;
import groowt.view.component.factory.ComponentFactory; import groowt.view.component.factory.ComponentFactory;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects;
public class DefaultComponentScope implements ComponentScope { public class DefaultComponentScope implements ComponentScope {
private final Map<String, ComponentFactory<?>> factories = new HashMap<>(); private final Map<String, TypeAndFactory<?>> stringFactories = new HashMap<>();
private final Map<Class<?>, TypeAndFactory<?>> classFactories = new HashMap<>();
@Override @Override
public void add(String name, ComponentFactory<?> factory) { public <T extends ViewComponent> void add(String typeName, Class<T> forClass, ComponentFactory<? extends T> factory) {
this.factories.put(name, factory); this.stringFactories.put(typeName, new TypeAndFactory<>(forClass, factory));
} }
@Override @Override
public boolean contains(String name) { public boolean contains(String typeName) {
return this.factories.containsKey(name); return this.stringFactories.containsKey(typeName);
} }
@Override @Override
public void remove(String name) { public TypeAndFactory<?> get(String typeName) {
this.factories.remove(name); return Objects.requireNonNull(this.stringFactories.get(typeName));
} }
@Override @Override
public ComponentFactory<?> get(String name) { public void remove(String typeName) {
return this.factories.get(name); this.stringFactories.remove(typeName);
}
@Override
public <T extends ViewComponent> void add(Class<T> forClass, ComponentFactory<? extends T> factory) {
this.classFactories.put(forClass, new TypeAndFactory<>(forClass, factory));
}
@Override
public <T extends ViewComponent> void add(
Class<T> publicType,
Class<? extends T> implementingType,
ComponentFactory<? extends T> factory
) {
this.classFactories.put(publicType, new TypeAndFactory<T>(implementingType, factory));
}
@Override
public boolean contains(Class<? extends ViewComponent> type) {
return this.classFactories.containsKey(type);
}
@SuppressWarnings("unchecked")
@Override
public <T extends ViewComponent> TypeAndFactory<T> get(Class<T> type) {
return (TypeAndFactory<T>) Objects.requireNonNull(this.classFactories.get(type));
}
@Override
public void remove(Class<? extends ViewComponent> type) {
this.classFactories.remove(type);
} }
} }

View File

@ -0,0 +1,48 @@
package groowt.view.component.context;
import groowt.view.component.ViewComponent;
import org.jetbrains.annotations.Nullable;
public class FactoryMissingUnsupportedException extends ComponentResolveException {
private final String message;
public FactoryMissingUnsupportedException(@Nullable String message, String typeName) {
super(typeName);
this.message = message;
}
public FactoryMissingUnsupportedException(
@Nullable String message,
String typeName,
@Nullable Class<? extends ViewComponent> type
) {
super(typeName, type);
this.message = message;
}
public FactoryMissingUnsupportedException(@Nullable String message, String typeName, Throwable cause) {
super(typeName, cause);
this.message = message;
}
public FactoryMissingUnsupportedException(
@Nullable String message,
String typeName,
@Nullable Class<? extends ViewComponent> type,
Throwable cause
) {
super(typeName, type, cause);
this.message = message;
}
@Override
public String getMessage() {
if (this.message != null) {
return super.getMessage() + " " + this.message;
} else {
return super.getMessage();
}
}
}

View File

@ -1,19 +0,0 @@
package groowt.view.component.context;
import groowt.view.component.ComponentTemplate;
public class MissingClassTypeException extends MissingComponentException {
private final String typeName;
public MissingClassTypeException(ComponentTemplate template, String typeName, int line, int col, Throwable cause) {
super(template, cause, line, col);
this.typeName = typeName;
}
@Override
protected String getMissingKeyName() {
return "class component " + this.typeName;
}
}

View File

@ -1,31 +0,0 @@
package groowt.view.component.context;
import groowt.view.component.ComponentTemplate;
import groowt.view.component.context.ComponentContext;
/**
* 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;
private final int line;
private final int col;
public MissingComponentException(ComponentTemplate template, Throwable cause, int line, int col) {
super(cause);
this.template = template;
this.line = line;
this.col = col;
}
protected abstract String getMissingKeyName();
@Override
public String getMessage() {
return "In " + this.template + " missing " + this.getMissingKeyName()
+ " on line " + this.line + ", column " + this.col + ".";
}
}

View File

@ -1,16 +0,0 @@
package groowt.view.component.context;
import groowt.view.component.ComponentTemplate;
public class MissingFragmentTypeException extends MissingComponentException {
public MissingFragmentTypeException(ComponentTemplate template, int line, int col, Throwable cause) {
super(template, cause, line, col);
}
@Override
protected String getMissingKeyName() {
return "fragment type";
}
}

View File

@ -1,19 +0,0 @@
package groowt.view.component.context;
import groowt.view.component.ComponentTemplate;
public abstract class MissingStringTypeException extends MissingComponentException {
private final String keyName;
public MissingStringTypeException(ComponentTemplate template, String keyName, int line, int col, Throwable cause) {
super(template, cause, line, col);
this.keyName = keyName;
}
@Override
protected String getMissingKeyName() {
return "string-typed component " + this.keyName;
}
}

View File

@ -1,19 +0,0 @@
package groowt.view.component.context;
public class NoFactoryMissingException extends UnsupportedOperationException {
public NoFactoryMissingException() {}
public NoFactoryMissingException(String message) {
super(message);
}
public NoFactoryMissingException(String message, Throwable cause) {
super(message, cause);
}
public NoFactoryMissingException(Throwable cause) {
super(cause);
}
}

View File

@ -1,12 +1,13 @@
package groowt.view.component.factory; package groowt.view.component.factory;
import groovy.lang.Closure; import groovy.lang.Closure;
import groowt.view.component.context.ComponentContext;
import groowt.view.component.ViewComponent; import groowt.view.component.ViewComponent;
import groowt.view.component.context.ComponentContext;
import static groowt.view.component.factory.ComponentFactoryUtil.flatten; import static groowt.view.component.factory.ComponentFactoryUtil.flatten;
final class ClosureComponentFactory<T extends ViewComponent> implements ComponentFactory<T> { public abstract class AbstractClosureComponentFactory<T extends ViewComponent> extends Closure<T> implements
ComponentFactory<T> {
private enum Type { private enum Type {
ALL, ALL,
@ -15,18 +16,18 @@ final class ClosureComponentFactory<T extends ViewComponent> implements Componen
NONE NONE
} }
private final Closure<T> closure; private final Closure<? extends T> closure;
private final Type type; private final Type type;
@SuppressWarnings("unchecked") public AbstractClosureComponentFactory(Closure<? extends T> closure) {
public ClosureComponentFactory(Closure<? extends T> closure) { super(closure.getOwner(), closure.getThisObject());
this.closure = (Closure<T>) closure; this.closure = closure;
final var paramTypes = this.closure.getParameterTypes(); final var paramTypes = this.closure.getParameterTypes();
if (paramTypes.length == 0) { if (paramTypes.length == 0) {
this.type = Type.NONE; this.type = Type.NONE;
} else if (paramTypes.length == 1) { } else if (paramTypes.length == 1) {
final var paramType = paramTypes[0]; final var paramType = paramTypes[0];
if (paramType == String.class || paramType == Class.class) { if (paramType == String.class) {
this.type = Type.NAME_ONLY; this.type = Type.NAME_ONLY;
} else if (ComponentContext.class.isAssignableFrom(paramType)) { } else if (ComponentContext.class.isAssignableFrom(paramType)) {
this.type = Type.CONTEXT_ONLY; this.type = Type.CONTEXT_ONLY;
@ -36,7 +37,7 @@ final class ClosureComponentFactory<T extends ViewComponent> implements Componen
} else { } else {
final var firstParamType = paramTypes[0]; final var firstParamType = paramTypes[0];
final var secondParamType = paramTypes[1]; final var secondParamType = paramTypes[1];
if (firstParamType == String.class || firstParamType == Class.class) { if (firstParamType == String.class) {
if (ComponentContext.class.isAssignableFrom(secondParamType)) { if (ComponentContext.class.isAssignableFrom(secondParamType)) {
if (paramTypes.length > 2) { if (paramTypes.length > 2) {
this.type = Type.ALL; this.type = Type.ALL;
@ -54,31 +55,17 @@ final class ClosureComponentFactory<T extends ViewComponent> implements Componen
} }
} }
private T flatCall(Object... args) { protected T doCall(String typeNameOrAlias, ComponentContext componentContext, Object... args) {
return this.closure.call(flatten(args));
}
private T objTypeCreate(Object type, ComponentContext componentContext, Object... args) {
return switch (this.type) { return switch (this.type) {
case ALL -> this.flatCall(type, componentContext, args); case ALL -> this.closure.call(flatten(typeNameOrAlias, componentContext, args));
case NAME_AND_CONTEXT -> this.closure.call(type, componentContext); case NAME_AND_CONTEXT -> this.closure.call(typeNameOrAlias, componentContext);
case NAME_AND_ARGS -> this.flatCall(type, args); case NAME_AND_ARGS -> this.closure.call(flatten(typeNameOrAlias, args));
case CONTEXT_AND_ARGS -> this.flatCall(componentContext, args); case CONTEXT_AND_ARGS -> this.closure.call(flatten(componentContext, args));
case NAME_ONLY -> this.closure.call(type); case NAME_ONLY -> this.closure.call(typeNameOrAlias);
case CONTEXT_ONLY -> this.closure.call(componentContext); case CONTEXT_ONLY -> this.closure.call(componentContext);
case ARGS_ONLY -> this.closure.call(args); case ARGS_ONLY -> this.closure.call(args);
case NONE -> this.closure.call(); case NONE -> this.closure.call();
}; };
} }
@Override
public T create(String type, ComponentContext componentContext, Object... args) {
return this.objTypeCreate(type, componentContext, args);
}
@Override
public T create(Class<?> type, ComponentContext componentContext, Object... args) {
return this.objTypeCreate(type, componentContext, args);
}
} }

View File

@ -0,0 +1,34 @@
package groowt.view.component.factory;
import groovy.lang.Closure;
import groowt.view.component.ViewComponent;
import groowt.view.component.context.ComponentContext;
final class ClassTypeClosureComponentFactory<T extends ViewComponent> extends AbstractClosureComponentFactory<T> {
private final Class<T> forClass;
public ClassTypeClosureComponentFactory(Class<T> forClass, Closure<? extends T> closure) {
super(closure);
this.forClass = forClass;
}
@Override
public T create(String typeName, ComponentContext componentContext, Object... args) {
throw new UnsupportedOperationException(
"ClassTypeClosureComponentFactory cannot handle string component types."
);
}
@Override
public T create(String alias, Class<?> type, ComponentContext componentContext, Object... args) {
if (!this.forClass.isAssignableFrom(type)) {
throw new IllegalArgumentException(
"This ClassTypeClosureComponentFactory cannot handle type " + type.getName()
+ "; can only handle " + this.forClass.getName() + "."
);
}
return this.doCall(alias, componentContext, args);
}
}

View File

@ -0,0 +1,27 @@
package groowt.view.component.factory;
import groovy.lang.Closure;
import groowt.view.component.ViewComponent;
import java.util.function.Supplier;
public final class ComponentFactories {
public static <T extends ViewComponent> ComponentFactory<T> ofClosureStringType(Closure<? extends T> closure) {
return new StringTypeClosureComponentFactory<>(closure);
}
public static <T extends ViewComponent> ComponentFactory<T> ofClosureClassType(
Class<T> forClass,
Closure<? extends T> closure
) {
return new ClassTypeClosureComponentFactory<>(forClass, closure);
}
public static <T extends ViewComponent> ComponentFactory<T> ofSupplier(Supplier<? extends T> supplier) {
return (typeName, componentContext, args) -> supplier.get();
}
private ComponentFactories() {}
}

View File

@ -1,25 +1,14 @@
package groowt.view.component.factory; package groowt.view.component.factory;
import groovy.lang.Closure;
import groowt.view.component.context.ComponentContext;
import groowt.view.component.ViewComponent; import groowt.view.component.ViewComponent;
import groowt.view.component.context.ComponentContext;
import java.util.function.Supplier;
@FunctionalInterface @FunctionalInterface
public interface ComponentFactory<T extends ViewComponent> { public interface ComponentFactory<T extends ViewComponent> {
static <T extends ViewComponent> ComponentFactory<T> ofClosure(Closure<? extends T> closure) {
return new ClosureComponentFactory<>(closure);
}
static <T extends ViewComponent> ComponentFactory<T> ofSupplier(Supplier<T> supplier) {
return new SupplierComponentFactory<>(supplier);
}
T create(String typeName, ComponentContext componentContext, Object... args); T create(String typeName, ComponentContext componentContext, Object... args);
default T create(Class<?> type, ComponentContext componentContext, Object... args) { default T create(String alias, Class<?> type, ComponentContext componentContext, Object... args) {
return this.create(type.getName(), componentContext, args); return this.create(type.getName(), componentContext, args);
} }

View File

@ -94,13 +94,14 @@ public abstract class ComponentFactoryBase<T extends ViewComponent> extends Groo
} }
@Override @Override
public T create(String type, ComponentContext componentContext, Object... args) { public T create(String typeName, ComponentContext componentContext, Object... args) {
return this.findAndDoCreate(type, componentContext, args); throw new UnsupportedOperationException();
} }
// TODO: this needs to be updated.
@Override @Override
public T create(Class<?> type, ComponentContext componentContext, Object... args) { public T create(String alias, Class<?> type, ComponentContext componentContext, Object... args) {
return this.findAndDoCreate(type, componentContext, args); throw new UnsupportedOperationException();
} }
} }

View File

@ -1,82 +0,0 @@
package groowt.view.component.factory;
import java.io.*;
import java.net.URI;
import java.net.URL;
public sealed interface ComponentTemplateSource {
static ComponentTemplateSource of(String template) {
return new StringSource(template);
}
static ComponentTemplateSource of(File templateFile) {
return new FileSource(templateFile);
}
static ComponentTemplateSource of(URI templateURI) {
return new URISource(templateURI);
}
static ComponentTemplateSource of(URL url) {
return new URLSource(url);
}
static ComponentTemplateSource of(InputStream templateInputStream) {
return new InputStreamSource(templateInputStream);
}
static ComponentTemplateSource of(Reader templateReader) {
return new ReaderSource(templateReader);
}
/**
* @param resourceName An <strong>absolute</strong> path resource name.
* @return A template source
*/
static ComponentTemplateSource fromResource(String resourceName) {
return of(ComponentTemplateSource.class.getClassLoader().getResource(resourceName));
}
static Reader toReader(ComponentTemplateSource source) {
return switch (source) {
case FileSource(File file) -> {
try {
yield new FileReader(file);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
case StringSource(String rawSource) -> new StringReader(rawSource);
case InputStreamSource(InputStream inputStream) -> new InputStreamReader(inputStream);
case URISource(URI uri) -> {
try {
yield new InputStreamReader(uri.toURL().openStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
case URLSource(URL url) -> {
try {
yield new InputStreamReader(url.openStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
case ReaderSource(Reader reader) -> reader;
};
}
record StringSource(String template) implements ComponentTemplateSource {}
record FileSource(File templateFile) implements ComponentTemplateSource {}
record URISource(URI templateURI) implements ComponentTemplateSource {}
record URLSource(URL templateURL) implements ComponentTemplateSource {}
record InputStreamSource(InputStream templateInputStream) implements ComponentTemplateSource {}
record ReaderSource(Reader templateReader) implements ComponentTemplateSource {}
}

View File

@ -0,0 +1,25 @@
package groowt.view.component.factory;
import groovy.lang.Closure;
import groowt.view.component.ViewComponent;
import groowt.view.component.context.ComponentContext;
final class StringTypeClosureComponentFactory<T extends ViewComponent> extends AbstractClosureComponentFactory<T> {
public StringTypeClosureComponentFactory(Closure<? extends T> closure) {
super(closure);
}
@Override
public T create(String typeName, ComponentContext componentContext, Object... args) {
return this.doCall(typeName, componentContext, args);
}
@Override
public T create(String alias, Class<?> type, ComponentContext componentContext, Object... args) {
throw new UnsupportedOperationException(
"StringTypeClosureComponentFactory cannot handle class component types."
);
}
}

View File

@ -1,20 +0,0 @@
package groowt.view.component.factory;
import groowt.view.component.context.ComponentContext;
import groowt.view.component.ViewComponent;
import java.util.function.Supplier;
final class SupplierComponentFactory<T extends ViewComponent> extends ComponentFactoryBase<T> {
private final Supplier<T> tSupplier;
public SupplierComponentFactory(Supplier<T> tSupplier) {
this.tSupplier = tSupplier;
}
public T doCreate(Object type, ComponentContext componentContext, Object... args) {
return this.tSupplier.get();
}
}

View File

@ -0,0 +1,91 @@
package groowt.view.component.runtime;
import groowt.view.component.ComponentTemplate;
import groowt.view.component.runtime.RenderContext.ResolvedStringType;
import org.jetbrains.annotations.ApiStatus;
import java.util.Objects;
/**
* 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 RenderContext.Resolved<?> resolved;
private ComponentTemplate template;
private int line;
private int column;
public ComponentCreateException(
RenderContext.Resolved<?> resolved,
Throwable cause
) {
super(cause);
this.resolved = resolved;
}
public RenderContext.Resolved<?> getResolved() {
return this.resolved;
}
public ComponentTemplate getTemplate() {
return this.template;
}
@ApiStatus.Internal
public void setTemplate(ComponentTemplate template) {
this.template = Objects.requireNonNull(template);
}
public int getLine() {
return this.line;
}
@ApiStatus.Internal
public void setLine(int line) {
this.line = line;
}
public int getColumn() {
return this.column;
}
@ApiStatus.Internal
public void setColumn(int column) {
this.column = column;
}
@Override
public String getMessage() {
final var sb = new StringBuilder("Exception in ")
.append(this.template.getClass().getName());
if (this.resolved instanceof ResolvedStringType<?> resolvedStringType) {
sb.append(" while creating string-typed component ")
.append(resolvedStringType.typeName())
.append(" (using ")
.append(resolvedStringType.resolvedType().getName())
.append(") ");
} else if (this.resolved instanceof RenderContext.ResolvedClassType<?> resolvedClassType) {
sb.append(" while creating class-typed component ")
.append(resolvedClassType.alias())
.append(" of public type ")
.append(resolvedClassType.requestedType())
.append(" (using ")
.append(resolvedClassType.resolvedType())
.append(") ");
} else {
sb.append(" while creating unknown-typed component (using ")
.append(resolved.resolvedType().getName())
.append(") ");
}
return sb.append(" at line ")
.append(this.line)
.append(", column ")
.append(this.column)
.append(".")
.toString();
}
}

View File

@ -0,0 +1,32 @@
package groowt.view.component.runtime;
import groovy.lang.GString;
import groowt.view.component.ViewComponent;
import groowt.view.component.context.ComponentContext;
import org.jetbrains.annotations.ApiStatus;
public interface ComponentWriter {
@ApiStatus.Internal
void setRenderContext(RenderContext renderContext);
@ApiStatus.Internal
void setComponentContext(ComponentContext componentContext);
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);
default 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);
}
}
}

View File

@ -1,20 +1,42 @@
package groowt.view.web.runtime; package groowt.view.component.runtime;
import groovy.lang.GString; import groovy.lang.GString;
import groowt.view.component.ComponentRenderException; import groowt.view.component.ComponentRenderException;
import groowt.view.component.ViewComponent; import groowt.view.component.ViewComponent;
import groowt.view.component.context.ComponentContext;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.util.Objects;
public class DefaultWebViewComponentWriter implements WebViewComponentWriter { public class DefaultComponentWriter implements ComponentWriter {
private final Writer delegate; private final Writer delegate;
private RenderContext renderContext;
private ComponentContext componentContext;
public DefaultWebViewComponentWriter(Writer delegate) { public DefaultComponentWriter(Writer delegate) {
this.delegate = delegate; this.delegate = delegate;
} }
protected RenderContext getRenderContext() {
return Objects.requireNonNull(this.renderContext);
}
@Override
public void setRenderContext(RenderContext renderContext) {
this.renderContext = Objects.requireNonNull(renderContext);
}
protected ComponentContext getComponentContext() {
return this.componentContext;
}
@Override
public void setComponentContext(ComponentContext componentContext) {
this.componentContext = Objects.requireNonNull(componentContext);
}
@Override @Override
public void append(String string) { public void append(String string) {
try { try {
@ -50,12 +72,22 @@ public class DefaultWebViewComponentWriter implements WebViewComponentWriter {
} }
} }
private void doComponentRender(ViewComponent viewComponent) throws IOException {
this.getRenderContext().pushComponent(viewComponent);
this.getComponentContext().pushDefaultScope();
viewComponent.renderTo(this.delegate);
this.getComponentContext().popScope();
this.getRenderContext().popComponent(viewComponent);
}
@Override @Override
public void append(ViewComponent viewComponent) { public void append(ViewComponent viewComponent) {
try { try {
viewComponent.renderTo(this.delegate); this.doComponentRender(viewComponent);
} catch (IOException ioException) { } catch (IOException ioException) {
throw new RuntimeException(ioException); throw new RuntimeException(ioException);
} catch (ComponentRenderException componentRenderException) {
throw componentRenderException;
} catch (Exception exception) { } catch (Exception exception) {
throw new ComponentRenderException(viewComponent, exception); throw new ComponentRenderException(viewComponent, exception);
} }
@ -64,9 +96,11 @@ public class DefaultWebViewComponentWriter implements WebViewComponentWriter {
@Override @Override
public void append(ViewComponent viewComponent, int line, int column) { public void append(ViewComponent viewComponent, int line, int column) {
try { try {
viewComponent.renderTo(this.delegate); this.doComponentRender(viewComponent);
} catch (IOException ioException) { } catch (IOException ioException) {
throw new RuntimeException(ioException); throw new RuntimeException(ioException);
} catch (ComponentRenderException componentRenderException) {
throw componentRenderException;
} catch (Exception exception) { } catch (Exception exception) {
throw new ComponentRenderException(viewComponent, line, column, exception); throw new ComponentRenderException(viewComponent, line, column, exception);
} }
@ -74,20 +108,17 @@ public class DefaultWebViewComponentWriter implements WebViewComponentWriter {
@Override @Override
public void append(Object object) { public void append(Object object) {
switch (object) {
case String s -> this.append(s);
case GString gString -> this.append(gString);
case ViewComponent viewComponent -> this.append(viewComponent);
default -> {
try { try {
this.delegate.append(object.toString()); this.delegate.append(object.toString());
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(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);
} }
} }

View File

@ -0,0 +1,141 @@
package groowt.view.component.runtime;
import groowt.view.component.ViewComponent;
import groowt.view.component.ViewComponentBugError;
import groowt.view.component.context.ComponentContext;
import groowt.view.component.context.ComponentResolveException;
import groowt.view.component.context.ComponentScope.TypeAndFactory;
import java.util.LinkedList;
import java.util.List;
public class DefaultRenderContext implements RenderContext {
private final ComponentContext componentContext;
private final ComponentWriter writer;
private final LinkedList<ViewComponent> componentStack = new LinkedList<>();
public DefaultRenderContext(ComponentContext componentContext, ComponentWriter writer) {
this.componentContext = componentContext;
this.writer = writer;
}
public ComponentContext getComponentContext() {
return this.componentContext;
}
@Override
public Resolved<?> resolve(String typeName) throws ComponentResolveException {
for (final var scope : this.getComponentContext().getScopeStack()) {
if (scope.contains(typeName)) {
try {
final var typeAndFactory = scope.get(typeName);
return new ResolvedStringType<>(typeName, typeAndFactory.type(), typeAndFactory.factory());
} catch (Exception e) {
throw new ComponentResolveException(typeName, e);
}
}
}
Exception firstException = null;
for (final var scope : this.getComponentContext().getScopeStack()) {
try {
final var typeAndFactory = (TypeAndFactory<?>) scope.factoryMissing(typeName);
return new ResolvedStringType<>(typeName, typeAndFactory.type(), typeAndFactory.factory());
} catch (Exception e) {
if (firstException == null) {
firstException = e;
}
}
}
if (firstException != null) {
throw new ComponentResolveException(typeName, firstException);
} else {
throw new ViewComponentBugError(
"Could not resolve factory for " + typeName + " and firstException was null."
);
}
}
@Override
public <T extends ViewComponent> Resolved<T> resolve(String alias, Class<T> type) throws ComponentResolveException {
for (final var scope : this.getComponentContext().getScopeStack()) {
if (scope.contains(type)) {
try {
final var typeAndFactory = (TypeAndFactory<T>) scope.get(type);
return new ResolvedClassType<>(
alias,
type,
typeAndFactory.type(),
typeAndFactory.factory()
);
} catch (Exception e) {
throw new ComponentResolveException(alias, type, e);
}
}
}
throw new ComponentResolveException(
"Could not find a factory for " + alias + " of type " + type.getName() + " in scope.",
alias,
type
);
}
@Override
public ViewComponent create(Resolved<?> resolved, Object... args) {
final ViewComponent created;
if (resolved instanceof ResolvedStringType<?> resolvedStringType) {
try {
created = resolvedStringType.componentFactory().create(
resolvedStringType.typeName(),
this.getComponentContext(),
args
);
} catch (Exception createException) {
throw new ComponentCreateException(resolved, createException);
}
} else if (resolved instanceof ResolvedClassType<?> resolvedClassType) {
try {
created = resolvedClassType.componentFactory().create(
resolvedClassType.alias(),
resolvedClassType.resolvedType(),
this.getComponentContext(),
args
);
} catch (Exception createException) {
throw new ComponentCreateException(resolved, createException);
}
} else {
throw new UnsupportedOperationException(
this.getClass().getName() + " cannot handle Resolved of sub-type " + resolved.getClass().getName()
);
}
created.setContext(this.getComponentContext());
return created;
}
@Override
public void pushComponent(ViewComponent component) {
this.componentStack.push(component);
}
@Override
public void popComponent(ViewComponent component) {
final var popped = this.componentStack.pop();
if (!popped.equals(component)) {
throw new ViewComponentBugError(
"Popped component != expected component; popped: " + popped + ", expected: " + component + "."
);
}
}
@Override
public List<ViewComponent> getComponentStack() {
return new LinkedList<>(this.componentStack);
}
@Override
public ComponentWriter getWriter() {
return this.writer;
}
}

View File

@ -0,0 +1,43 @@
package groowt.view.component.runtime;
import groowt.view.component.ViewComponent;
import groowt.view.component.context.ComponentResolveException;
import groowt.view.component.factory.ComponentFactory;
import org.jetbrains.annotations.ApiStatus;
import java.util.List;
@ApiStatus.Internal
public interface RenderContext {
interface Resolved<T extends ViewComponent> {
Class<? extends T> resolvedType();
ComponentFactory<? extends T> componentFactory();
}
record ResolvedStringType<T extends ViewComponent>(
String typeName,
Class<? extends T> resolvedType,
ComponentFactory<? extends T> componentFactory
) implements Resolved<T> {}
record ResolvedClassType<T extends ViewComponent>(
String alias,
Class<T> requestedType,
Class<? extends T> resolvedType,
ComponentFactory<? extends T> componentFactory
) implements Resolved<T> {}
Resolved<?> resolve(String typeName) throws ComponentResolveException;
<T extends ViewComponent> Resolved<T> resolve(String alias, Class<T> type) throws ComponentResolveException;
ViewComponent create(Resolved<?> resolved, Object... args);
void pushComponent(ViewComponent component);
void popComponent(ViewComponent component);
List<ViewComponent> getComponentStack();
ComponentWriter getWriter();
}

View File

@ -4,6 +4,7 @@ import groowt.gradle.antlr.GroowtAntlrExecTask
plugins { plugins {
id 'groowt-conventions' id 'groowt-conventions'
id 'groowt-antlr-plugin' id 'groowt-antlr-plugin'
id 'groowt-logging'
id 'java-library' id 'java-library'
id 'groovy' id 'groovy'
id 'org.jetbrains.kotlin.jvm' id 'org.jetbrains.kotlin.jvm'
@ -19,13 +20,23 @@ configurations {
toolsImplementation { toolsImplementation {
extendsFrom(apiElements, runtimeElements) extendsFrom(apiElements, runtimeElements)
} }
sketchingImplementation {
extendsFrom(apiElements, runtimeElements)
}
} }
sourceSets { sourceSets {
sketching {
java {
compileClasspath += sourceSets.main.output
runtimeClasspath += sourceSets.main.output
}
}
tools { tools {
java { java {
compileClasspath += sourceSets.main.output compileClasspath += sourceSets.main.output
runtimeClasspath += sourceSets.main.output runtimeClasspath += sourceSets.main.output
runtimeClasspath += sourceSets.sketching.output
} }
} }
} }
@ -35,7 +46,6 @@ dependencies {
libs.groovy, libs.groovy,
libs.groovy.templates, libs.groovy.templates,
libs.antlr.runtime, libs.antlr.runtime,
libs.classgraph,
project(':view-components'), project(':view-components'),
project(':views') project(':views')
) )
@ -64,6 +74,8 @@ dependencies {
groovyConsole libs.groovy.console groovyConsole libs.groovy.console
toolsApi libs.picocli toolsApi libs.picocli
toolsImplementation libs.groovy.console toolsImplementation libs.groovy.console
sketchingApi libs.groovy
} }
java { java {

View File

@ -0,0 +1,130 @@
= Component Template Specification
== Compiled Component Template Code
The following code represents a typical (transpiled) component template:
[source, groovy]
----
package com.jessebrault.website
import groowt.view.component.ComponentTemplate
import groowt.view.component.context.ComponentContext
import groowt.view.component.runtime.*
import groowt.view.web.WebViewComponent
import groowt.view.web.lib.*
import groowt.view.web.runtime.*
class MyComponentTemplate implements ComponentTemplate {
Closure getRenderer() {
return { ComponentContext componentContext, ComponentWriter out ->
// <1>
final RenderContext renderContext = new DefaultWebViewComponentRenderContext(componentContext, out)
componentContext.setRenderContext(renderContext)
out.setRenderContext(renderContext)
out.setComponentContext(renderContext)
out.append 'Hello from simple text!' // <2>
out.append "Hello from GString!" // <3>
// <4>
ComponentContext.Resolved c0Resolved // <5>
try {
c0Resolved = renderContext.resolve('MySubComponent', MySubComponent) // <6>
} catch (ComponentResolveException c0ResolveException) { // <7>
c0ResolveException.template = this
c0ResolveException.line = 1
c0ResolveException.column = 1
throw c0ResolveException
}
WebViewComponent c0 // <8>
try {
c0 = renderContext.create( // <9>
c0Resolved,
[greeting: 'Hello, World!'],
"Some constructor arg",
WebViewComponentChildCollectorClosure.get(this) { c0cc ->
c0cc.add 'JString child.' // <10>
c0cc.add "GString child"
ComponentContext.Resolved c1Resolved
try {
c1Resolved = renderContext.resolve('h1') // <11>
} catch (ComponentResolveException c1ResolveException) {
c1ResolveException.type = IntrinsicHtml // <12>
c1ResolveException.template = this
c1ResolveException.line = 1
c1ResolveException.column = 10
throw c1ResolveException
}
WebViewComponent c1
try {
c1 = renderContext.create(
c1_resolved,
WebViewComponentChildCollectorClosure.get(this) { c1cc ->
c1cc.add "$greeting"
}
)
} catch (ComponentCreateException c1CreateException) {
c1CreateException.template = this
c1CreateException.line = 1
c1CreateException.column = 1
}
c0cc.add c1
}
)
} catch (ComponentCreateException c0CreateException) {
c0CreateException.template = this
c0CreateException.line = 1
c0CreateException.column = 1
throw c0CreateException
}
// append it
out.append c0
}
}
}
----
<1> Initialize the contexts.
<2> Appending a plain old java string (jstring) to `out`.
<3> Appending a GString to `out`.
<4> Now begins a component 'block', where a component is resolved, created, and either rendered or appended
to a child collector (see below).
<5> First, we define the `resolved` variable, of the type `ComponentContext.Resolved`.
<6> Resolve it from the context.
<7> If the context cannot resolve the component, it should throw a `ComponentResolveException`. We catch it
here in order to set the template (`this`), line, and column information for debugging purposes.
<8> Now we can start to create the component by first defining a variable for it.
<9> The create function takes a few things:
. The relevant instance of `ComponentContext.Resolved`.
. Any attributes for the component, passed as a `Map`.
. Any arguments from the component constructor.
. A `WebViewComponentChildCollectorClosure`, which is a 'marker' subclass of `Closure`
(so that constructor args ending with a closure don't get confused), which simply collects
the children of the component.
<10> For children, we add the value of the child, rather than appending it directly to `out`.
The collector itself will be given the `out` writer from the `context`, which will then
supply the parent with the ability to render to out. This way the parent doesn't ever have to worry about `out` itself
(though if the parent wants access to `out` (for example, it is using a non-wvc template) it can access the out writer
from the context).
<11> Here, a string type is passed, since all lowercase type names are treated as string types in web view components.
<12> Because this is an intrinsic html element, we set the type here.
<13> Finally, our child component is added as a `Closure` which accepts the component back again and appends
it to out.
== Items requiring Groovy ASTNode position adjustment
The following items all need to have their transpiled Groovy ASTNodes' positions adjusted to match the original source
file, in case there is a Groovy compilation error involved.
* JStrings
* GStrings
* Component types (Class and String expressions)
* Attribute keys and values
* Component constructor args

View File

@ -0,0 +1,4 @@
---
package groowt.view.web.sketching
---
<Greeters.Simple target='World' />

View File

@ -1 +1,11 @@
---
class Greeter extends BaseWebViewComponent {
String target
Greeter(Map attr) {
super('Hello, $target!')
this.target = attr.target
}
}
---
<Greeter target='World' /> <Greeter target='World' />

View File

@ -0,0 +1,46 @@
package groowt.view.web
import groowt.view.component.AbstractViewComponent
import groowt.view.component.ComponentTemplate
import groowt.view.component.compiler.ComponentTemplateCompileUnit
import groowt.view.component.compiler.source.ComponentTemplateSource
import java.util.function.Function
abstract class BaseWebViewComponent extends AbstractWebViewComponent {
BaseWebViewComponent() {}
BaseWebViewComponent(ComponentTemplate template) {
super(template)
}
BaseWebViewComponent(Class<? extends ComponentTemplate> templateClass) {
super(templateClass)
}
BaseWebViewComponent(
Function<? super Class<? extends AbstractViewComponent>, ComponentTemplateCompileUnit> compileUnitFunction
) {
super(compileUnitFunction)
}
BaseWebViewComponent(ComponentTemplateSource source) {
super(source)
}
/**
* A convenience constructor which creates a {@link ComponentTemplateSource}
* from the given {@code source} parameter and passes it to super. See
* {@link ComponentTemplateSource} for possible types.
*
* @param source the object passed to {@link ComponentTemplateSource#of}
*
* @see ComponentTemplateSource
*/
@SuppressWarnings('GroovyAssignabilityCheck')
BaseWebViewComponent(Object source) {
super(ComponentTemplateSource.of(source))
}
}

View File

@ -1,78 +0,0 @@
package groowt.view.web
import groowt.view.component.AbstractViewComponent
import groowt.view.component.ComponentTemplate
import groowt.view.component.compiler.ComponentTemplateCompiler
import groowt.view.component.factory.ComponentTemplateSource
import groowt.view.web.compiler.DefaultWebViewComponentTemplateCompiler
import groowt.view.web.compiler.WebViewComponentTemplateCompiler
import org.codehaus.groovy.control.CompilerConfiguration
import java.util.function.Function
abstract class DefaultWebViewComponent extends AbstractWebViewComponent {
private static final GroovyClassLoader groovyClassLoader =
new GroovyClassLoader(DefaultWebViewComponent.classLoader)
private static final Function<Class, ComponentTemplateCompiler> compilerFunction = { Class givenSelfClass ->
new DefaultWebViewComponentTemplateCompiler(
groovyClassLoader,
CompilerConfiguration.DEFAULT,
givenSelfClass.packageName
)
}
protected DefaultWebViewComponent() {}
protected DefaultWebViewComponent(ComponentTemplate template) {
super(template)
}
protected DefaultWebViewComponent(Class<? extends ComponentTemplate> templateType) {
super(templateType)
}
protected DefaultWebViewComponent(ComponentTemplateSource source) {
super(source, compilerFunction)
}
protected DefaultWebViewComponent(ComponentTemplateSource source, WebViewComponentTemplateCompiler compiler) {
super(source, compiler)
}
/**
* A convenience constructor which creates a {@link ComponentTemplateSource}
* from the given {@code source} parameter and passes it to super. See
* {@link ComponentTemplateSource} for possible types.
*
* @param source the object passed to {@link ComponentTemplateSource#of}
* @param compiler the compiler to use
*
* @see ComponentTemplateSource
*/
@SuppressWarnings('GroovyAssignabilityCheck')
protected DefaultWebViewComponent(Object source, WebViewComponentTemplateCompiler compiler) {
super(ComponentTemplateSource.of(source), compiler)
}
/**
* A convenience constructor which creates a {@link ComponentTemplateSource}
* from the given {@code source} parameter and passes it to super. See
* {@link ComponentTemplateSource} for possible types.
*
* @param source the object passed to {@link ComponentTemplateSource#of}
*
* @see ComponentTemplateSource
*/
@SuppressWarnings('GroovyAssignabilityCheck')
protected DefaultWebViewComponent(Object source) {
super(ComponentTemplateSource.of(source), compilerFunction)
}
@Override
protected final Class<? extends AbstractViewComponent> getSelfClass() {
this.class
}
}

View File

@ -2,26 +2,16 @@ package groowt.view.web
import groowt.view.component.context.ComponentScope import groowt.view.component.context.ComponentScope
import groowt.view.component.context.DefaultComponentContext import groowt.view.component.context.DefaultComponentContext
import groowt.view.component.ViewComponent
import groowt.view.web.lib.Fragment
import groowt.view.web.runtime.DefaultWebViewComponentChildCollection
import org.jetbrains.annotations.ApiStatus
class DefaultWebViewComponentContext extends DefaultComponentContext implements WebViewComponentContext { class DefaultWebViewComponentContext extends DefaultComponentContext implements WebViewComponentContext {
@Override DefaultWebViewComponentContext() {
protected ComponentScope getNewDefaultScope() { this.pushScope(WebViewComponentScope.getDefaultRootScope())
new WebViewScope()
} }
@Override @Override
@ApiStatus.Internal protected ComponentScope getNewDefaultScope() {
ViewComponent createFragment(Closure<?> childCollector) { new WebViewComponentScope()
def childCollection = new DefaultWebViewComponentChildCollection()
childCollector.call(childCollection)
def fragment = new Fragment()
fragment.childRenderers = childCollection.children
fragment
} }
} }

View File

@ -3,20 +3,42 @@ package groowt.view.web
import groovy.transform.stc.ClosureParams import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FromString import groovy.transform.stc.FromString
import groowt.view.component.factory.ComponentFactory import groowt.view.component.factory.ComponentFactory
import groowt.view.web.runtime.DefaultWebViewComponentChildCollector
import groowt.view.web.runtime.WebViewComponentChildCollector
import java.util.function.Function import static groowt.view.component.factory.ComponentFactories.ofClosureClassType
final class WebViewComponentFactories { final class WebViewComponentFactories {
static <T extends WebViewComponent> ComponentFactory<T> withAttr( static <T extends WebViewComponent> ComponentFactory<T> withAttr(
Class<T> forClass,
@ClosureParams(value = FromString, options = 'java.util.Map<String, Object>') @ClosureParams(value = FromString, options = 'java.util.Map<String, Object>')
Closure<T> closure Closure<? extends T> closure
) { ) {
ComponentFactory.ofClosure { Map<String, Object> attr -> closure(attr) } ofClosureClassType(forClass) { Map<String, Object> attr -> closure(attr) }
} }
static <T extends WebViewComponent> ComponentFactory<T> withAttr(Function<Map<String, Object>, T> tFunction) { static <T extends WebViewComponent> ComponentFactory<T> withChildren(
ComponentFactory.ofClosure { Map<String, Object> attr -> tFunction.apply(attr) } Class<T> forClass,
@ClosureParams(value = FromString, options = 'java.util.List<groowt.view.web.WebViewComponentChild>')
Closure<? extends T> closure
) {
ofClosureClassType(forClass) { WebViewComponentChildCollector childCollector ->
closure(childCollector.children)
}
}
static <T extends WebViewComponent> ComponentFactory<T> withAttrAndChildren(
Class<T> forClass,
@ClosureParams(
value = FromString,
options = 'java.util.Map<String, Object>, java.util.List<groowt.view.web.WebViewComponentChild>'
)
Closure<? extends T> closure
) {
ofClosureClassType(forClass) { Map<String, Object> attr, WebViewComponentChildCollector childCollector ->
closure(attr, childCollector.children)
}
} }
private WebViewComponentFactories() {} private WebViewComponentFactories() {}

View File

@ -0,0 +1,41 @@
package groowt.view.web
import groowt.view.component.context.DefaultComponentScope
import groowt.view.web.lib.Echo
import groowt.view.web.lib.IntrinsicHtml
import org.codehaus.groovy.runtime.InvokerHelper
import static groowt.view.web.WebViewComponentFactories.*
class WebViewComponentScope extends DefaultComponentScope {
static WebViewComponentScope getDefaultRootScope() {
new WebViewComponentScope().tap {
add(Echo, Echo.FACTORY)
}
}
<T extends WebViewComponent> void addWithAttr(Class<T> componentClass) {
add(componentClass, withAttr(componentClass) { attr ->
InvokerHelper.invokeConstructorOf(componentClass, attr) as T
})
}
<T extends WebViewComponent> void addWithChildren(Class<T> componentClass) {
add(componentClass, withChildren(componentClass) { children ->
InvokerHelper.invokeConstructorOf(componentClass, children) as T
})
}
<T extends WebViewComponent> void addWithAttrAndChildren(Class<T> componentClass) {
add(componentClass, withAttrAndChildren(componentClass) { attr, children ->
InvokerHelper.invokeConstructorOf(componentClass, [attr, children] as Object[]) as T
})
}
@Override
TypeAndFactory factoryMissing(String typeName) {
IntrinsicHtml.TYPE_AND_FACTORY
}
}

View File

@ -1,14 +0,0 @@
package groowt.view.web
import groowt.view.component.factory.ComponentFactory
import groowt.view.component.context.DefaultComponentScope
import groowt.view.web.lib.Echo
class WebViewScope extends DefaultComponentScope {
@Override
ComponentFactory factoryMissing(String typeName) {
Echo.FACTORY
}
}

View File

@ -1,23 +1,12 @@
package groowt.view.web.lib; package groowt.view.web.lib;
import groowt.view.View; import groowt.view.View;
import groowt.view.web.DefaultWebViewComponent; import groowt.view.web.BaseWebViewComponent;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.util.Map;
public abstract class DelegatingWebViewComponent extends DefaultWebViewComponent { public abstract class DelegatingWebViewComponent extends BaseWebViewComponent {
private final Map<String, Object> attr;
public DelegatingWebViewComponent(Map<String, Object> attr) {
this.attr = attr;
}
protected Map<String, Object> getAttr() {
return this.attr;
}
protected abstract View getDelegate(); protected abstract View getDelegate();

View File

@ -3,8 +3,7 @@ package groowt.view.web.lib
import groowt.view.View import groowt.view.View
import groowt.view.component.context.ComponentContext import groowt.view.component.context.ComponentContext
import groowt.view.component.factory.ComponentFactory import groowt.view.component.factory.ComponentFactory
import groowt.view.component.ComponentRenderException import groowt.view.web.runtime.WebViewComponentChildCollector
import groowt.view.web.WebViewChildComponentRenderer
class Echo extends DelegatingWebViewComponent { class Echo extends DelegatingWebViewComponent {
@ -12,95 +11,50 @@ class Echo extends DelegatingWebViewComponent {
protected static class EchoFactory implements ComponentFactory<Echo> { protected static class EchoFactory implements ComponentFactory<Echo> {
Echo doCreate(String typeName) { protected Echo doCreate() {
doCreate(typeName, [:], true) new Echo([:], [])
} }
Echo doCreate(String typeName, boolean selfClose) { protected Echo doCreate(Map attr) {
doCreate(typeName, [:], selfClose) new Echo(attr, [])
} }
Echo doCreate(String typeName, Map<String, Object> attr) { protected Echo doCreate(WebViewComponentChildCollector childCollector) {
doCreate(typeName, attr, true) new Echo([:], childCollector.children)
} }
Echo doCreate(String typeName, Map<String, Object> attr, boolean selfClose) { protected Echo doCreate(Map attr, WebViewComponentChildCollector childCollector) {
new Echo(attr, typeName, selfClose) new Echo(attr, childCollector.children)
}
Echo doCreate(
String typeName,
Map<String, Object> attr,
List<WebViewChildComponentRenderer> children
) {
def echo = new Echo(attr, typeName, false)
echo.childRenderers = children
echo
} }
@Override @Override
Echo create(String type, ComponentContext componentContext, Object... args) { Echo create(String typeName, ComponentContext componentContext, Object... args) {
this.doCreate(type, *args) as Echo throw new UnsupportedOperationException('Cannot create Echo for string type components')
} }
@Override @Override
Echo create(Class<?> type, ComponentContext componentContext, Object... args) { Echo create(String alias, Class<?> type, ComponentContext componentContext, Object... args) {
throw new UnsupportedOperationException('<Echo> can only be used with String types.') this.doCreate(*args)
} }
} }
String name Map attr
boolean selfClose
Echo(Map<String, Object> attr, String name, boolean selfClose) { Echo(Map attr, List children) {
super(attr) this.attr = attr
this.name = name this.children = children
this.selfClose = selfClose }
Object propertyMissing(String propertyName) {
attr[propertyName]
} }
@Override @Override
protected View getDelegate() { protected View getDelegate() {
if (this.selfClose && this.hasChildren()) {
throw new ComponentRenderException('Cannot have selfClose set to true and have children.')
}
return { return {
it << '<' this.children.each {
it << this.name it.render(this)
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 << '>'
}
}
}
protected void formatAttr(Writer writer) {
def iter = this.attr.iterator()
while (iter.hasNext()) {
def entry = iter.next()
writer << entry.key
def value = entry.value
if (value instanceof Boolean) {
// no-op, because we already wrote the key
} else {
writer << '="'
writer << value
writer << '"'
}
if (iter.hasNext()) {
writer << ' '
} }
} }
} }

View File

@ -1,8 +1,8 @@
package groowt.view.web.lib package groowt.view.web.lib
import groowt.view.web.DefaultWebViewComponent import groowt.view.web.BaseWebViewComponent
final class Fragment extends DefaultWebViewComponent { final class Fragment extends BaseWebViewComponent {
@Override @Override
void renderTo(Writer out) throws IOException { void renderTo(Writer out) throws IOException {

View File

@ -0,0 +1,6 @@
package groowt.view.web.lib
import groowt.view.web.BaseWebViewComponent
import groowt.view.web.util.ConfigurableComponent
class HtmlPage extends BaseWebViewComponent implements ConfigurableComponent<HtmlPage> {}

View File

@ -1,16 +1,29 @@
package groowt.view.web.lib package groowt.view.web.lib
import groowt.view.View
import groowt.view.component.ComponentRenderException
import groowt.view.component.context.ComponentContext import groowt.view.component.context.ComponentContext
import groowt.view.component.context.ComponentScope.TypeAndFactory
import groowt.view.component.factory.ComponentFactory import groowt.view.component.factory.ComponentFactory
import groowt.view.web.WebViewChildComponentRenderer import groowt.view.web.WebViewComponentChild
import groowt.view.web.util.WithHtml
class IntrinsicHtml extends Echo { class IntrinsicHtml extends DelegatingWebViewComponent implements WithHtml {
static final ComponentFactory<IntrinsicHtml> FACTORY = new IntrinsicHtmlFactory()
static final TypeAndFactory<IntrinsicHtml> TYPE_AND_FACTORY = new TypeAndFactory<>(IntrinsicHtml, FACTORY)
private static final Set<String> voidElements = Set.of(
'area', 'base', 'br', 'col',
'embed', 'hr', 'img', 'input',
'link', 'meta', 'param', 'source',
'track', 'wbr'
)
// TODO: check type name for HTML 5 validity
protected static class IntrinsicHtmlFactory implements ComponentFactory<IntrinsicHtml> { protected static class IntrinsicHtmlFactory implements ComponentFactory<IntrinsicHtml> {
IntrinsicHtml doCreate(String typeName) { IntrinsicHtml doCreate(String typeName) {
new IntrinsicHtml([:], typeName, false) new IntrinsicHtml([:], typeName, typeName in voidElements)
} }
IntrinsicHtml doCreate(String typeName, boolean selfClose) { IntrinsicHtml doCreate(String typeName, boolean selfClose) {
@ -18,16 +31,16 @@ class IntrinsicHtml extends Echo {
} }
IntrinsicHtml doCreate(String typeName, Map<String, Object> attr) { IntrinsicHtml doCreate(String typeName, Map<String, Object> attr) {
new IntrinsicHtml(attr, typeName, false) new IntrinsicHtml(attr, typeName, typeName in voidElements)
} }
IntrinsicHtml doCreate(String typeName, Map<String, Object> attr, boolean selfClose) { IntrinsicHtml doCreate(String typeName, Map<String, Object> attr, boolean selfClose) {
new IntrinsicHtml(attr, typeName, selfClose) new IntrinsicHtml(attr, typeName, selfClose)
} }
IntrinsicHtml doCreate(String typeName, Map<String, Object> attr, List<WebViewChildComponentRenderer> children) { IntrinsicHtml doCreate(String typeName, Map<String, Object> attr, List<WebViewComponentChild> children) {
def intrinsicHtml = new IntrinsicHtml(attr, typeName, false) def intrinsicHtml = new IntrinsicHtml(attr, typeName, typeName in voidElements)
intrinsicHtml.childRenderers = children intrinsicHtml.children = children
intrinsicHtml intrinsicHtml
} }
@ -37,14 +50,50 @@ class IntrinsicHtml extends Echo {
} }
@Override @Override
IntrinsicHtml create(Class<?> type, ComponentContext componentContext, Object... args) { IntrinsicHtml create(String alias, Class<?> type, ComponentContext componentContext, Object... args) {
throw new UnsupportedOperationException('Cannot create an IntrinsicHtml component with a class type.') throw new UnsupportedOperationException('Cannot create an IntrinsicHtml component with a class type.')
} }
} }
IntrinsicHtml(Map<String, Object> attr, String elementName, boolean selfClose) { Map attr
super(attr, elementName, selfClose) String name
boolean selfClose
IntrinsicHtml(Map attr, String elementName, boolean selfClose) {
this.attr = attr
this.name = elementName
this.selfClose = selfClose
}
@Override
protected View getDelegate() {
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 << ' '
this.formatAttr(it)
}
if (this.selfClose) {
it << ' /'
}
it << '>'
if (this.hasChildren()) {
this.children.each {
def renderer = it.getRenderer(this)
renderer.call(it.child)
}
}
if (this.hasChildren() || !this.selfClose) {
it << '</'
it << this.name
it << '>'
}
}
} }
} }

View File

@ -0,0 +1,27 @@
package groowt.view.web.util
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import groowt.view.web.WebViewComponent
import groowt.view.web.WebViewComponentContext
class ComponentConfigurator {
private final WebViewComponent self
ComponentConfigurator(WebViewComponent self) {
this.self = self
}
void context(
@DelegatesTo(ContextConfigurator)
@ClosureParams(value = SimpleType, options = 'groowt.view.web.WebViewComponentContext')
Closure configureContext
) {
//noinspection GroovyAssignabilityCheck
WebViewComponentContext context = self.context
configureContext.delegate = new ContextConfigurator(context)
configureContext(context)
}
}

View File

@ -0,0 +1,22 @@
package groowt.view.web.util
import groovy.transform.SelfType
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FromString
import groowt.view.web.WebViewComponent
@SelfType(WebViewComponent)
trait ConfigurableComponent<T extends WebViewComponent> {
@SuppressWarnings('GroovyAssignabilityCheck')
T configure(
@DelegatesTo(ComponentConfigurator)
@ClosureParams(value = FromString, options = 'T')
Closure configure
) {
configure.delegate = new ComponentConfigurator(this)
configure(this)
this
}
}

View File

@ -0,0 +1,27 @@
package groowt.view.web.util
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import groowt.view.web.WebViewComponentContext
import groowt.view.web.WebViewComponentScope
class ContextConfigurator {
private final WebViewComponentContext context
ContextConfigurator(WebViewComponentContext context) {
this.context = context
}
void rootScope(
@DelegatesTo(WebViewComponentScope)
@ClosureParams(value = SimpleType, options = 'groowt.view.web.WebViewComponentScope')
Closure configureRootScope
) {
//noinspection GroovyAssignabilityCheck
WebViewComponentScope rootScope = context.rootScope
configureRootScope.delegate = rootScope
configureRootScope(rootScope)
}
}

View File

@ -0,0 +1,30 @@
package groowt.view.web.util
trait WithHtml {
abstract Map getAttr()
/**
* @param writer A {@link java.io.Writer}, a {@link groowt.view.component.runtime.ComponentWriter},
* or anything which has {@code leftShift(String | Object)} as a method.
*/
void formatAttr(writer) {
def iter = attr.iterator()
while (iter.hasNext()) {
def entry = iter.next()
writer << entry.key
def value = entry.value
if (value instanceof Boolean) {
// no-op, because we already wrote the key
} else {
writer << '="'
writer << value
writer << '"'
}
if (iter.hasNext()) {
writer << ' '
}
}
}
}

View File

@ -3,11 +3,11 @@ package groowt.view.web;
import groovy.lang.Closure; import groovy.lang.Closure;
import groowt.view.component.AbstractViewComponent; import groowt.view.component.AbstractViewComponent;
import groowt.view.component.ComponentTemplate; import groowt.view.component.ComponentTemplate;
import groowt.view.component.compiler.ComponentTemplateCompiler; import groowt.view.component.compiler.ComponentTemplateCompileUnit;
import groowt.view.component.factory.ComponentTemplateSource; import groowt.view.component.compiler.source.ComponentTemplateSource;
import groowt.view.web.compiler.WebViewComponentTemplateCompiler; import groowt.view.component.runtime.ComponentWriter;
import groowt.view.web.runtime.DefaultWebViewComponentWriter; import groowt.view.component.runtime.DefaultComponentWriter;
import groowt.view.web.runtime.WebViewComponentWriter; import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
@ -17,7 +17,7 @@ import java.util.function.Function;
public abstract class AbstractWebViewComponent extends AbstractViewComponent implements WebViewComponent { public abstract class AbstractWebViewComponent extends AbstractViewComponent implements WebViewComponent {
private List<WebViewChildRenderer> childRenderers; private List<WebViewComponentChild> childRenderers;
public AbstractWebViewComponent() {} public AbstractWebViewComponent() {}
@ -29,20 +29,18 @@ public abstract class AbstractWebViewComponent extends AbstractViewComponent imp
super(templateClass); super(templateClass);
} }
protected AbstractWebViewComponent(ComponentTemplateSource source, WebViewComponentTemplateCompiler compiler) { public AbstractWebViewComponent(
super(source, compiler); Function<? super Class<? extends AbstractViewComponent>, ComponentTemplateCompileUnit> compileUnitFunction
) {
super(compileUnitFunction);
} }
@SuppressWarnings("unchecked") public AbstractWebViewComponent(ComponentTemplateSource source) {
protected AbstractWebViewComponent( this(selfClass -> new WebViewComponentTemplateCompileUnit(selfClass, source, selfClass.getPackageName()));
ComponentTemplateSource source,
Function<? super Class<? extends AbstractWebViewComponent>, ? extends ComponentTemplateCompiler> compilerFunction
) {
super(source, selfClass -> compilerFunction.apply((Class<AbstractWebViewComponent>) selfClass));
} }
@Override @Override
public List<WebViewChildRenderer> getChildRenderers() { public List<WebViewComponentChild> getChildren() {
if (this.childRenderers == null) { if (this.childRenderers == null) {
this.childRenderers = new ArrayList<>(); this.childRenderers = new ArrayList<>();
} }
@ -51,35 +49,28 @@ public abstract class AbstractWebViewComponent extends AbstractViewComponent imp
@Override @Override
public boolean hasChildren() { public boolean hasChildren() {
return !this.getChildRenderers().isEmpty(); return !this.getChildren().isEmpty();
} }
@Override @Override
public void setChildRenderers(List<WebViewChildRenderer> children) { public void setChildren(List<WebViewComponentChild> children) {
this.childRenderers = children; this.childRenderers = children;
} }
@Override @Override
public void renderChildren() { public void renderChildren() {
for (final var childRenderer : this.getChildRenderers()) { for (final var childRenderer : this.getChildren()) {
try { try {
if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) {
this.getContext().beforeComponentRender(childComponentRenderer.getComponent());
}
childRenderer.render(this); childRenderer.render(this);
} catch (Exception e) { } catch (Exception e) {
throw new ChildRenderException(e); throw new ChildRenderException(e);
} finally {
if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) {
this.getContext().afterComponentRender(childComponentRenderer.getComponent());
}
} }
} }
} }
@Override @Override
public void renderTo(Writer out) throws IOException { public void renderTo(Writer out) throws IOException {
final WebViewComponentWriter webWriter = new DefaultWebViewComponentWriter(out); final ComponentWriter webWriter = new DefaultComponentWriter(out);
final Closure<?> renderer = this.getTemplate().getRenderer(); final Closure<?> renderer = this.getTemplate().getRenderer();
renderer.setDelegate(this); renderer.setDelegate(this);
renderer.setResolveStrategy(Closure.DELEGATE_FIRST); renderer.setResolveStrategy(Closure.DELEGATE_FIRST);

View File

@ -1,19 +0,0 @@
package groowt.view.web;
import groovy.lang.Closure;
import groowt.view.component.ViewComponent;
public non-sealed class WebViewChildComponentRenderer extends WebViewChildRenderer {
private final ViewComponent component;
public WebViewChildComponentRenderer(ViewComponent component, Closure<Void> renderer) {
super(renderer);
this.component = component;
}
public ViewComponent getComponent() {
return this.component;
}
}

View File

@ -1,23 +0,0 @@
package groowt.view.web;
import groovy.lang.Closure;
import groovy.lang.GString;
public non-sealed class WebViewChildGStringRenderer extends WebViewChildRenderer {
private final GString gString;
public WebViewChildGStringRenderer(GString gString, Closure<Void> renderer) {
super(renderer);
this.gString = gString;
}
public GString getGString() {
return this.gString;
}
public String getContent() {
return this.gString.toString();
}
}

View File

@ -1,18 +0,0 @@
package groowt.view.web;
import groovy.lang.Closure;
public non-sealed class WebViewChildJStringRenderer extends WebViewChildRenderer {
private final String content;
public WebViewChildJStringRenderer(String content, Closure<Void> renderer) {
super(renderer);
this.content = content;
}
public String getContent() {
return this.content;
}
}

View File

@ -1,22 +0,0 @@
package groowt.view.web;
import groovy.lang.Closure;
import groowt.view.component.ViewComponent;
public sealed abstract class WebViewChildRenderer permits WebViewChildComponentRenderer,
WebViewChildGStringRenderer,
WebViewChildJStringRenderer {
private final Closure<Void> renderer;
public WebViewChildRenderer(Closure<Void> renderer) {
this.renderer = renderer;
}
public void render(ViewComponent parent) {
this.renderer.setDelegate(parent);
this.renderer.setResolveStrategy(Closure.DELEGATE_FIRST);
this.renderer.call();
}
}

View File

@ -1,24 +1,45 @@
package groowt.view.web; package groowt.view.web;
import groovy.lang.GString;
import groowt.view.component.ViewComponent; import groowt.view.component.ViewComponent;
import java.util.List; import java.util.List;
public interface WebViewComponent extends ViewComponent { public interface WebViewComponent extends ViewComponent {
List<WebViewChildRenderer> getChildRenderers(); List<WebViewComponentChild> getChildren();
boolean hasChildren(); boolean hasChildren();
void setChildRenderers(List<WebViewChildRenderer> children); void setChildren(List<WebViewComponentChild> children);
void renderChildren(); void renderChildren();
default List<Object> getChildren() { default List<String> getChildStrings() {
return this.getChildRenderers().stream() return this.getChildren().stream()
.map(childRenderer -> switch (childRenderer) { .map(WebViewComponentChild::getChild)
case WebViewChildComponentRenderer componentRenderer -> componentRenderer.getComponent(); .filter(obj -> obj instanceof String || obj instanceof GString)
case WebViewChildGStringRenderer gStringRenderer -> gStringRenderer.getGString(); .map(obj -> {
case WebViewChildJStringRenderer jStringRenderer -> jStringRenderer.getContent(); if (obj instanceof String s) {
return s;
} else {
return ((GString) obj).toString();
}
}) })
.toList(); .toList();
} }
default List<GString> getChildGStrings() {
return this.getChildren().stream()
.map(WebViewComponentChild::getChild)
.filter(GString.class::isInstance)
.map(GString.class::cast)
.toList();
}
default List<WebViewComponent> getChildComponents() {
return this.getChildren().stream()
.map(WebViewComponentChild::getChild)
.filter(WebViewComponent.class::isInstance)
.map(WebViewComponent.class::cast)
.toList();
}
} }

View File

@ -0,0 +1,31 @@
package groowt.view.web;
public class WebViewComponentBugError extends RuntimeException {
public WebViewComponentBugError(String message) {
super(message);
}
public WebViewComponentBugError(String message, Throwable cause) {
super(message, cause);
}
public WebViewComponentBugError(Throwable cause) {
super(cause);
}
public WebViewComponentBugError(
String message,
Throwable cause,
boolean enableSuppression,
boolean writableStackTrace
) {
super(message, cause, enableSuppression, writableStackTrace);
}
@Override
public String getMessage() {
return "BUG! Please file an issue at the github repository. " + super.getMessage();
}
}

View File

@ -0,0 +1,64 @@
package groowt.view.web;
import groovy.lang.Closure;
import groowt.view.component.ComponentTemplate;
import groowt.view.component.ViewComponent;
import groowt.view.component.runtime.ComponentWriter;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public class WebViewComponentChild {
private static final class ChildRenderClosure extends Closure<Void> {
private final ViewComponent parent;
private final @Nullable Object child;
public ChildRenderClosure(ComponentTemplate template, ViewComponent parent, @Nullable Object child) {
super(template, template);
this.parent = parent;
this.child = child;
this.setDelegate(this.parent);
this.setResolveStrategy(Closure.DELEGATE_FIRST);
}
public ViewComponent getParent() {
return this.parent;
}
public void doCall(ComponentWriter writer) {
writer.append(Objects.requireNonNull(this.child));
}
public void doCall(ComponentWriter writer, Object givenChild) {
writer.append(givenChild);
}
}
private final ComponentTemplate template;
private final ComponentWriter out;
private final Object child;
public WebViewComponentChild(ComponentTemplate template, ComponentWriter out, Object child) {
this.template = template;
this.out = out;
this.child = child;
}
public Object getChild() {
return this.child;
}
public void render(ViewComponent parent) {
final var cl = this.getRenderer(parent);
cl.call(this.child);
}
public Closure<Void> getRenderer(ViewComponent parent) {
final var cl = new ChildRenderClosure(this.template, parent, null);
return cl.curry(this.out);
}
}

View File

@ -1,16 +1,5 @@
package groowt.view.web; package groowt.view.web;
import groovy.lang.Closure;
import groowt.view.component.context.ComponentContext; import groowt.view.component.context.ComponentContext;
import groowt.view.component.ViewComponent;
import org.jetbrains.annotations.ApiStatus;
public interface WebViewComponentContext extends ComponentContext { public interface WebViewComponentContext extends ComponentContext {}
/**
* For use only by compiled web view component templates.
*/
@ApiStatus.Internal
ViewComponent createFragment(Closure<?> childCollector);
}

View File

@ -75,11 +75,11 @@ public abstract class AbstractWebViewComponentsLexer extends Lexer {
@Override @Override
public void pushMode(int m) { public void pushMode(int m) {
if (logger.isDebugEnabled()) { if (logger.isTraceEnabled()) {
final var old = this._mode; final var old = this._mode;
super.pushMode(m); super.pushMode(m);
final var delta = this._mode; final var delta = this._mode;
logger.debug("pushMode: target {} to {}", this.getModeName(old), this.getModeName(delta)); logger.trace("pushMode: target {} to {}", this.getModeName(old), this.getModeName(delta));
} else { } else {
super.pushMode(m); super.pushMode(m);
} }
@ -87,10 +87,10 @@ public abstract class AbstractWebViewComponentsLexer extends Lexer {
@Override @Override
public int popMode() { public int popMode() {
if (logger.isDebugEnabled()) { if (logger.isTraceEnabled()) {
final var popped = this._mode; final var popped = this._mode;
final var delta = super.popMode(); final var delta = super.popMode();
logger.debug("popMode: to {} target {}", this.getModeName(popped), this.getModeName(delta)); logger.trace("popMode: to {} target {}", this.getModeName(popped), this.getModeName(delta));
return popped; return popped;
} else { } else {
return super.popMode(); return super.popMode();

View File

@ -32,6 +32,16 @@ fun shortFormatToken(token: Token): String =
fun formatTokenText(text: String): String = excerptTokenParts(escapeTokenPartsToList(text)) fun formatTokenText(text: String): String = excerptTokenParts(escapeTokenPartsToList(text))
fun formatTokenPosition(token: Token): String {
return "line ${token.line}, column ${token.charPositionInLine + 1}"
}
fun excerptToken(token: Token) = excerptToken(token, 30, 7, "...")
fun excerptToken(token: Token, startLength: Int = 30, endLength: Int = 7, separator: String = "..."): String {
return excerptTokenParts(escapeTokenPartsToList(token.text), startLength, endLength, separator)
}
fun excerptTokenParts( fun excerptTokenParts(
parts: List<String>, parts: List<String>,
startLength: Int = 30, startLength: Int = 30,

View File

@ -25,6 +25,7 @@ public non-sealed class ClassComponentTypeNode extends ComponentTypeNode {
this.fqn = Objects.requireNonNull(fullyQualifiedName); this.fqn = Objects.requireNonNull(fullyQualifiedName);
} }
@Deprecated
public String getFullyQualifiedName() { public String getFullyQualifiedName() {
return Objects.requireNonNullElse(this.fqn, this.getIdentifier()); return Objects.requireNonNullElse(this.fqn, this.getIdentifier());
} }

View File

@ -0,0 +1,49 @@
package groowt.view.web.compiler;
import groowt.view.component.context.ComponentContext;
import groowt.view.web.WebViewComponentChild;
import groowt.view.web.WebViewComponent;
import org.jetbrains.annotations.ApiStatus;
import java.io.Writer;
import java.util.List;
@ApiStatus.Internal
public final class AnonymousWebViewComponent implements WebViewComponent {
@Override
public void setContext(ComponentContext context) {
throw new UnsupportedOperationException();
}
@Override
public ComponentContext getContext() {
throw new UnsupportedOperationException();
}
@Override
public void renderTo(Writer writer) {
throw new UnsupportedOperationException();
}
@Override
public List<WebViewComponentChild> getChildren() {
throw new UnsupportedOperationException();
}
@Override
public boolean hasChildren() {
throw new UnsupportedOperationException();
}
@Override
public void setChildren(List<WebViewComponentChild> children) {
throw new UnsupportedOperationException();
}
@Override
public void renderChildren() {
throw new UnsupportedOperationException();
}
}

View File

@ -1,201 +1,114 @@
package groowt.view.web.compiler; package groowt.view.web.compiler;
import groovy.lang.GroovyClassLoader; import groowt.view.component.compiler.*;
import groowt.view.component.AbstractViewComponent; import groowt.view.web.WebViewComponentBugError;
import groowt.view.component.ComponentTemplate;
import groowt.view.component.ViewComponent;
import groowt.view.component.compiler.CachingComponentTemplateCompiler;
import groowt.view.component.compiler.ComponentTemplateCompileErrorException;
import groowt.view.component.context.ComponentContext;
import groowt.view.component.factory.ComponentTemplateSource;
import groowt.view.web.analysis.MismatchedComponentTypeAnalysis; import groowt.view.web.analysis.MismatchedComponentTypeAnalysis;
import groowt.view.web.analysis.MismatchedComponentTypeError; import groowt.view.web.analysis.MismatchedComponentTypeError;
import groowt.view.web.antlr.AntlrUtil; import groowt.view.web.antlr.*;
import groowt.view.web.antlr.CompilationUnitParseResult;
import groowt.view.web.antlr.ParserUtil;
import groowt.view.web.antlr.TokenList;
import groowt.view.web.ast.DefaultAstBuilder; import groowt.view.web.ast.DefaultAstBuilder;
import groowt.view.web.ast.DefaultNodeFactory; import groowt.view.web.ast.DefaultNodeFactory;
import groowt.view.web.ast.node.CompilationUnitNode; import groowt.view.web.ast.node.CompilationUnitNode;
import groowt.view.web.transpile.DefaultGroovyTranspiler; import groowt.view.web.transpile.DefaultGroovyTranspiler;
import groowt.view.web.transpile.DefaultTranspilerConfiguration;
import groowt.view.web.util.SourcePosition;
import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.TerminalNode; import org.antlr.v4.runtime.tree.TerminalNode;
import org.antlr.v4.runtime.tree.Tree; import org.antlr.v4.runtime.tree.Tree;
import org.codehaus.groovy.control.CompilationFailedException; import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilationUnit; import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.Phases;
import org.codehaus.groovy.control.io.AbstractReaderSource;
import org.codehaus.groovy.tools.GroovyClass; import org.codehaus.groovy.tools.GroovyClass;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.Reader; import java.io.Reader;
import java.io.Writer; import java.util.HashSet;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set;
public class DefaultWebViewComponentTemplateCompiler extends CachingComponentTemplateCompiler public class DefaultWebViewComponentTemplateCompiler
extends CachingComponentTemplateCompiler<WebViewComponentTemplateCompileUnit>
implements WebViewComponentTemplateCompiler { implements WebViewComponentTemplateCompiler {
protected static final class AnonymousWebViewComponent extends AbstractViewComponent { private final ComponentTemplateCompilerConfiguration configuration;
// DO NOT INSTANTIATE, this is merely a marker class public DefaultWebViewComponentTemplateCompiler(ComponentTemplateCompilerConfiguration configuration) {
private AnonymousWebViewComponent() {
throw new UnsupportedOperationException();
}
@Override
public void setContext(ComponentContext context) {
throw new UnsupportedOperationException();
}
@Override
public ComponentContext getContext() {
throw new UnsupportedOperationException();
}
@Override
protected ComponentTemplate getTemplate() {
throw new UnsupportedOperationException();
}
@Override
protected void setTemplate(ComponentTemplate template) {
throw new UnsupportedOperationException();
}
@Override
protected void beforeRender() {
throw new UnsupportedOperationException();
}
@Override
protected void afterRender() {
throw new UnsupportedOperationException();
}
@Override
public void renderTo(Writer out) {
throw new UnsupportedOperationException();
}
@Override
protected Class<? extends AbstractViewComponent> getSelfClass() {
throw new UnsupportedOperationException();
}
}
private final CompilerConfiguration configuration;
private final String defaultPackageName;
private final int phase;
public DefaultWebViewComponentTemplateCompiler(
GroovyClassLoader groovyClassLoader,
CompilerConfiguration configuration,
String defaultPackageName
) {
this(groovyClassLoader, configuration, defaultPackageName, Phases.CLASS_GENERATION);
}
@ApiStatus.Internal
public DefaultWebViewComponentTemplateCompiler(
GroovyClassLoader groovyClassLoader,
CompilerConfiguration configuration,
String defaultPackageName,
int phase
) {
super(groovyClassLoader);
this.configuration = configuration; this.configuration = configuration;
this.defaultPackageName = defaultPackageName;
this.phase = phase;
} }
protected WebViewComponentTemplateCompileException getException( protected WebViewComponentTemplateCompileException getException(
TerminalNode terminalNode, WebViewComponentTemplateCompileUnit compileUnit,
Class<? extends ViewComponent> forClass, TerminalNode terminalNode
Reader reader
) { ) {
final Token offending = terminalNode.getSymbol(); final Token offending = terminalNode.getSymbol();
return new WebViewComponentTemplateCompileException( final var exception = new WebViewComponentTemplateCompileException(
"Compile error on token at " + SourcePosition.fromStartOfToken(offending).toStringLong() + ".", compileUnit, "Invalid token '" + TokenUtil.excerptToken(offending) + "'."
forClass,
reader,
offending
); );
exception.setTerminalNode(terminalNode);
return exception;
} }
protected WebViewComponentTemplateCompileException getException( protected WebViewComponentTemplateCompileException getException(
ParserRuleContext parserRuleContext, WebViewComponentTemplateCompileUnit compileUnit,
Class<? extends ViewComponent> forClass, ParserRuleContext parserRuleContext
Reader reader
) { ) {
return new WebViewComponentTemplateCompileException( final var exception = new WebViewComponentTemplateCompileException(
"Compile error at " + SourcePosition.fromStartOfToken(parserRuleContext.getStart()).toStringLong() compileUnit,
+ ".", "Parser error: " + parserRuleContext.exception.getMessage(),
forClass, parserRuleContext.exception
reader,
parserRuleContext
); );
exception.setParserRuleContext(parserRuleContext);
return exception;
} }
protected WebViewComponentTemplateCompileException mapToErrorException( protected WebViewComponentTemplateCompileException getException(
Tree tree, WebViewComponentTemplateCompileUnit compileUnit,
Class<? extends ViewComponent> forClass, Tree tree
Reader reader
) { ) {
if (tree instanceof TerminalNode terminalNode) { if (tree instanceof ParserRuleContext parserRuleContext) {
return getException(terminalNode, forClass, reader); return getException(compileUnit, parserRuleContext);
} else if (tree instanceof ParserRuleContext parserRuleContext) { } else if (tree instanceof TerminalNode terminalNode) {
return getException(parserRuleContext, forClass, reader); return getException(compileUnit, terminalNode);
} else { } else {
return new WebViewComponentTemplateCompileException( return new WebViewComponentTemplateCompileException(
"Compile error with " + tree + ".", compileUnit,
forClass, "Error at parser/lexer node " + tree.toString()
reader,
tree
); );
} }
} }
protected WebViewComponentTemplateCompileException mapToErrorException( protected WebViewComponentTemplateCompileException getException(
MismatchedComponentTypeError error, WebViewComponentTemplateCompileUnit compileUnit,
Class<? extends ViewComponent> forClass, MismatchedComponentTypeError error
Reader reader
) { ) {
return new WebViewComponentTemplateCompileException( final var exception = new WebViewComponentTemplateCompileException(
error.getMessage(), compileUnit,
forClass, error.getMessage()
reader,
error.getComponent()
); );
exception.setParserRuleContext(error.getComponent());
return exception;
} }
protected ComponentTemplateCompileResult doCompile( @Override
Class<? extends ViewComponent> forClass, protected ComponentTemplateCompileResult doCompile(WebViewComponentTemplateCompileUnit compileUnit)
Reader reader, throws ComponentTemplateCompileException {
@Nullable URI uri
) throws ComponentTemplateCompileErrorException { final Reader sourceReader;
final CompilationUnitParseResult parseResult = ParserUtil.parseCompilationUnit(reader); try {
sourceReader = compileUnit.getSource().toReader();
} catch (Exception e) {
throw new RuntimeException(e);
}
final CompilationUnitParseResult parseResult = ParserUtil.parseCompilationUnit(sourceReader);
// check for parser/lexer errors // check for parser/lexer errors
final var parseErrors = AntlrUtil.findErrorNodes(parseResult.getCompilationUnitContext()); final var parseErrors = AntlrUtil.findErrorNodes(parseResult.getCompilationUnitContext());
if (!parseErrors.isEmpty()) { if (!parseErrors.isEmpty()) {
if (parseErrors.getErrorCount() == 1) { if (parseErrors.getErrorCount() == 1) {
final var errorNode = parseErrors.getAll().getFirst(); final var errorNode = parseErrors.getAll().getFirst();
throw mapToErrorException(errorNode, forClass, reader); throw getException(compileUnit, errorNode);
} else { } else {
final var errorExceptions = parseErrors.getAll().stream() final var errorExceptions = parseErrors.getAll().stream()
.map(errorNode -> mapToErrorException(errorNode, forClass, reader)) .map(errorNode -> getException(compileUnit, errorNode))
.toList(); .toList();
throw new MultipleWebViewComponentCompileErrorsException(errorExceptions, forClass, reader); throw new MultipleWebViewComponentCompileErrorsException(compileUnit, errorExceptions);
} }
} }
@ -205,16 +118,12 @@ public class DefaultWebViewComponentTemplateCompiler extends CachingComponentTem
if (!mismatchedComponentTypeErrors.isEmpty()) { if (!mismatchedComponentTypeErrors.isEmpty()) {
if (mismatchedComponentTypeErrors.size() == 1) { if (mismatchedComponentTypeErrors.size() == 1) {
throw mapToErrorException(mismatchedComponentTypeErrors.getFirst(), forClass, reader); throw getException(compileUnit, mismatchedComponentTypeErrors.getFirst());
} else { } else {
final var errorExceptions = mismatchedComponentTypeErrors.stream() final var errorExceptions = mismatchedComponentTypeErrors.stream()
.map(error -> mapToErrorException(error, forClass, reader)) .map(error -> getException(compileUnit, error))
.toList(); .toList();
throw new MultipleWebViewComponentCompileErrorsException( throw new MultipleWebViewComponentCompileErrorsException(compileUnit, errorExceptions);
errorExceptions,
forClass,
reader
);
} }
} }
@ -224,63 +133,39 @@ public class DefaultWebViewComponentTemplateCompiler extends CachingComponentTem
final var cuNode = (CompilationUnitNode) astBuilder.build(parseResult.getCompilationUnitContext()); final var cuNode = (CompilationUnitNode) astBuilder.build(parseResult.getCompilationUnitContext());
// transpile to Groovy // transpile to Groovy
final var groovyCompilationUnit = new CompilationUnit(this.configuration); final var transpiler = new DefaultGroovyTranspiler();
final var transpiler = new DefaultGroovyTranspiler(
groovyCompilationUnit, final var ownerComponentName = compileUnit.getForClass() != AnonymousWebViewComponent.class
this.defaultPackageName, ? compileUnit.getForClass().getSimpleName()
DefaultTranspilerConfiguration::new : "AnonymousWebViewComponent" + System.nanoTime();
final var templateClassSimpleName = ownerComponentName + "Template";
final SourceUnit sourceUnit = transpiler.transpile(
this.configuration,
compileUnit,
cuNode,
templateClassSimpleName
); );
compileUnit.getGroovyCompilationUnit().addSource(sourceUnit);
final var ownerComponentName = forClass != null ? forClass.getSimpleName() : "AnonymousComponent" + System.nanoTime(); // compile groovy
final var templateClassName = ownerComponentName + "Template";
final var fqn = this.defaultPackageName + "." + templateClassName;
final var readerSource = new AbstractReaderSource(this.configuration) {
@Override
public Reader getReader() throws IOException {
reader.reset();
return reader;
}
@Override
public @Nullable URI getURI() {
return uri;
}
@Override
public void cleanup() {
super.cleanup();
try { try {
reader.close(); compileUnit.getGroovyCompilationUnit().compile(this.configuration.getToCompilePhase().getPhaseNumber());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
transpiler.transpile(cuNode, tokenList, ownerComponentName, readerSource);
try {
groovyCompilationUnit.compile(this.phase);
} catch (CompilationFailedException compilationFailedException) { } catch (CompilationFailedException compilationFailedException) {
throw new WebViewComponentTemplateCompileException( throw new WebViewComponentTemplateCompileException(
"Error while compiling Groovy in " + templateClassName + " for component class " + compileUnit,
forClass.getName() + ".", "Error while compiling Groovy.",
compilationFailedException, compilationFailedException
forClass,
forClass,
reader
); );
} }
// get the classes // get the classes
final var allClasses = groovyCompilationUnit.getClasses(); final var allClasses = compileUnit.getGroovyCompilationUnit().getClasses();
GroovyClass templateGroovyClass = null; GroovyClass templateGroovyClass = null;
final List<GroovyClass> otherClasses = new ArrayList<>(); final Set<GroovyClass> otherClasses = new HashSet<>();
final String templateClassFqn = sourceUnit.getAST().getPackageName() + "." + templateClassSimpleName;
for (final GroovyClass groovyClass : allClasses) { for (final GroovyClass groovyClass : allClasses) {
if (groovyClass.getName().equals(fqn)) { if (groovyClass.getName().equals(templateClassFqn)) {
if (templateGroovyClass != null) { if (templateGroovyClass != null) {
throw new IllegalStateException("Already found a templateGroovyClass."); throw new IllegalStateException("Already found a templateGroovyClass.");
} }
@ -291,40 +176,13 @@ public class DefaultWebViewComponentTemplateCompiler extends CachingComponentTem
} }
if (templateGroovyClass == null) { if (templateGroovyClass == null) {
throw new IllegalStateException("Did not find templateClass"); throw new WebViewComponentBugError(new IllegalStateException("Did not find templateClass"));
} }
return new ComponentTemplateCompileResult(templateGroovyClass, otherClasses); return new SimpleComponentTemplateCompileResult(
} templateGroovyClass,
otherClasses
@Override );
protected ComponentTemplateCompileResult doCompile(
@Nullable ComponentTemplateSource source,
Class<? extends ViewComponent> forClass,
Reader sourceReader
) throws ComponentTemplateCompileErrorException {
if (source instanceof ComponentTemplateSource.URISource uriSource) {
return this.doCompile(forClass, sourceReader, uriSource.templateURI());
} else if (source instanceof ComponentTemplateSource.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);
}
}
@Override
public ComponentTemplateCompileResult compileAnonymous(ComponentTemplateSource source)
throws ComponentTemplateCompileErrorException {
return this.compile(AnonymousWebViewComponent.class, source);
}
@Override
public ComponentTemplate compileAndGetAnonymous(ComponentTemplateSource source) throws ComponentTemplateCompileErrorException {
return this.compileAndGet(AnonymousWebViewComponent.class, source);
} }
} }

View File

@ -1,31 +1,22 @@
package groowt.view.web.compiler; package groowt.view.web.compiler;
import groowt.view.component.ViewComponent; import groowt.view.component.compiler.ComponentTemplateCompileException;
import groowt.view.component.compiler.ComponentTemplateCompileErrorException; import groowt.view.component.compiler.ComponentTemplateCompileUnit;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class MultipleWebViewComponentCompileErrorsException extends ComponentTemplateCompileErrorException { public class MultipleWebViewComponentCompileErrorsException extends ComponentTemplateCompileException {
private final List<Throwable> errors = new ArrayList<>(); private final List<Throwable> errors = new ArrayList<>();
public MultipleWebViewComponentCompileErrorsException( public MultipleWebViewComponentCompileErrorsException(
String message, ComponentTemplateCompileUnit compileUnit,
List<? extends Throwable> errors, List<? extends Throwable> errors
Class<? extends ViewComponent> forClass,
Object templateSource
) { ) {
super(message, forClass, templateSource); super(compileUnit, "There were multiple errors during compilation.");
this.errors.addAll(errors);
}
public MultipleWebViewComponentCompileErrorsException(
List<? extends Throwable> errors,
Class<? extends ViewComponent> forClass,
Object templateSource
) {
super(forClass, templateSource);
this.errors.addAll(errors); this.errors.addAll(errors);
} }
@ -33,4 +24,17 @@ public class MultipleWebViewComponentCompileErrorsException extends ComponentTem
return this.errors; return this.errors;
} }
@Override
public String getMessage() {
final var sw = new StringWriter();
sw.append(super.getMessage()).append("\n\n");
for (int i = 0; i < this.errors.size(); i++) {
final var error = this.errors.get(i);
sw.append(String.format("Error no. %d:\n", i + 1));
error.printStackTrace(new PrintWriter(sw));
sw.append("\n");
}
return sw.toString();
}
} }

View File

@ -1,56 +1,75 @@
package groowt.view.web.compiler; package groowt.view.web.compiler;
import groowt.view.component.ViewComponent; import groowt.view.component.compiler.ComponentTemplateCompileException;
import groowt.view.component.compiler.ComponentTemplateCompileErrorException; import groowt.view.component.compiler.ComponentTemplateCompileUnit;
import groowt.view.web.antlr.TokenUtil;
import groowt.view.web.ast.node.Node; import groowt.view.web.ast.node.Node;
import groowt.view.web.util.SourcePosition; import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.jetbrains.annotations.Nullable;
public class WebViewComponentTemplateCompileException extends ComponentTemplateCompileErrorException { public class WebViewComponentTemplateCompileException extends ComponentTemplateCompileException {
private final Object node; private @Nullable TerminalNode terminalNode;
private @Nullable ParserRuleContext parserRuleContext;
private @Nullable Node node;
public WebViewComponentTemplateCompileException( public WebViewComponentTemplateCompileException(
ComponentTemplateCompileUnit compileUnit,
String message
) {
super(compileUnit, message);
}
public WebViewComponentTemplateCompileException(
ComponentTemplateCompileUnit compileUnit,
String message, String message,
Class<? extends ViewComponent> forClass, Throwable cause
Object templateSource,
Object node
) { ) {
super(message, forClass, templateSource); super(compileUnit, message, cause);
this.node = node;
} }
public WebViewComponentTemplateCompileException( public WebViewComponentTemplateCompileException(
String message, ComponentTemplateCompileUnit compileUnit,
Throwable cause, Throwable cause
Class<? extends ViewComponent> forClass,
Object templateSource,
Object node
) { ) {
super(message, cause, forClass, templateSource); super(compileUnit, "There was an error during compilation.", cause);
this.node = node;
} }
public WebViewComponentTemplateCompileException( public @Nullable TerminalNode getTerminalNode() {
Throwable cause, return this.terminalNode;
Class<? extends ViewComponent> forClass,
Object templateSource,
Object node
) {
super(cause, forClass, templateSource);
this.node = node;
} }
public Object getNode() { public void setTerminalNode(@Nullable TerminalNode terminalNode) {
this.terminalNode = terminalNode;
}
public @Nullable ParserRuleContext getParserRuleContext() {
return this.parserRuleContext;
}
public void setParserRuleContext(@Nullable ParserRuleContext parserRuleContext) {
this.parserRuleContext = parserRuleContext;
}
public @Nullable Node getNode() {
return this.node; return this.node;
} }
public void setNode(@Nullable Node node) {
this.node = node;
}
@Override @Override
public String getMessage() { protected @Nullable String getPosition() {
if (this.node instanceof Node asNode) { if (this.node != null) {
final SourcePosition start = asNode.getTokenRange().getStartPosition(); return this.node.getTokenRange().getStartPosition().toStringLong();
return "At " + start.toStringLong() + ": " + super.getMessage(); } else if (this.parserRuleContext != null) {
return TokenUtil.formatTokenPosition(this.parserRuleContext.start);
} else if (this.terminalNode != null) {
return TokenUtil.formatTokenPosition(terminalNode.getSymbol());
} else { } else {
return super.getMessage(); return null;
} }
} }

View File

@ -0,0 +1,86 @@
package groowt.view.web.compiler;
import groowt.view.component.ViewComponent;
import groowt.view.component.compiler.AbstractComponentTemplateCompileUnit;
import groowt.view.component.compiler.ComponentTemplateCompileException;
import groowt.view.component.compiler.ComponentTemplateCompileResult;
import groowt.view.component.compiler.ComponentTemplateCompilerConfiguration;
import groowt.view.component.compiler.source.ComponentTemplateSource;
import groowt.view.component.compiler.source.FileSource;
import groowt.view.component.compiler.source.URISource;
import groowt.view.component.compiler.source.URLSource;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.Janitor;
import org.codehaus.groovy.control.io.ReaderSource;
import org.jetbrains.annotations.Nullable;
import java.io.Reader;
import java.net.URI;
public class WebViewComponentTemplateCompileUnit extends AbstractComponentTemplateCompileUnit implements ReaderSource {
private final String defaultPackageName;
private final CompilationUnit groovyCompilationUnit = new CompilationUnit();
public WebViewComponentTemplateCompileUnit(
Class<? extends ViewComponent> forClass,
ComponentTemplateSource source,
String defaultPackageName
) {
super(forClass, source);
this.defaultPackageName = defaultPackageName;
}
@Override
public String getDefaultPackageName() {
return this.defaultPackageName;
}
public CompilationUnit getGroovyCompilationUnit() {
return this.groovyCompilationUnit;
}
@Override
public ComponentTemplateCompileResult compile(ComponentTemplateCompilerConfiguration configuration)
throws ComponentTemplateCompileException {
final var compiler = new DefaultWebViewComponentTemplateCompiler(configuration);
return compiler.compile(this);
}
@Override
public Reader getReader() {
try {
return this.getSource().toReader();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public boolean canReopenSource() {
return this.getSource().canReopen();
}
@Override
public @Nullable String getLine(int lineNumber, Janitor janitor) {
if (this.getSource().canReopen()) {
return this.getSource().getLines().get(lineNumber);
} else {
return null;
}
}
@Override
public void cleanup() {}
@Override
public @Nullable URI getURI() {
return switch (this.getSource()) {
case FileSource fileSource -> fileSource.getURI();
case URISource uriSource -> uriSource.getURI();
case URLSource urlSource -> urlSource.getURI();
default -> null;
};
}
}

View File

@ -1,11 +1,20 @@
package groowt.view.web.compiler; package groowt.view.web.compiler;
import groowt.view.component.ComponentTemplate; import groowt.view.component.compiler.ComponentTemplateCompileException;
import groowt.view.component.compiler.ComponentTemplateCompileErrorException; import groowt.view.component.compiler.ComponentTemplateCompileResult;
import groowt.view.component.compiler.ComponentTemplateCompiler; import groowt.view.component.compiler.ComponentTemplateCompiler;
import groowt.view.component.factory.ComponentTemplateSource; import groowt.view.component.compiler.source.ComponentTemplateSource;
public interface WebViewComponentTemplateCompiler
extends ComponentTemplateCompiler<WebViewComponentTemplateCompileUnit> {
default ComponentTemplateCompileResult compileAnonymous(ComponentTemplateSource source, String packageName)
throws ComponentTemplateCompileException {
return this.compile(new WebViewComponentTemplateCompileUnit(
AnonymousWebViewComponent.class,
source,
packageName
));
}
public interface WebViewComponentTemplateCompiler extends ComponentTemplateCompiler {
ComponentTemplateCompileResult compileAnonymous(ComponentTemplateSource source) throws ComponentTemplateCompileErrorException;
ComponentTemplate compileAndGetAnonymous(ComponentTemplateSource source) throws ComponentTemplateCompileErrorException;
} }

View File

@ -1,38 +0,0 @@
package groowt.view.web.runtime;
import groovy.lang.Closure;
import groovy.lang.GString;
import groowt.view.component.ViewComponent;
import groowt.view.web.WebViewChildComponentRenderer;
import groowt.view.web.WebViewChildGStringRenderer;
import groowt.view.web.WebViewChildJStringRenderer;
import groowt.view.web.WebViewChildRenderer;
import java.util.ArrayList;
import java.util.List;
public class 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;
}
}

View File

@ -0,0 +1,43 @@
package groowt.view.web.runtime;
import groovy.lang.GString;
import groowt.view.component.ComponentTemplate;
import groowt.view.component.ViewComponent;
import groowt.view.component.runtime.ComponentWriter;
import groowt.view.web.WebViewComponentChild;
import java.util.ArrayList;
import java.util.List;
public class DefaultWebViewComponentChildCollector implements WebViewComponentChildCollector {
private final ComponentTemplate template;
private final ComponentWriter out;
private final List<WebViewComponentChild> children = new ArrayList<>();
public DefaultWebViewComponentChildCollector(ComponentTemplate template, ComponentWriter out) {
this.template = template;
this.out = out;
}
@Override
public void add(String jString) {
this.children.add(new WebViewComponentChild(this.template, this.out, jString));
}
@Override
public void add(GString gString) {
this.children.add(new WebViewComponentChild(this.template, this.out, gString));
}
@Override
public void add(ViewComponent component) {
this.children.add(new WebViewComponentChild(this.template, this.out, component));
}
@Override
public List<WebViewComponentChild> getChildren() {
return this.children;
}
}

View File

@ -0,0 +1,41 @@
package groowt.view.web.runtime;
import groowt.view.component.ViewComponent;
import groowt.view.component.context.ComponentContext;
import groowt.view.component.runtime.ComponentWriter;
import groowt.view.component.runtime.DefaultRenderContext;
import groowt.view.web.WebViewComponent;
import org.jetbrains.annotations.ApiStatus;
public class DefaultWebViewComponentRenderContext extends DefaultRenderContext
implements WebViewComponentRenderContext {
DefaultWebViewComponentRenderContext(ComponentContext componentContext, ComponentWriter writer) {
super(componentContext, writer);
}
@Override
public ViewComponent create(Resolved<?> resolved, Object... args) {
if (args != null && args.length > 0) {
final Object last = args[args.length - 1];
if (last instanceof WebViewComponentChildCollectorClosure cl) {
final var childCollector = new DefaultWebViewComponentChildCollector(
cl.getTemplate(),
this.getWriter()
);
args[args.length - 1] = childCollector;
cl.call(childCollector);
}
}
return super.create(resolved, args);
}
@ApiStatus.Internal
public ViewComponent createFragment(WebViewComponent fragment, WebViewComponentChildCollectorClosure cl) {
final var childCollection = new DefaultWebViewComponentChildCollector(cl.getTemplate(), this.getWriter());
cl.call(childCollection);
fragment.setChildren(childCollection.getChildren());
return fragment;
}
}

View File

@ -1,20 +0,0 @@
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();
}

View File

@ -0,0 +1,14 @@
package groowt.view.web.runtime;
import groovy.lang.GString;
import groowt.view.component.ViewComponent;
import groowt.view.web.WebViewComponentChild;
import java.util.List;
public interface WebViewComponentChildCollector {
void add(String jString);
void add(GString gString);
void add(ViewComponent component);
List<WebViewComponentChild> getChildren();
}

View File

@ -0,0 +1,29 @@
package groowt.view.web.runtime;
import groovy.lang.Closure;
import groowt.view.component.ComponentTemplate;
public class WebViewComponentChildCollectorClosure extends Closure<Object> {
public static Closure<?> get(ComponentTemplate template, Closure<?> collectorClosure) {
return new WebViewComponentChildCollectorClosure(template, collectorClosure);
}
private final ComponentTemplate template;
private final Closure<?> collectorClosure;
private WebViewComponentChildCollectorClosure(ComponentTemplate template, Closure<?> collectorClosure) {
super(template, template);
this.template = template;
this.collectorClosure = collectorClosure;
}
public ComponentTemplate getTemplate() {
return this.template;
}
public Object doCall(WebViewComponentChildCollector childCollector) {
return this.collectorClosure.call(childCollector);
}
}

View File

@ -0,0 +1,14 @@
package groowt.view.web.runtime;
import groowt.view.component.ViewComponent;
import groowt.view.component.runtime.RenderContext;
import groowt.view.web.WebViewComponent;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
public interface WebViewComponentRenderContext extends RenderContext {
@ApiStatus.Internal
ViewComponent createFragment(WebViewComponent fragment, WebViewComponentChildCollectorClosure cl);
}

View File

@ -1,14 +0,0 @@
package groowt.view.web.runtime;
import groovy.lang.GString;
import groowt.view.component.ViewComponent;
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);
}

View File

@ -17,4 +17,8 @@ public interface AppendOrAddStatementFactory {
Statement appendOnly(BodyChildNode sourceNode, TranspilerState state, Expression rightSide); Statement appendOnly(BodyChildNode sourceNode, TranspilerState state, Expression rightSide);
Statement addOrAppend(BodyChildNode sourceNode, TranspilerState state, Function<Action, Expression> getRightSide); Statement addOrAppend(BodyChildNode sourceNode, TranspilerState state, Function<Action, Expression> getRightSide);
default Statement addOrAppend(BodyChildNode sourceNode, TranspilerState state, Expression rightSide) {
return this.addOrAppend(sourceNode, state, ignored -> rightSide);
}
} }

View File

@ -3,9 +3,12 @@ package groowt.view.web.transpile;
import groowt.view.web.ast.node.ComponentNode; import groowt.view.web.ast.node.ComponentNode;
import groowt.view.web.transpile.TranspilerUtil.TranspilerState; import groowt.view.web.transpile.TranspilerUtil.TranspilerState;
import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import java.util.List;
public interface ComponentTranspiler { public interface ComponentTranspiler {
BlockStatement createComponentStatements( List<Statement> createComponentStatements(
ComponentNode componentNode, ComponentNode componentNode,
TranspilerState state TranspilerState state
); );

View File

@ -63,7 +63,7 @@ public class DefaultAppendOrAddStatementFactory implements AppendOrAddStatementF
return this.doCreate( return this.doCreate(
bodyChildNode, bodyChildNode,
rightSide, rightSide,
new VariableExpression(state.out()), new VariableExpression(state.getWriter()),
TranspilerUtil.APPEND, TranspilerUtil.APPEND,
true true
); );

View File

@ -1,5 +1,6 @@
package groowt.view.web.transpile; package groowt.view.web.transpile;
import groowt.view.web.WebViewComponentBugError;
import groowt.view.web.ast.node.*; import groowt.view.web.ast.node.*;
import groowt.view.web.transpile.TranspilerUtil.TranspilerState; import groowt.view.web.transpile.TranspilerUtil.TranspilerState;
import jakarta.inject.Inject; import jakarta.inject.Inject;
@ -30,7 +31,7 @@ public class DefaultBodyTranspiler implements BodyTranspiler {
TranspilerState state TranspilerState state
) { ) {
final BlockStatement block = new BlockStatement(); final BlockStatement block = new BlockStatement();
block.setVariableScope(state.currentScope()); block.setVariableScope(state.pushScope());
for (final Node child : bodyNode.getChildren()) { for (final Node child : bodyNode.getChildren()) {
switch (child) { switch (child) {
case GStringBodyTextNode gStringBodyTextNode -> { case GStringBodyTextNode gStringBodyTextNode -> {
@ -49,16 +50,17 @@ public class DefaultBodyTranspiler implements BodyTranspiler {
} }
case ComponentNode componentNode -> { case ComponentNode componentNode -> {
// DO NOT add/append this, because the component transpiler does it already // DO NOT add/append this, because the component transpiler does it already
block.addStatement(this.componentTranspiler.createComponentStatements(componentNode, state)); block.addStatements(this.componentTranspiler.createComponentStatements(componentNode, state));
} }
case PlainScriptletNode plainScriptletNode -> { case PlainScriptletNode plainScriptletNode -> {
throw new UnsupportedOperationException("TODO"); throw new UnsupportedOperationException("TODO");
} }
default -> throw new UnsupportedOperationException( default -> throw new WebViewComponentBugError(new UnsupportedOperationException(
"BodyNode child of type " + child.getClass().getSimpleName() + " is not supported." "BodyNode child of type " + child.getClass().getSimpleName() + " is not supported."
); ));
} }
} }
state.popScope();
return block; return block;
} }

View File

@ -1,106 +1,283 @@
package groowt.view.web.transpile; package groowt.view.web.transpile;
import groovy.lang.Tuple2; import groowt.view.component.context.ComponentResolveException;
import groowt.view.component.*; import groowt.view.component.runtime.ComponentCreateException;
import groowt.view.component.context.*; import groowt.view.component.runtime.RenderContext;
import groowt.view.web.WebViewComponentBugError;
import groowt.view.web.ast.node.*; import groowt.view.web.ast.node.*;
import groowt.view.web.runtime.WebViewComponentChildCollection; import groowt.view.web.runtime.WebViewComponentChildCollector;
import groowt.view.web.runtime.WebViewComponentChildCollectorClosure;
import groowt.view.web.transpile.resolve.ComponentClassNodeResolver; import groowt.view.web.transpile.resolve.ComponentClassNodeResolver;
import groowt.view.web.transpile.util.GroovyUtil; import groowt.view.web.transpile.util.GroovyUtil;
import groowt.view.web.transpile.util.GroovyUtil.ConvertResult; import groowt.view.web.transpile.util.GroovyUtil.ConvertResult;
import groowt.view.web.util.Provider;
import groowt.view.web.util.SourcePosition;
import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.*; import org.codehaus.groovy.ast.stmt.*;
import org.codehaus.groovy.syntax.Token;
import org.codehaus.groovy.syntax.Types;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.regex.Pattern;
import static groowt.view.web.transpile.TranspilerUtil.*; import static groowt.view.web.transpile.TranspilerUtil.*;
public class DefaultComponentTranspiler implements ComponentTranspiler { public class DefaultComponentTranspiler implements ComponentTranspiler {
private static final ClassNode VIEW_COMPONENT = ClassHelper.make(ViewComponent.class); private static final ClassNode CHILD_COLLECTOR_TYPE = ClassHelper.make(WebViewComponentChildCollector.class);
private static final ClassNode CHILD_COLLECTION = ClassHelper.make(WebViewComponentChildCollection.class); private static final ClassNode FRAGMENT_TYPE = ClassHelper.make(GROOWT_VIEW_WEB + ".lib.Fragment");
private static final ClassNode RESOLVED_TYPE = ClassHelper.make(RenderContext.Resolved.class);
private static final ClassNode CHILD_COLLECTOR_CLOSURE_TYPE =
ClassHelper.make(WebViewComponentChildCollectorClosure.class);
private static final ClassNode COMPONENT_RESOLVE_EXCEPTION_TYPE = ClassHelper.make(ComponentResolveException.class);
private static final ClassNode COMPONENT_CREATE_EXCEPTION_TYPE = ClassHelper.make(ComponentCreateException.class);
private static final ClassNode EXCEPTION = ClassHelper.make(Exception.class); private static final Pattern isFqn = Pattern.compile("^(\\p{Ll}.+\\.)+\\p{Lu}.+$");
private static final ClassNode COMPONENT_CREATE = ClassHelper.make(ComponentCreateException.class); private static final Pattern isWithPackage = Pattern.compile("^\\p{Ll}.+\\.");
private static final ClassNode NO_FACTORY_MISSING_EXCEPTION = ClassHelper.make(NoFactoryMissingException.class); private final Provider<AppendOrAddStatementFactory> appendOrAddStatementFactoryProvider;
private final Provider<ComponentClassNodeResolver> componentClassNodeResolverProvider;
private final Provider<ValueNodeTranspiler> valueNodeTranspilerProvider;
private final Provider<BodyTranspiler> bodyTranspilerProvider;
private static final ClassNode MISSING_COMPONENT_EXCEPTION = ClassHelper.make(MissingComponentException.class); public DefaultComponentTranspiler(
private static final ClassNode MISSING_CLASS_TYPE_EXCEPTION = ClassHelper.make(MissingClassTypeException.class); Provider<AppendOrAddStatementFactory> appendOrAddStatementFactoryProvider,
private static final ClassNode MISSING_STRING_TYPE_EXCEPTION = ClassHelper.make(MissingStringTypeException.class); Provider<ComponentClassNodeResolver> componentClassNodeResolverProvider,
private static final ClassNode MISSING_FRAGMENT_TYPE_EXCEPTION = Provider<ValueNodeTranspiler> valueNodeTranspilerProvider,
ClassHelper.make(MissingFragmentTypeException.class); Provider<BodyTranspiler> bodyTranspilerProvider
) {
private static final String CREATE = "create"; this.appendOrAddStatementFactoryProvider = appendOrAddStatementFactoryProvider;
private static final String CREATE_FRAGMENT = "createFragment"; this.componentClassNodeResolverProvider = componentClassNodeResolverProvider;
private static final String RESOLVE = "resolve"; this.valueNodeTranspilerProvider = valueNodeTranspilerProvider;
private static final String ADD = "add"; this.bodyTranspilerProvider = bodyTranspilerProvider;
private static final String APPEND = "append";
private static final String FRAGMENT_FQN = GROOWT_VIEW_WEB + ".lib.Fragment";
private ValueNodeTranspiler valueNodeTranspiler;
private BodyTranspiler bodyTranspiler;
private AppendOrAddStatementFactory appendOrAddStatementFactory;
private ComponentClassNodeResolver componentClassNodeResolver;
public void setValueNodeTranspiler(ValueNodeTranspiler valueNodeTranspiler) {
this.valueNodeTranspiler = valueNodeTranspiler;
} }
public void setBodyTranspiler(BodyTranspiler bodyTranspiler) { protected ValueNodeTranspiler getValueNodeTranspiler() {
this.bodyTranspiler = bodyTranspiler; return this.valueNodeTranspilerProvider.get();
} }
public void setAppendOrAddStatementFactory(AppendOrAddStatementFactory appendOrAddStatementFactory) { protected BodyTranspiler getBodyTranspiler() {
this.appendOrAddStatementFactory = appendOrAddStatementFactory; return this.bodyTranspilerProvider.get();
} }
public void setComponentClassNodeResolver(ComponentClassNodeResolver componentClassNodeResolver) { protected AppendOrAddStatementFactory getAppendOrAddStatementFactory() {
this.componentClassNodeResolver = componentClassNodeResolver; return this.appendOrAddStatementFactoryProvider.get();
} }
// ViewComponent c0 protected ComponentClassNodeResolver getComponentClassNodeResolver() {
protected ExpressionStatement getComponentDeclaration(Variable component) { return this.componentClassNodeResolverProvider.get();
final var componentDeclaration = new DeclarationExpression( }
new VariableExpression(component),
new Token(Types.ASSIGN, "=", -1, -1), /* UTIL */
private void addLineAndColumn(Node sourceNode, ArgumentListExpression args) {
final var lineAndColumn = lineAndColumn(sourceNode.getTokenRange().getStartPosition());
args.addExpression(lineAndColumn.getV1());
args.addExpression(lineAndColumn.getV2());
}
protected String getComponentName(int componentNumber) {
return "c" + componentNumber;
}
/* RESOLVED DECLARATION */
// RenderContext.Resolved c0Resolved
protected Statement getResolvedDeclaration(TranspilerState state) {
final ClassNode resolvedType = RESOLVED_TYPE.getPlainNodeReference();
resolvedType.setGenericsTypes(
new GenericsType[] { new GenericsType(WEB_VIEW_COMPONENT_TYPE) }
);
final var resolvedVariable = new VariableExpression(
this.getComponentName(state.newComponentNumber()) + "Resolved",
resolvedType
);
state.pushResolved(resolvedVariable);
final var declarationExpr = new DeclarationExpression(
resolvedVariable,
getAssignToken(),
EmptyExpression.INSTANCE EmptyExpression.INSTANCE
); );
return new ExpressionStatement(componentDeclaration); return new ExpressionStatement(declarationExpr);
} }
// 'ComponentName' /* RESOLVE */
protected ConstantExpression getComponentTypeNameExpression(ComponentNode componentNode) {
final String componentTypeName = switch (componentNode) { protected List<Expression> getArgsAsList(
case TypedComponentNode typedComponentNode -> switch (typedComponentNode.getArgs().getType()) { TypedComponentNode componentNode,
case ClassComponentTypeNode classComponentTypeNode -> classComponentTypeNode.getFullyQualifiedName(); TranspilerState state
case StringComponentTypeNode stringComponentTypeNode -> stringComponentTypeNode.getIdentifier(); ) {
return switch (componentNode.getArgs().getType()) {
case ClassComponentTypeNode classComponentTypeNode -> {
final String identifier = classComponentTypeNode.getIdentifier();
final ConstantExpression alias = getStringLiteral(identifier);
final var matcher = isFqn.matcher(identifier);
if (matcher.matches()) {
final ClassNode classNode = ClassHelper.make(identifier);
final ClassExpression classExpression = new ClassExpression(classNode);
yield List.of(alias, classExpression);
} else {
// we need to resolve it
final var isWithPackageMatcher = isWithPackage.matcher(identifier);
if (isWithPackageMatcher.matches()) {
final var resolveResult = this.getComponentClassNodeResolver().getClassForFqn(identifier);
if (resolveResult.isLeft()) {
final var error = resolveResult.getLeft();
error.setNode(componentNode.getArgs().getType());
state.addError(error);
yield List.of();
} else {
final ClassNode classNode = resolveResult.getRight();
final ClassExpression classExpression = new ClassExpression(classNode); // TODO: pos
yield List.of(alias, classExpression);
}
} else {
final var resolveResult =
this.getComponentClassNodeResolver().getClassForNameWithoutPackage(identifier);
if (resolveResult.isLeft()) {
final var error = resolveResult.getLeft();
error.setNode(componentNode.getArgs().getType());
state.addError(error);
yield List.of();
} else {
final ClassNode classNode = resolveResult.getRight();
final ClassExpression classExpression = new ClassExpression(classNode); // TODO: pos
yield List.of(alias, classExpression);
}
}
}
}
case StringComponentTypeNode stringComponentTypeNode -> {
final String identifier = stringComponentTypeNode.getIdentifier();
final ConstantExpression typeName = getStringLiteral(identifier);
yield List.of(typeName);
}
}; };
case FragmentComponentNode ignored -> FRAGMENT_FQN;
};
return makeStringLiteral(componentTypeName);
} }
// context.resolve('ComponentName') // 'h1' | 'MyComponent', MyComponent(.class)
protected MethodCallExpression getContextResolveExpr(ComponentNode componentNode, Variable componentContext) { protected ArgumentListExpression getResolveArgs(TypedComponentNode componentNode, TranspilerState state) {
final var args = new ArgumentListExpression(); final List<Expression> args = this.getArgsAsList(componentNode, state);
args.addExpression(this.getComponentTypeNameExpression(componentNode)); final ArgumentListExpression argsListExpr = new ArgumentListExpression();
return new MethodCallExpression(new VariableExpression(componentContext), RESOLVE, args); args.forEach(argsListExpr::addExpression);
return argsListExpr;
} }
// context.resolve('h1' | 'MyComponent', MyComponent.class)
protected MethodCallExpression getContextResolveExpr(
TypedComponentNode componentNode,
TranspilerState state
) {
return new MethodCallExpression(
new VariableExpression(state.getRenderContext()),
"resolve",
this.getResolveArgs(componentNode, state)
);
}
// context.resolve('h1' | 'MyComponent', MyComponent.class)
protected ExpressionStatement getContextResolveStmt(
TypedComponentNode componentNode,
TranspilerState state
) {
final BinaryExpression assignment = new BinaryExpression(
new VariableExpression(state.getCurrentResolved()),
getAssignToken(),
this.getContextResolveExpr(componentNode, state)
);
return new ExpressionStatement(assignment);
}
/* RESOLVE CATCH */
protected CatchStatement getResolveCatch(TypedComponentNode componentNode) {
final Parameter exceptionParameter = new Parameter(
COMPONENT_RESOLVE_EXCEPTION_TYPE,
"componentResolveException"
);
final VariableExpression exceptionVariable = new VariableExpression(exceptionParameter);
final VariableScope variableScope = new VariableScope();
variableScope.putDeclaredVariable(exceptionParameter);
final BinaryExpression setTemplateExpression = new BinaryExpression(
new PropertyExpression(exceptionVariable, new ConstantExpression("template")),
getAssignToken(),
VariableExpression.THIS_EXPRESSION
);
final Statement setTemplateStatement = new ExpressionStatement(setTemplateExpression);
final SourcePosition position = componentNode.getTokenRange().getStartPosition();
final BinaryExpression setLineExpression = new BinaryExpression(
new PropertyExpression(exceptionVariable, new ConstantExpression("line")),
getAssignToken(),
new ConstantExpression(position.line())
);
final Statement setLineStatement = new ExpressionStatement(setLineExpression);
final BinaryExpression setColumnExpression = new BinaryExpression(
new PropertyExpression(exceptionVariable, new ConstantExpression("column")),
getAssignToken(),
new ConstantExpression(position.column())
);
final Statement setColumnStatement = new ExpressionStatement(setColumnExpression);
final Statement throwStatement = new ThrowStatement(exceptionVariable);
final List<Statement> statements = new ArrayList<>();
statements.add(setTemplateStatement);
statements.add(setLineStatement);
statements.add(setColumnStatement);
statements.add(throwStatement);
final BlockStatement block = new BlockStatement(statements, variableScope);
return new CatchStatement(exceptionParameter, block);
}
/* RESOLVE BLOCK */
protected List<Statement> getResolveStatements(
TypedComponentNode componentNode,
TranspilerState state
) {
final Statement declaration = this.getResolvedDeclaration(state);
final Statement resolveStatement = this.getContextResolveStmt(componentNode, state);
final TryCatchStatement resolveTryCatch = new TryCatchStatement(resolveStatement, EmptyStatement.INSTANCE);
resolveTryCatch.addCatch(this.getResolveCatch(componentNode));
return List.of(declaration, resolveTryCatch);
}
/* TYPED COMPONENT DECLARATION */
// ViewComponent c0
protected Statement getTypedComponentDeclaration(TranspilerState state) {
final VariableExpression componentVariable = new VariableExpression(
this.getComponentName(state.getCurrentComponentNumber()), WEB_VIEW_COMPONENT_TYPE
);
state.pushComponent(componentVariable);
state.getCurrentScope().putDeclaredVariable(componentVariable);
final Expression declarationExpr = new DeclarationExpression(
componentVariable,
getAssignToken(),
EmptyExpression.INSTANCE
);
return new ExpressionStatement(declarationExpr);
}
/* TYPED COMPONENT CREATE: attributes map */
// key: value // key: value
protected MapEntryExpression getAttrExpression(AttrNode attrNode, TranspilerState state) { protected MapEntryExpression getAttrExpression(AttrNode attrNode, TranspilerState state) {
final var keyExpr = makeStringLiteral(attrNode.getKeyNode().getKey()); final ConstantExpression keyExpr = getStringLiteral(attrNode.getKeyNode().getKey()); // TODO: pos
final Expression valueExpr = switch (attrNode) { final Expression valueExpr = switch (attrNode) {
case BooleanValueAttrNode ignored -> ConstantExpression.PRIM_TRUE; case BooleanValueAttrNode ignored -> ConstantExpression.PRIM_TRUE;
case KeyValueAttrNode keyValueAttrNode -> case KeyValueAttrNode keyValueAttrNode ->
this.valueNodeTranspiler.createExpression(keyValueAttrNode.getValueNode(), state); this.getValueNodeTranspiler().createExpression(keyValueAttrNode.getValueNode(), state);
}; };
return new MapEntryExpression(keyExpr, valueExpr); return new MapEntryExpression(keyExpr, valueExpr);
} }
@ -108,7 +285,7 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
// [key: value, ...] // [key: value, ...]
protected MapExpression getAttrMap(List<AttrNode> attributeNodes, TranspilerState state) { protected MapExpression getAttrMap(List<AttrNode> attributeNodes, TranspilerState state) {
if (attributeNodes.isEmpty()) { if (attributeNodes.isEmpty()) {
throw new IllegalArgumentException("attributeNodes cannot be empty"); throw new WebViewComponentBugError(new IllegalArgumentException("attributeNodes cannot be empty."));
} }
final var result = new MapExpression(); final var result = new MapExpression();
attributeNodes.stream() attributeNodes.stream()
@ -117,343 +294,234 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
return result; return result;
} }
/* TYPED COMPONENT CREATE: component constructor */
// arg0, arg1, arg2, etc // arg0, arg1, arg2, etc
protected List<Expression> getConstructorArgs(ComponentConstructorNode componentConstructorNode) { protected List<Expression> getConstructorArgs(ComponentConstructorNode componentConstructorNode) {
final ConvertResult convertResult = GroovyUtil.convert(componentConstructorNode.getGroovyCode() final ConvertResult convertResult = GroovyUtil.convert(componentConstructorNode.getGroovyCode()
.getAsValidGroovyCode()); .getAsValidGroovyCode());
final var blockStatement = convertResult.blockStatement(); final BlockStatement blockStatement = convertResult.blockStatement();
if (blockStatement == null) { if (blockStatement == null) {
throw new IllegalStateException("Did not expect blockStatement to be null"); throw new WebViewComponentBugError(new IllegalStateException("Did not expect blockStatement to be null."));
} }
final var statements = blockStatement.getStatements(); final List<Statement> statements = blockStatement.getStatements();
if (statements.size() != 1) { if (statements.size() != 1) {
throw new IllegalStateException("statements size is not 1"); throw new WebViewComponentBugError(new IllegalStateException("statements.size() != 1"));
} }
final ExpressionStatement exprStmt = (ExpressionStatement) statements.getFirst(); final ExpressionStatement exprStmt = (ExpressionStatement) statements.getFirst();
final ListExpression listExpr = (ListExpression) exprStmt.getExpression(); final ListExpression listExpr = (ListExpression) exprStmt.getExpression();
return listExpr.getExpressions(); return listExpr.getExpressions(); // TODO: pos for each expression
} }
private void addLineAndColumn(Node sourceNode, ArgumentListExpression args) { /* COMPONENT CHILDREN */
final var lineAndColumn = lineAndColumn(sourceNode.getTokenRange().getStartPosition());
args.addExpression(lineAndColumn.getV1());
args.addExpression(lineAndColumn.getV2());
}
protected MethodCallExpression getOutCall(BodyChildNode sourceNode, TranspilerState state, Expression toOutput) { // c0cc.add (jString | gString | component)
final VariableExpression outVariableExpr = new VariableExpression(state.out()); protected Statement getChildCollectorAdd(Variable childCollector, Expression toAdd) {
final ArgumentListExpression args = new ArgumentListExpression(); final VariableExpression childCollectorVariableExpr = new VariableExpression(childCollector);
args.addExpression(toOutput);
switch (sourceNode) {
case GStringBodyTextNode ignored -> this.addLineAndColumn(sourceNode.asNode(), args);
case ComponentNode ignored -> this.addLineAndColumn(sourceNode.asNode(), args);
default -> {
}
}
return new MethodCallExpression(outVariableExpr, APPEND, args);
}
// { out << jString | gString | component }
protected ClosureExpression getOutClosure(BodyChildNode sourceNode, TranspilerState state, Expression toRender) {
if (toRender instanceof VariableExpression variableExpression) {
variableExpression.setClosureSharedVariable(true);
}
final Statement stmt = new ExpressionStatement(this.getOutCall(sourceNode, state, toRender));
return new ClosureExpression(Parameter.EMPTY_ARRAY, stmt);
}
// c0_childCollector.add (jString | gString | component) { out << ... }
protected Statement getChildCollectorAdd(
BodyChildNode sourceNode,
TranspilerState state,
Variable childCollector,
Expression toAdd
) {
final var childCollectorVariableExpr = new VariableExpression(childCollector);
final ClosureExpression renderChild = this.getOutClosure(sourceNode, state, toAdd);
final MethodCallExpression methodCall = new MethodCallExpression( final MethodCallExpression methodCall = new MethodCallExpression(
childCollectorVariableExpr, childCollectorVariableExpr,
ADD, "add",
new ArgumentListExpression(List.of(toAdd, renderChild)) new ArgumentListExpression(List.of(toAdd))
); );
return new ExpressionStatement(methodCall); return new ExpressionStatement(methodCall);
} }
/** // { WebViewComponentChildCollector c0cc -> ... }
* @return Tuple containing 1. body ClosureExpression, and 2. childCollector Variable protected ClosureExpression getChildCollectorClosure(
*/
// { WebViewComponentChildCollector c0_childCollector -> ... }
protected Tuple2<ClosureExpression, Variable> getBodyClosure(
BodyNode bodyNode, BodyNode bodyNode,
TranspilerState state, TranspilerState state
String componentVariableName
) { ) {
final Parameter childCollectorParam = new Parameter( final Parameter ccParam = new Parameter(
CHILD_COLLECTION, CHILD_COLLECTOR_TYPE,
componentVariableName + "_childCollector" this.getComponentName(state.getCurrentComponentNumber()) + "cc"
); );
final var scope = state.pushScope(); final var scope = state.pushScope();
scope.putDeclaredVariable(childCollectorParam); scope.putDeclaredVariable(ccParam);
state.pushChildCollector(childCollectorParam); state.pushChildCollector(ccParam);
final BlockStatement bodyStatements = this.bodyTranspiler.transpileBody(
final BlockStatement bodyStatements = this.getBodyTranspiler().transpileBody(
bodyNode, bodyNode,
(sourceNode, expr) -> this.getChildCollectorAdd(sourceNode, state, childCollectorParam, expr), (sourceNode, expr) -> this.getChildCollectorAdd(ccParam, expr),
state state
); );
// clean up
state.popChildCollector(); state.popChildCollector();
state.popScope(); state.popScope();
final ClosureExpression bodyClosure = new ClosureExpression( return new ClosureExpression(
new Parameter[] { childCollectorParam }, new Parameter[] { ccParam },
bodyStatements bodyStatements
); );
return new Tuple2<>(bodyClosure, childCollectorParam);
} }
/** protected StaticMethodCallExpression getChildCollectorGetter(
* @return Tuple containing 1. create Expression, BodyNode bodyNode,
* and 2. childCollector Variable, possibly {@code null}. TranspilerState state
*/ ) {
final ArgumentListExpression args = new ArgumentListExpression();
args.addExpression(VariableExpression.THIS_EXPRESSION);
args.addExpression(this.getChildCollectorClosure(bodyNode, state));
return new StaticMethodCallExpression(
CHILD_COLLECTOR_CLOSURE_TYPE,
"get",
args
);
}
/* TYPED COMPONENT CREATE: expression and statement */
// context.create(...) {...} // context.create(...) {...}
protected Tuple2<MethodCallExpression, @Nullable Variable> getCreateExpression( protected MethodCallExpression getTypedComponentCreateExpression(
ComponentNode componentNode, TypedComponentNode componentNode,
TranspilerState state, TranspilerState state
String componentVariableName
) { ) {
final var createArgs = new ArgumentListExpression(); final var createArgs = new ArgumentListExpression();
final String createName;
Variable childCollector = null;
if (componentNode instanceof TypedComponentNode typedComponentNode) {
createName = CREATE;
final var contextResolve = this.getContextResolveExpr(componentNode, state.context());
createArgs.addExpression(contextResolve);
final List<AttrNode> attributeNodes = typedComponentNode.getArgs().getAttributes(); createArgs.addExpression(new VariableExpression(state.getCurrentResolved()));
final List<AttrNode> attributeNodes = componentNode.getArgs().getAttributes();
if (!attributeNodes.isEmpty()) { if (!attributeNodes.isEmpty()) {
createArgs.addExpression(this.getAttrMap(attributeNodes, state)); createArgs.addExpression(this.getAttrMap(attributeNodes, state));
} }
final ComponentConstructorNode constructorNode = typedComponentNode.getArgs().getConstructor(); final ComponentConstructorNode constructorNode = componentNode.getArgs().getConstructor();
if (constructorNode != null) { if (constructorNode != null) {
this.getConstructorArgs(constructorNode).forEach(createArgs::addExpression); this.getConstructorArgs(constructorNode).forEach(createArgs::addExpression);
} }
final @Nullable BodyNode bodyNode = componentNode.getBody(); final @Nullable BodyNode bodyNode = componentNode.getBody();
if (bodyNode != null) { if (bodyNode != null) {
final var bodyResult = this.getBodyClosure(bodyNode, state, componentVariableName); createArgs.addExpression(this.getChildCollectorGetter(bodyNode, state));
childCollector = bodyResult.getV2();
createArgs.addExpression(bodyResult.getV1());
}
} else if (componentNode instanceof FragmentComponentNode fragmentComponentNode) {
createName = CREATE_FRAGMENT;
final BodyNode bodyNode = Objects.requireNonNull(
fragmentComponentNode.getBody(),
"FragmentComponentNode cannot have a null body."
);
final var bodyResult = this.getBodyClosure(bodyNode, state, componentVariableName);
childCollector = bodyResult.getV2();
createArgs.addExpression(bodyResult.getV1());
} else {
throw new IllegalArgumentException("Unsupported ComponentNode type: " + componentNode.getClass().getName());
} }
final var createCall = new MethodCallExpression( return new MethodCallExpression(new VariableExpression(state.getRenderContext()), "create", createArgs);
new VariableExpression(state.context()),
createName,
createArgs
);
return new Tuple2<>(createCall, childCollector);
} }
/**
* @return Tuple containing 1. assignment ExpressionStatement,
* and 2. childCollector Variable, possibly {@code null}.
*/
// c0 = context.create(context.resolve(''), [:], ...) {...} // c0 = context.create(context.resolve(''), [:], ...) {...}
protected Tuple2<ExpressionStatement, @Nullable Variable> getCreateAssignStatement( protected ExpressionStatement getTypedComponentCreateStatement(
ComponentNode componentNode, TypedComponentNode componentNode,
TranspilerState state, TranspilerState state
String componentVariableName,
Variable component
) { ) {
final var componentAssignLeft = new VariableExpression(component); final var left = new VariableExpression(state.getCurrentComponent());
final var createExprResult = this.getCreateExpression(componentNode, state, componentVariableName); final var right = this.getTypedComponentCreateExpression(componentNode, state);
final var componentAssignExpr = new BinaryExpression( final var componentAssignExpr = new BinaryExpression(left, getAssignToken(), right);
componentAssignLeft, return new ExpressionStatement(componentAssignExpr);
new Token(Types.ASSIGN, "=", -1, -1),
createExprResult.getV1()
);
return new Tuple2<>(new ExpressionStatement(componentAssignExpr), createExprResult.getV2());
} }
// catch (NoFactoryMissingException c0nfme) { /* CREATE CATCH */
// throw new MissingClassComponentException(this, 'ComponentType', c0nfme)
// }
protected CatchStatement getNoMissingFactoryExceptionCatch(
ComponentNode componentNode,
String componentVariableName
) {
final String exceptionName = componentVariableName + "nfme";
final Parameter fmeParam = new Parameter(NO_FACTORY_MISSING_EXCEPTION, exceptionName);
final VariableExpression fmeVar = new VariableExpression(exceptionName);
final var lineAndColumn = lineAndColumn(componentNode.getTokenRange().getStartPosition()); // catch (ComponentCreateException c0CreateException) { ... }
final ConstantExpression line = lineAndColumn.getV1(); protected CatchStatement getTypedCreateCatch(TypedComponentNode componentNode, TranspilerState state) {
final ConstantExpression column = lineAndColumn.getV2(); final String exceptionName = this.getComponentName(state.getCurrentComponentNumber()) + "CreateException";
final Parameter exceptionParam = new Parameter(COMPONENT_CREATE_EXCEPTION_TYPE, exceptionName);
final ConstructorCallExpression mcceConstructorExpr = switch (componentNode) {
case TypedComponentNode typedComponentNode -> switch (typedComponentNode.getArgs().getType()) {
case StringComponentTypeNode stringComponentTypeNode ->
new ConstructorCallExpression(MISSING_STRING_TYPE_EXCEPTION, new ArgumentListExpression(List.of(
VariableExpression.THIS_EXPRESSION,
makeStringLiteral(stringComponentTypeNode.getIdentifier()),
line,
column,
fmeVar
)));
case ClassComponentTypeNode classComponentTypeNode ->
new ConstructorCallExpression(MISSING_CLASS_TYPE_EXCEPTION, new ArgumentListExpression(List.of(
VariableExpression.THIS_EXPRESSION,
makeStringLiteral(classComponentTypeNode.getIdentifier()),
line,
column,
fmeVar
)));
};
case FragmentComponentNode ignored -> new ConstructorCallExpression(
MISSING_FRAGMENT_TYPE_EXCEPTION,
new ArgumentListExpression(List.of(VariableExpression.THIS_EXPRESSION, line, column, fmeVar))
);
};
final Statement throwMcceStmt = new ThrowStatement(mcceConstructorExpr);
return new CatchStatement(fmeParam, throwMcceStmt);
}
// catch (MissingComponentException c0mce) { throw c0mce }
protected CatchStatement getMissingComponentExceptionCatch(String componentVariableName) {
final String exceptionName = componentVariableName + "mce";
final Parameter exceptionParam = new Parameter(MISSING_COMPONENT_EXCEPTION, exceptionName);
final VariableExpression mceVar = new VariableExpression(exceptionName);
final Statement throwMceStmt = new ThrowStatement(mceVar);
return new CatchStatement(exceptionParam, throwMceStmt);
}
// catch (Exception c0ce) { throw new ComponentCreateException(c0ce) }
protected CatchStatement getGeneralCreateExceptionCatch(
ComponentNode componentNode,
String componentVariableName
) {
final String exceptionName = componentVariableName + "ce";
final Parameter exceptionParam = new Parameter(EXCEPTION, exceptionName);
final VariableExpression exceptionVar = new VariableExpression(exceptionName); final VariableExpression exceptionVar = new VariableExpression(exceptionName);
final ConstantExpression componentTypeExpression = switch (componentNode) { final VariableScope scope = new VariableScope();
case TypedComponentNode typedComponentNode -> switch (typedComponentNode.getArgs().getType()) { scope.putDeclaredVariable(exceptionParam);
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 List<Statement> statements = new ArrayList<>();
final ConstructorCallExpression cce = new ConstructorCallExpression( final BinaryExpression setTemplateExpression = new BinaryExpression(
COMPONENT_CREATE, new PropertyExpression(exceptionVar, "template"),
new ArgumentListExpression(List.of( getAssignToken(),
componentTypeExpression, VariableExpression.THIS_EXPRESSION
VariableExpression.THIS_EXPRESSION,
lineAndColumn.getV1(),
lineAndColumn.getV2(),
exceptionVar
))
); );
final Statement throwCcStmt = new ThrowStatement(cce); statements.add(new ExpressionStatement(setTemplateExpression));
return new CatchStatement(exceptionParam, throwCcStmt);
}
protected List<CatchStatement> getCreateCatches(ComponentNode componentNode, String componentVariableName) { final SourcePosition start = componentNode.getTokenRange().getStartPosition();
final List<CatchStatement> catches = new ArrayList<>();
catches.add(this.getNoMissingFactoryExceptionCatch(componentNode, componentVariableName));
catches.add(this.getMissingComponentExceptionCatch(componentVariableName));
catches.add(this.getGeneralCreateExceptionCatch(componentNode, componentVariableName));
return catches;
}
protected Statement createSetContext(TranspilerState state, Variable component) { final BinaryExpression setLineExpression = new BinaryExpression(
final VariableExpression componentExpr = new VariableExpression(component); new PropertyExpression(exceptionVar, "line"),
final VariableExpression contextExpr = new VariableExpression(state.context()); getAssignToken(),
final var args = new ArgumentListExpression(contextExpr); new ConstantExpression(start.line())
final var setContext = new MethodCallExpression(componentExpr, "setContext", args);
return new ExpressionStatement(setContext);
}
protected String getComponentVariableName(int componentNumber) {
return "c" + componentNumber;
}
@Override
public BlockStatement createComponentStatements(ComponentNode componentNode, TranspilerState state) {
final var componentVariableName = this.getComponentVariableName(state.newComponentNumber());
final VariableExpression component = new VariableExpression(componentVariableName, VIEW_COMPONENT);
final BlockStatement result = new BlockStatement();
final VariableScope scope = state.currentScope();
result.setVariableScope(scope);
scope.putDeclaredVariable(component);
// ViewComponent c0;
result.addStatement(this.getComponentDeclaration(component));
// c0 = context.create(...) { ... }
final var createAssignStatementResult = this.getCreateAssignStatement(
componentNode,
state,
componentVariableName,
component
); );
statements.add(new ExpressionStatement(setLineExpression));
// try { ... } catch { ... } final BinaryExpression setColumnExpression = new BinaryExpression(
final var tryCreateStatement = new TryCatchStatement( new PropertyExpression(exceptionVar, "column"),
createAssignStatementResult.getV1(), getAssignToken(),
new ConstantExpression(start.column())
);
statements.add(new ExpressionStatement(setColumnExpression));
statements.add(new ThrowStatement(exceptionVar));
return new CatchStatement(exceptionParam, new BlockStatement(statements, scope));
}
protected List<Statement> getTypedCreateStatements(TypedComponentNode componentNode, TranspilerState state) {
final Statement declaration = this.getTypedComponentDeclaration(state);
final TryCatchStatement createTryCatch = new TryCatchStatement(
this.getTypedComponentCreateStatement(componentNode, state),
EmptyStatement.INSTANCE EmptyStatement.INSTANCE
); );
this.getCreateCatches(componentNode, componentVariableName).forEach(tryCreateStatement::addCatch); createTryCatch.addCatch(this.getTypedCreateCatch(componentNode, state));
result.addStatement(tryCreateStatement); return List.of(declaration, createTryCatch);
}
// component.setContext(context) /* FRAGMENT COMPONENT */
result.addStatement(this.createSetContext(state, component));
// out or collect // context.createFragment(new Fragment(), <child cl>)
final var addOrAppend = this.appendOrAddStatementFactory.addOrAppend( protected MethodCallExpression getFragmentCreateExpression(
FragmentComponentNode componentNode,
TranspilerState state
) {
final Expression fragmentConstructor = new ConstructorCallExpression(
FRAGMENT_TYPE,
ArgumentListExpression.EMPTY_ARGUMENTS
);
final Expression ccClosure = this.getChildCollectorGetter(componentNode.getBody(), state);
final ArgumentListExpression args = new ArgumentListExpression(List.of(fragmentConstructor, ccClosure));
return new MethodCallExpression(
new VariableExpression(state.getRenderContext()),
"createFragment",
args
);
}
/* MAIN */
@Override
public List<Statement> createComponentStatements(ComponentNode componentNode, TranspilerState state) {
if (componentNode instanceof TypedComponentNode typedComponentNode) {
// Resolve
final List<Statement> resolveStatements = this.getResolveStatements(typedComponentNode, state);
// Create
final List<Statement> createStatements = this.getTypedCreateStatements(typedComponentNode, state);
// Append/Add
final Statement addOrAppend = this.getAppendOrAddStatementFactory().addOrAppend(
componentNode, componentNode,
state, state,
action -> switch (action) { new VariableExpression(state.getCurrentComponent())
case ADD -> { );
final var args = new ArgumentListExpression();
args.addExpression(component); // cleanup
final var outComponent = new VariableExpression(component); state.popResolved();
outComponent.setClosureSharedVariable(true); state.popComponent();
final Statement renderStatement = this.appendOrAddStatementFactory.appendOnly(
final List<Statement> allStatements = new ArrayList<>();
allStatements.addAll(resolveStatements);
allStatements.addAll(createStatements);
allStatements.add(addOrAppend);
return allStatements;
} else if (componentNode instanceof FragmentComponentNode fragmentComponentNode) {
// Create and add all at once
final Statement addOrAppend = this.getAppendOrAddStatementFactory().addOrAppend(
componentNode, componentNode,
state, state,
outComponent this.getFragmentCreateExpression(fragmentComponentNode, state)
); );
final ClosureExpression renderArg = new ClosureExpression( return List.of(addOrAppend);
Parameter.EMPTY_ARRAY, } else {
renderStatement throw new WebViewComponentBugError(new IllegalArgumentException(
); "Cannot handle a ComponentNode not of type TypedComponentNode or FragmentComponentNode."
args.addExpression(renderArg); ));
yield args;
} }
case APPEND -> new VariableExpression(component);
}
);
result.addStatement(addOrAppend);
return result;
} }
} }

View File

@ -1,28 +1,32 @@
package groowt.view.web.transpile; package groowt.view.web.transpile;
import groowt.view.web.antlr.TokenList; import groovy.transform.Field;
import groowt.view.component.compiler.ComponentTemplateCompileException;
import groowt.view.component.compiler.ComponentTemplateCompilerConfiguration;
import groowt.view.web.WebViewComponentBugError;
import groowt.view.web.ast.node.BodyNode; import groowt.view.web.ast.node.BodyNode;
import groowt.view.web.ast.node.CompilationUnitNode; import groowt.view.web.ast.node.CompilationUnitNode;
import groowt.view.web.ast.node.PreambleNode; import groowt.view.web.ast.node.PreambleNode;
import groowt.view.web.compiler.MultipleWebViewComponentCompileErrorsException;
import groowt.view.web.compiler.WebViewComponentTemplateCompileException;
import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit;
import groowt.view.web.runtime.DefaultWebViewComponentRenderContext;
import groowt.view.web.transpile.resolve.ClassLoaderComponentClassNodeResolver;
import groowt.view.web.transpile.util.GroovyUtil; import groowt.view.web.transpile.util.GroovyUtil;
import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.ClosureExpression; import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement; import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.ReturnStatement; import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.ast.stmt.Statement; import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.CompilationUnit; import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.ErrorCollector;
import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.io.ReaderSource;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
import static groowt.view.web.transpile.TranspilerUtil.*; import static groowt.view.web.transpile.TranspilerUtil.*;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
@ -38,34 +42,18 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler {
private static final Logger logger = LoggerFactory.getLogger(DefaultGroovyTranspiler.class); private static final Logger logger = LoggerFactory.getLogger(DefaultGroovyTranspiler.class);
private final CompilationUnit groovyCompilationUnit; private static final ClassNode FIELD_ANNOTATION = ClassHelper.make(Field.class);
private final String defaultPackageName; private static final ClassNode RENDER_CONTEXT_IMPLEMENTATION =
private final Supplier<? extends TranspilerConfiguration> configurationSupplier; ClassHelper.make(DefaultWebViewComponentRenderContext.class);
public DefaultGroovyTranspiler( protected TranspilerConfiguration getConfiguration(
CompilationUnit groovyCompilationUnit, WebViewComponentTemplateCompileUnit compileUnit,
@Nullable String defaultPackageName, ModuleNode moduleNode,
Supplier<? extends TranspilerConfiguration> configurationSupplier ClassLoader classLoader
) { ) {
this.groovyCompilationUnit = groovyCompilationUnit; return new DefaultTranspilerConfiguration(new ClassLoaderComponentClassNodeResolver(
this.defaultPackageName = defaultPackageName; compileUnit, moduleNode, classLoader
this.configurationSupplier = configurationSupplier; ));
}
protected TranspilerConfiguration getConfiguration() {
return this.configurationSupplier.get();
}
public @NotNull String getDefaultPackageName() {
return this.defaultPackageName != null ? this.defaultPackageName : GROOWT_VIEW_WEB;
}
protected @NotNull String getPackageName(ModuleNode moduleNode) {
if (moduleNode.hasPackageName()) {
return moduleNode.getPackageName();
} else {
return this.getDefaultPackageName();
}
} }
protected void checkPreambleClasses(String templateName, List<ClassNode> classNodes) { protected void checkPreambleClasses(String templateName, List<ClassNode> classNodes) {
@ -81,7 +69,10 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler {
} }
} }
protected List<InnerClassNode> convertPreambleClassesToInnerClasses(ClassNode mainClassNode, List<ClassNode> classNodes) { protected List<InnerClassNode> convertPreambleClassesToInnerClasses(
ClassNode mainClassNode,
List<ClassNode> classNodes
) {
final List<InnerClassNode> result = new ArrayList<>(); final List<InnerClassNode> result = new ArrayList<>();
for (final var classNode : classNodes) { for (final var classNode : classNodes) {
if (classNode instanceof InnerClassNode innerClassNode) { if (classNode instanceof InnerClassNode innerClassNode) {
@ -102,18 +93,23 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler {
} }
protected void handlePreamble( protected void handlePreamble(
String templateName, String templateClassName,
PreambleNode preambleNode, PreambleNode preambleNode,
ClassNode mainClassNode, ClassNode mainClassNode,
WebViewComponentModuleNode moduleNode WebViewComponentModuleNode moduleNode
) { ) {
final GroovyUtil.ConvertResult preambleConvert = GroovyUtil.convert( final GroovyUtil.ConvertResult convertResult = GroovyUtil.convert(
preambleNode.getGroovyCode().getAsValidGroovyCode() preambleNode.getGroovyCode().getAsValidGroovyCode()
); );
WebViewComponentModuleNode.copyTo(preambleConvert.moduleNode(), moduleNode); WebViewComponentModuleNode.copyTo(convertResult.moduleNode(), moduleNode);
final BlockStatement preambleBlock = preambleConvert.blockStatement(); if (convertResult.moduleNode().hasPackage()) {
moduleNode.setPackage(convertResult.moduleNode().getPackage());
mainClassNode.setName(moduleNode.getPackageName() + "." + templateClassName);
}
final BlockStatement preambleBlock = convertResult.blockStatement();
if (preambleBlock != null) { if (preambleBlock != null) {
// Fields // Fields
final List<Statement> preambleStatements = preambleBlock.getStatements(); final List<Statement> preambleStatements = preambleBlock.getStatements();
@ -133,7 +129,7 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler {
"Currently, only classes, methods, and field declarations " + "Currently, only classes, methods, and field declarations " +
"(marked with @groovy.transform.Field) " + "(marked with @groovy.transform.Field) " +
"are supported. The rest will be ignored.", "are supported. The rest will be ignored.",
templateName templateClassName
); );
} }
declarationsWithField.forEach(declaration -> { declarationsWithField.forEach(declaration -> {
@ -142,16 +138,16 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler {
} }
// move methods from script class // move methods from script class
final ClassNode scriptClass = preambleConvert.scriptClass(); final ClassNode scriptClass = convertResult.scriptClass();
if (scriptClass != null) { if (scriptClass != null) {
scriptClass.getMethods().forEach(mainClassNode::addMethod); scriptClass.getMethods().forEach(mainClassNode::addMethod);
} }
// handle classes // handle classes
final List<ClassNode> classNodes = preambleConvert.classNodes(); final List<ClassNode> classNodes = convertResult.classNodes();
this.checkPreambleClasses(templateName, classNodes); this.checkPreambleClasses(templateClassName, classNodes);
final List<ClassNode> toInner = classNodes.stream() final List<ClassNode> toInner = classNodes.stream()
.filter(classNode -> classNode != preambleConvert.scriptClass()) .filter(classNode -> classNode != convertResult.scriptClass())
.filter(classNode -> !classNode.isScript()) .filter(classNode -> !classNode.isScript())
.toList(); .toList();
final List<InnerClassNode> innerClassNodes = this.convertPreambleClassesToInnerClasses(mainClassNode, toInner); final List<InnerClassNode> innerClassNodes = this.convertPreambleClassesToInnerClasses(mainClassNode, toInner);
@ -164,30 +160,35 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler {
// - preamble with script -> use the script class from the converted preamble, // - preamble with script -> use the script class from the converted preamble,
// and don't forget to call run in our render method // and don't forget to call run in our render method
@Override @Override
public void transpile( public WebViewComponentSourceUnit transpile(
ComponentTemplateCompilerConfiguration compilerConfiguration,
WebViewComponentTemplateCompileUnit compileUnit,
CompilationUnitNode compilationUnitNode, CompilationUnitNode compilationUnitNode,
TokenList tokens, String templateClassName
String ownerComponentName, ) throws ComponentTemplateCompileException {
ReaderSource readerSource final var groovyCompilerConfiguration = compilerConfiguration.getGroovyCompilerConfiguration();
) {
final var configuration = this.getConfiguration();
final String templateName = ownerComponentName + "Template";
final var sourceUnit = new WebViewComponentSourceUnit( final var sourceUnit = new WebViewComponentSourceUnit(
templateName, templateClassName,
readerSource, compileUnit,
this.groovyCompilationUnit.getConfiguration(), groovyCompilerConfiguration,
this.groovyCompilationUnit.getClassLoader(), compilerConfiguration.getGroovyClassLoader(),
this.groovyCompilationUnit.getErrorCollector() new ErrorCollector(groovyCompilerConfiguration)
); );
final var moduleNode = new WebViewComponentModuleNode(sourceUnit); final var moduleNode = new WebViewComponentModuleNode(sourceUnit);
sourceUnit.setModuleNode(moduleNode); sourceUnit.setModuleNode(moduleNode);
final String packageName = this.getPackageName(moduleNode); moduleNode.setPackageName(compileUnit.getDefaultPackageName());
moduleNode.setPackageName(packageName);
moduleNode.addStarImport(GROOWT_VIEW_WEB + ".lib");
moduleNode.addImport(COMPONENT_TEMPLATE.getNameWithoutPackage(), COMPONENT_TEMPLATE);
moduleNode.addImport(COMPONENT_CONTEXT_TYPE.getNameWithoutPackage(), COMPONENT_CONTEXT_TYPE);
moduleNode.addImport(WEB_VIEW_COMPONENT_TYPE.getNameWithoutPackage(), WEB_VIEW_COMPONENT_TYPE);
moduleNode.addStarImport("groowt.view.component.runtime");
moduleNode.addStarImport(GROOWT_VIEW_WEB + ".runtime");
final ClassNode mainClassNode = new ClassNode( final ClassNode mainClassNode = new ClassNode(
packageName + "." + templateName, compileUnit.getDefaultPackageName() + "." + templateClassName,
ACC_PUBLIC, ACC_PUBLIC,
ClassHelper.OBJECT_TYPE ClassHelper.OBJECT_TYPE
); );
@ -199,15 +200,73 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler {
// preamble // preamble
final PreambleNode preambleNode = compilationUnitNode.getPreambleNode(); final PreambleNode preambleNode = compilationUnitNode.getPreambleNode();
if (preambleNode != null) { if (preambleNode != null) {
this.handlePreamble(templateName, preambleNode, mainClassNode, moduleNode); this.handlePreamble(templateClassName, preambleNode, mainClassNode, moduleNode);
} }
// renderer // getRenderer
final var renderBlock = new BlockStatement(); // params
final Parameter componentContextParam = new Parameter(COMPONENT_CONTEXT_TYPE, COMPONENT_CONTEXT_NAME);
final Parameter writerParam = new Parameter(COMPONENT_WRITER_TYPE, COMPONENT_WRITER_NAME);
final VariableExpression renderContextVariable = new VariableExpression(
RENDER_CONTEXT_NAME,
RENDER_CONTEXT_TYPE
);
final TranspilerState state = TranspilerState.withDefaultRootScope(); // closure body
renderBlock.setVariableScope(state.currentScope()); final BlockStatement renderBlock = new BlockStatement();
final TranspilerState state = TranspilerState.withRootScope(
componentContextParam,
writerParam,
renderContextVariable
);
renderBlock.setVariableScope(state.getCurrentScope());
// init: construct RenderContext
final ConstructorCallExpression renderContextConstructor = new ConstructorCallExpression(
RENDER_CONTEXT_IMPLEMENTATION,
new ArgumentListExpression(
new VariableExpression(componentContextParam), // component context
new VariableExpression(writerParam)
)
);
final BinaryExpression renderContextAssignExpr = new DeclarationExpression(
renderContextVariable,
getAssignToken(),
renderContextConstructor
);
renderBlock.addStatement(new ExpressionStatement(renderContextAssignExpr));
// init: componentContext.renderContext = renderContext
final BinaryExpression componentContextRenderContextAssign = new BinaryExpression(
new PropertyExpression(new VariableExpression(componentContextParam), "renderContext"),
getAssignToken(),
renderContextVariable
);
renderBlock.addStatement(new ExpressionStatement(componentContextRenderContextAssign));
// init: writer.renderContext = renderContext
final BinaryExpression writerRenderContextAssign = new BinaryExpression(
new PropertyExpression(new VariableExpression(writerParam), "renderContext"),
getAssignToken(),
renderContextVariable
);
renderBlock.addStatement(new ExpressionStatement(writerRenderContextAssign));
// init: writer.componentContext = componentContext
final BinaryExpression writerComponentContextAssign = new BinaryExpression(
new PropertyExpression(new VariableExpression(writerParam), "componentContext"),
getAssignToken(),
new VariableExpression(componentContextParam)
);
renderBlock.addStatement(new ExpressionStatement(writerComponentContextAssign));
// actual rendering of body
final var configuration = this.getConfiguration(
compileUnit,
moduleNode,
compilerConfiguration.getGroovyClassLoader()
);
final BodyNode bodyNode = compilationUnitNode.getBodyNode(); final BodyNode bodyNode = compilationUnitNode.getBodyNode();
if (bodyNode != null) { if (bodyNode != null) {
final var appendOrAddStatementFactory = configuration.getAppendOrAddStatementFactory(); final var appendOrAddStatementFactory = configuration.getAppendOrAddStatementFactory();
@ -215,9 +274,14 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler {
configuration.getBodyTranspiler() configuration.getBodyTranspiler()
.transpileBody( .transpileBody(
compilationUnitNode.getBodyNode(), compilationUnitNode.getBodyNode(),
(source, expr) -> appendOrAddStatementFactory.addOrAppend(source, state, action -> { (source, expr) -> appendOrAddStatementFactory.addOrAppend(
source,
state,
action -> {
if (action == AppendOrAddStatementFactory.Action.ADD) { if (action == AppendOrAddStatementFactory.Action.ADD) {
throw new IllegalStateException("Should not be adding here!"); throw new WebViewComponentBugError(new IllegalStateException(
"Should not be adding from document root!"
));
} }
return expr; return expr;
}), }),
@ -226,11 +290,10 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler {
); );
} }
renderBlock.addStatement(new ReturnStatement(ConstantExpression.NULL));
final ClosureExpression renderer = new ClosureExpression( final ClosureExpression renderer = new ClosureExpression(
new Parameter[] { new Parameter[] { componentContextParam, writerParam },
(Parameter) state.context(),
(Parameter) state.out()
},
renderBlock renderBlock
); );
@ -250,7 +313,16 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler {
); );
mainClassNode.addMethod(getRenderer); mainClassNode.addMethod(getRenderer);
this.groovyCompilationUnit.addSource(sourceUnit); if (state.hasErrors()) {
final List<ComponentTemplateCompileException> errors = state.getErrors();
if (errors.size() == 1) {
throw new WebViewComponentTemplateCompileException(compileUnit, errors.getFirst());
} else {
throw new MultipleWebViewComponentCompileErrorsException(compileUnit, errors);
}
}
return sourceUnit;
} }
} }

View File

@ -1,25 +1,26 @@
package groowt.view.web.transpile; package groowt.view.web.transpile;
import jakarta.inject.Inject; import groowt.view.web.transpile.resolve.ComponentClassNodeResolver;
import groowt.view.web.util.Provider;
public class DefaultTranspilerConfiguration implements TranspilerConfiguration { public class DefaultTranspilerConfiguration implements TranspilerConfiguration {
private final AppendOrAddStatementFactory appendOrAddStatementFactory = new DefaultAppendOrAddStatementFactory(); private final AppendOrAddStatementFactory appendOrAddStatementFactory = new DefaultAppendOrAddStatementFactory();
private final BodyTranspiler bodyTranspiler; private final BodyTranspiler bodyTranspiler;
private final ValueNodeTranspiler valueNodeTranspiler;
@Inject public DefaultTranspilerConfiguration(ComponentClassNodeResolver classNodeResolver) {
public DefaultTranspilerConfiguration() {
final var positionSetter = new SimplePositionSetter(); final var positionSetter = new SimplePositionSetter();
final var jStringTranspiler = new DefaultJStringTranspiler(positionSetter); final var jStringTranspiler = new DefaultJStringTranspiler(positionSetter);
final var gStringTranspiler = new DefaultGStringTranspiler(positionSetter, jStringTranspiler); final var gStringTranspiler = new DefaultGStringTranspiler(positionSetter, jStringTranspiler);
final var componentTranspiler = new DefaultComponentTranspiler(); final var componentTranspiler = new DefaultComponentTranspiler(
final var valueNodeTranspiler = new DefaultValueNodeTranspiler(componentTranspiler); Provider.of(this.appendOrAddStatementFactory),
Provider.of(classNodeResolver),
Provider.ofLazy(this::getValueNodeTranspiler),
Provider.ofLazy(this::getBodyTranspiler)
);
this.valueNodeTranspiler = new DefaultValueNodeTranspiler(componentTranspiler);
this.bodyTranspiler = new DefaultBodyTranspiler(gStringTranspiler, jStringTranspiler, componentTranspiler); this.bodyTranspiler = new DefaultBodyTranspiler(gStringTranspiler, jStringTranspiler, componentTranspiler);
componentTranspiler.setBodyTranspiler(this.bodyTranspiler);
componentTranspiler.setValueNodeTranspiler(valueNodeTranspiler);
componentTranspiler.setAppendOrAddStatementFactory(this.appendOrAddStatementFactory);
} }
@Override @Override
@ -32,4 +33,8 @@ public class DefaultTranspilerConfiguration implements TranspilerConfiguration {
return this.appendOrAddStatementFactory; return this.appendOrAddStatementFactory;
} }
protected ValueNodeTranspiler getValueNodeTranspiler() {
return this.valueNodeTranspiler;
}
} }

Some files were not shown because too many files have changed in this diff Show More