diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/FxmlParser.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/FxmlParser.java index fd56db1..09f5422 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/FxmlParser.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/FxmlParser.java @@ -1,5 +1,6 @@ package io.github.sheikah45.fx2j.parser; +import io.github.sheikah45.fx2j.parser.attribute.AssignableAttribute; import io.github.sheikah45.fx2j.parser.attribute.ControllerAttribute; import io.github.sheikah45.fx2j.parser.attribute.DefaultNameSpaceAttribute; import io.github.sheikah45.fx2j.parser.attribute.EventHandlerAttribute; @@ -8,11 +9,13 @@ import io.github.sheikah45.fx2j.parser.attribute.InstancePropertyAttribute; import io.github.sheikah45.fx2j.parser.attribute.NameSpaceAttribute; import io.github.sheikah45.fx2j.parser.attribute.StaticPropertyAttribute; +import io.github.sheikah45.fx2j.parser.element.AssignableElement; import io.github.sheikah45.fx2j.parser.element.ClassInstanceElement; import io.github.sheikah45.fx2j.parser.element.ConstantElement; import io.github.sheikah45.fx2j.parser.element.CopyElement; import io.github.sheikah45.fx2j.parser.element.DeclarationElement; import io.github.sheikah45.fx2j.parser.element.DefineElement; +import io.github.sheikah45.fx2j.parser.element.ElementContent; import io.github.sheikah45.fx2j.parser.element.FactoryElement; import io.github.sheikah45.fx2j.parser.element.FxmlElement; import io.github.sheikah45.fx2j.parser.element.IncludeElement; @@ -24,7 +27,6 @@ import io.github.sheikah45.fx2j.parser.element.ScriptSource; import io.github.sheikah45.fx2j.parser.element.StaticPropertyElement; import io.github.sheikah45.fx2j.parser.element.ValueElement; -import io.github.sheikah45.fx2j.parser.property.Concrete; import io.github.sheikah45.fx2j.parser.property.Expression; import io.github.sheikah45.fx2j.parser.property.Handler; import io.github.sheikah45.fx2j.parser.property.Value; @@ -69,14 +71,15 @@ public static FxmlComponents readFxml(Path filePath) { } } - private static ClassInstanceElement.Content createContent(Element element) { - List attributes = createAttributes(element); - List children = createChildren(element); - return new ClassInstanceElement.Content(attributes, children, retrieveInnerValue(element)); + private static ElementContent createContent(Element element) { + List fxmlAttributes = createFxmlAttributes(element); + List fxmlElements = createFxmlElements(element); + return new ElementContent<>(fxmlAttributes, fxmlElements, retrieveInnerValue(element)); } - private static Value.Single retrieveInnerValue(Element element) { - return createPropertyValue(retrieveInnerText(element)); + private static Value retrieveInnerValue(Element element) { + String innerText = retrieveInnerText(element); + return innerText.isBlank() ? new Value.Empty() : createPropertyValue(innerText); } private static String retrieveInnerText(Element element) { @@ -96,39 +99,7 @@ private static String retrieveInnerText(Element element) { .orElse(""); } - private static Value createPropertyValue(Element element) { - List values = new ArrayList<>(); - - createAttributes(element).stream().map(attribute -> { - if (!(attribute instanceof FxmlAttribute.CommonAttribute commonAttribute)) { - throw new ParseException("property attribute contains a non common attribute"); - } - - return commonAttribute; - }).map(Concrete.Attribute::new).forEach(values::add); - - createChildren(element) - .stream() - .map(Concrete.Element::new) - .forEach(values::add); - - Value.Single innerValue = retrieveInnerValue(element); - if (!(innerValue instanceof Concrete.Empty)) { - values.add(innerValue); - } - - if (values.isEmpty()) { - return new Concrete.Empty(); - } - - if (values.size() == 1) { - return values.getFirst(); - } - - return new Value.Multi(values); - } - - private static List createAttributes(Element element) { + private static List createFxmlAttributes(Element element) { NamedNodeMap attributesNodeMap = element.getAttributes(); int attrLength = attributesNodeMap.getLength(); return IntStream.range(0, attrLength) @@ -139,7 +110,7 @@ private static List createAttributes(Element element) { .toList(); } - private static List createChildren(Element element) { + private static List createFxmlElements(Element element) { NodeList childNodes = element.getChildNodes(); int childrenLength = childNodes.getLength(); @@ -162,12 +133,44 @@ private static FxmlElement createFxmlElement(Element element) { case String tag -> { int separatorIndex = tag.lastIndexOf("."); if (Character.isLowerCase(tag.charAt(separatorIndex + 1))) { + ElementContent content = createContent(element); + List assignableElements = content.elements().stream().map(fxmlElement -> { + if (!(fxmlElement instanceof AssignableElement assignable)) { + throw new ParseException( + "A property element cannot contain unassignable values"); + } + + return assignable; + }).toList(); + List assignableAttributes = content.attributes() + .stream() + .map(fxmlAttribute -> { + if (!(fxmlAttribute instanceof AssignableAttribute assignable)) { + throw new ParseException( + "A property attribute cannot contain unassignable values"); + } + + return assignable; + }) + .toList(); + if (separatorIndex == -1) { - yield new InstancePropertyElement(tag, createPropertyValue(element)); + yield new InstancePropertyElement(tag, + new ElementContent<>(assignableAttributes, assignableElements, + content.value())); } else { + if (!content.attributes().isEmpty()) { + throw new ParseException("static property elements cannot have attributes"); + } + + if (!content.elements().isEmpty()) { + throw new ParseException("static property elements cannot have elements"); + } + yield new StaticPropertyElement(tag.substring(0, separatorIndex), tag.substring(separatorIndex + 1), - createPropertyValue(element)); + new ElementContent<>(assignableAttributes, assignableElements, + content.value())); } } else { yield createInstanceElement(element); @@ -185,14 +188,14 @@ private static ClassInstanceElement createInstanceElement(Element element) { throw new ParseException("Multiple initialization attributes specified: %s".formatted(element)); } - ClassInstanceElement.Content content = createContent(element); + ElementContent content = createContent(element); if (factory != null) { return new FactoryElement(className, factory, content); } if (value != null) { - return new ValueElement(className, createPropertyValue(value), content); + return new ValueElement(className, value, content); } if (constant != null) { @@ -208,22 +211,50 @@ private static RootElement createRootElement(Element element) { } private static DefineElement createDefineElement(Element element) { - List children = createChildren(element).stream().map(child -> { - if (!(child instanceof ClassInstanceElement classInstanceElement)) { - throw new ParseException("define element contains a non class instance element"); + ElementContent content = createContent(element); + + if (!content.attributes().isEmpty()) { + throw new ParseException("fx:define element cannot have attributes"); + } + + if (!(content.value() instanceof Value.Empty)) { + throw new ParseException("fx:define element cannot have an inner value"); + } + + List instanceElements = content.elements().stream().map(fxmlElement -> { + if (!(fxmlElement instanceof ClassInstanceElement classInstanceElement)) { + throw new ParseException("fx:define element contains a non class instance element"); } return classInstanceElement; }).toList(); - return new DefineElement(children); + return new DefineElement(instanceElements); } private static ScriptElement createScriptElement(Element element) { - return removeAndGetValueIfPresent(element, "source").map(Path::of).map(source -> { - Optional charset = removeAndGetValueIfPresent(element, "charset"); - return new ScriptElement(new ScriptSource.Reference(source, charset.map(Charset::forName).orElse(null))); - }).orElseGet(() -> new ScriptElement(new ScriptSource.Inline(retrieveInnerText(element)))); + Path source = removeAndGetValueIfPresent(element, "source").map(Path::of).orElse(null); + Charset charset = removeAndGetValueIfPresent(element, "charset").map(Charset::forName) + .orElse(StandardCharsets.UTF_8); + ElementContent content = createContent(element); + + ScriptSource scriptSource; + if (source == null) { + if (!content.elements().isEmpty() || !content.attributes().isEmpty()) { + throw new ParseException( + "fx:script with inline source cannot have any elements or attributes other than charset"); + } + scriptSource = new ScriptSource.Inline(retrieveInnerText(element), charset); + } else { + if (!content.attributes().isEmpty() || + !content.elements().isEmpty() || !(content.value() instanceof Value.Empty)) { + throw new ParseException( + "fx:script with reference source cannot have any elements, attributes other than charset and source, or an inner value"); + } + scriptSource = new ScriptSource.Reference(source, charset); + } + + return new ScriptElement(scriptSource); } private static CopyElement createCopyElement(Element element) { @@ -274,16 +305,15 @@ yield new StaticPropertyAttribute(name.substring(0, separatorIndex), }; } - private static Value.Single createPropertyValue(String value) { + private static Value createPropertyValue(String value) { return switch (value) { - case String val when val.startsWith("@") -> new Concrete.Location(Path.of(val.substring(1))); - case String val when val.startsWith("%") -> new Concrete.Resource(val.substring(1)); + case String val when val.startsWith("@") -> new Value.Location(Path.of(val.substring(1))); + case String val when val.startsWith("%") -> new Value.Resource(val.substring(1)); case String val when val.startsWith("${") && val.endsWith("}") -> Expression.parse(val.substring(2, val.length() - 1)); - case String val when val.startsWith("$") -> new Concrete.Reference(val.substring(1)); - case String val when val.startsWith("\\") -> new Concrete.Literal(val.substring(1)); - case String val when val.isBlank() -> new Concrete.Empty(); - case String val -> new Concrete.Literal(val); + case String val when val.startsWith("$") -> new Value.Reference(val.substring(1)); + case String val when val.startsWith("\\") -> new Value.Literal(val.substring(1)); + case String val -> new Value.Literal(val); }; } diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/antlr/BindExpressionVisitorImpl.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/antlr/BindExpressionVisitorImpl.java index e05073f..466f038 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/antlr/BindExpressionVisitorImpl.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/antlr/BindExpressionVisitorImpl.java @@ -82,7 +82,7 @@ public Expression visitAdditive(BindExpressionParser.AdditiveContext ctx) { @Override public Expression visitStringLiteral(BindExpressionParser.StringLiteralContext ctx) { String text = ctx.getText(); - return new Expression.Str(text.substring(1, text.length() - 1)); + return new Expression.String(text.substring(1, text.length() - 1)); } @Override diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/AssignableAttribute.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/AssignableAttribute.java new file mode 100644 index 0000000..9a7b706 --- /dev/null +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/AssignableAttribute.java @@ -0,0 +1,4 @@ +package io.github.sheikah45.fx2j.parser.attribute; + +sealed public interface AssignableAttribute extends CommonAttribute + permits EventHandlerAttribute, InstancePropertyAttribute {} diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/CommonAttribute.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/CommonAttribute.java new file mode 100644 index 0000000..1452541 --- /dev/null +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/CommonAttribute.java @@ -0,0 +1,4 @@ +package io.github.sheikah45.fx2j.parser.attribute; + +sealed public interface CommonAttribute extends FxmlAttribute + permits AssignableAttribute, StaticPropertyAttribute {} diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/ControllerAttribute.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/ControllerAttribute.java index 4b135c5..3eaf0e4 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/ControllerAttribute.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/ControllerAttribute.java @@ -2,7 +2,7 @@ import io.github.sheikah45.fx2j.parser.utils.StringUtils; -public record ControllerAttribute(String className) implements FxmlAttribute.SpecialAttribute { +public record ControllerAttribute(String className) implements SpecialAttribute { public ControllerAttribute { if (StringUtils.isNullOrBlank(className)) { throw new IllegalArgumentException("className cannot be blank or null"); diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/DefaultNameSpaceAttribute.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/DefaultNameSpaceAttribute.java index 9f28c6d..c45f037 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/DefaultNameSpaceAttribute.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/DefaultNameSpaceAttribute.java @@ -1,11 +1,9 @@ package io.github.sheikah45.fx2j.parser.attribute; -import io.github.sheikah45.fx2j.parser.utils.StringUtils; - import java.net.URI; import java.util.Objects; -public record DefaultNameSpaceAttribute(URI location) implements FxmlAttribute.SpecialAttribute { +public record DefaultNameSpaceAttribute(URI location) implements SpecialAttribute { public DefaultNameSpaceAttribute { Objects.requireNonNull(location, "location cannot be null"); } diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/EventHandlerAttribute.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/EventHandlerAttribute.java index ef5f731..b0289b0 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/EventHandlerAttribute.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/EventHandlerAttribute.java @@ -6,7 +6,7 @@ import java.util.Objects; public record EventHandlerAttribute(String eventName, Handler handler) implements FxmlProperty.EventHandler, - FxmlAttribute.CommonAttribute { + AssignableAttribute { public EventHandlerAttribute { Objects.requireNonNull(eventName, "eventName cannot be null"); Objects.requireNonNull(handler, "handler cannot be null"); diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/FxmlAttribute.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/FxmlAttribute.java index 8dcf022..2c10f00 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/FxmlAttribute.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/FxmlAttribute.java @@ -1,11 +1,3 @@ package io.github.sheikah45.fx2j.parser.attribute; -public sealed interface FxmlAttribute { - - sealed interface SpecialAttribute extends FxmlAttribute - permits ControllerAttribute, DefaultNameSpaceAttribute, IdAttribute, NameSpaceAttribute {} - - sealed interface CommonAttribute extends FxmlAttribute - permits EventHandlerAttribute, InstancePropertyAttribute, StaticPropertyAttribute {} - -} +public sealed interface FxmlAttribute permits CommonAttribute, SpecialAttribute {} diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/IdAttribute.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/IdAttribute.java index 84234ed..f4e8ebd 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/IdAttribute.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/IdAttribute.java @@ -2,7 +2,7 @@ import io.github.sheikah45.fx2j.parser.utils.StringUtils; -public record IdAttribute(String value) implements FxmlAttribute.SpecialAttribute { +public record IdAttribute(String value) implements SpecialAttribute { public IdAttribute { if (StringUtils.isNullOrBlank(value)) { throw new IllegalArgumentException("id cannot be blank or null"); diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/InstancePropertyAttribute.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/InstancePropertyAttribute.java index 62f2b57..d7eafc8 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/InstancePropertyAttribute.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/InstancePropertyAttribute.java @@ -6,11 +6,11 @@ import java.util.Objects; -public record InstancePropertyAttribute(String property, Value.Single value) implements FxmlProperty.Instance, - FxmlAttribute.CommonAttribute { +public record InstancePropertyAttribute(String propertyName, Value value) implements FxmlProperty.Instance, + AssignableAttribute { public InstancePropertyAttribute { - if (StringUtils.isNullOrBlank(property)) { - throw new IllegalArgumentException("property cannot be blank or null"); + if (StringUtils.isNullOrBlank(propertyName)) { + throw new IllegalArgumentException("propertyName cannot be blank or null"); } Objects.requireNonNull(value, "value cannot be null"); } diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/NameSpaceAttribute.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/NameSpaceAttribute.java index 1f06efe..912f478 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/NameSpaceAttribute.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/NameSpaceAttribute.java @@ -5,7 +5,7 @@ import java.net.URI; import java.util.Objects; -public record NameSpaceAttribute(String namespace, URI location) implements FxmlAttribute.SpecialAttribute { +public record NameSpaceAttribute(String namespace, URI location) implements SpecialAttribute { public NameSpaceAttribute { Objects.requireNonNull(location, "location cannot be null"); if (StringUtils.isNullOrBlank(namespace)) { diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/SpecialAttribute.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/SpecialAttribute.java new file mode 100644 index 0000000..975c354 --- /dev/null +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/SpecialAttribute.java @@ -0,0 +1,4 @@ +package io.github.sheikah45.fx2j.parser.attribute; + +sealed public interface SpecialAttribute extends FxmlAttribute + permits ControllerAttribute, DefaultNameSpaceAttribute, IdAttribute, NameSpaceAttribute {} diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/StaticPropertyAttribute.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/StaticPropertyAttribute.java index d67c00a..0db5d2c 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/StaticPropertyAttribute.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/attribute/StaticPropertyAttribute.java @@ -6,14 +6,14 @@ import java.util.Objects; -public record StaticPropertyAttribute(String className, String property, Value.Single value) - implements FxmlAttribute.CommonAttribute, FxmlProperty.Static { +public record StaticPropertyAttribute(String className, String property, Value value) + implements CommonAttribute, FxmlProperty.Static { public StaticPropertyAttribute { if (StringUtils.isNullOrBlank(className)) { throw new IllegalArgumentException("className cannot be blank or null"); } if (StringUtils.isNullOrBlank(property)) { - throw new IllegalArgumentException("property cannot be blank or null"); + throw new IllegalArgumentException("propertyName cannot be blank or null"); } Objects.requireNonNull(value, "value cannot be null"); } diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/AssignableElement.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/AssignableElement.java new file mode 100644 index 0000000..89f5782 --- /dev/null +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/AssignableElement.java @@ -0,0 +1,3 @@ +package io.github.sheikah45.fx2j.parser.element; + +sealed public interface AssignableElement extends FxmlElement permits ClassInstanceElement, InstancePropertyElement {} diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ClassInstanceElement.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ClassInstanceElement.java index c407d86..cb1f0e3 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ClassInstanceElement.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ClassInstanceElement.java @@ -1,22 +1,7 @@ package io.github.sheikah45.fx2j.parser.element; -import io.github.sheikah45.fx2j.parser.attribute.FxmlAttribute; -import io.github.sheikah45.fx2j.parser.property.Value; - -import java.util.List; -import java.util.Objects; - -sealed public interface ClassInstanceElement extends FxmlElement +sealed public interface ClassInstanceElement extends AssignableElement permits CopyElement, DeclarationElement, IncludeElement, ReferenceElement { - Content content(); + ElementContent content(); - record Content(List attributes, List children, Value.Single body) { - public Content { - Objects.requireNonNull(attributes, "attributes cannot be null"); - Objects.requireNonNull(children, "children cannot be null"); - Objects.requireNonNull(body, "text cannot be null"); - attributes = List.copyOf(attributes); - children = List.copyOf(children); - } - } } diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ConstantElement.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ConstantElement.java index 538cb3e..c97daf4 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ConstantElement.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ConstantElement.java @@ -4,7 +4,7 @@ import java.util.Objects; -public record ConstantElement(String className, String member, Content content) +public record ConstantElement(String className, String member, ElementContent content) implements DeclarationElement { public ConstantElement { Objects.requireNonNull(content, "content cannot be null"); diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/CopyElement.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/CopyElement.java index 54a2bba..4532496 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/CopyElement.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/CopyElement.java @@ -4,7 +4,7 @@ import java.util.Objects; -public record CopyElement(String source, Content content) implements ClassInstanceElement { +public record CopyElement(String source, ElementContent content) implements ClassInstanceElement { public CopyElement { Objects.requireNonNull(content, "content cannot be null"); if (StringUtils.isNullOrBlank(source)) { diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/DefineElement.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/DefineElement.java index d8a25e8..6b331db 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/DefineElement.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/DefineElement.java @@ -2,4 +2,4 @@ import java.util.List; -public record DefineElement(List children) implements FxmlElement {} +public record DefineElement(List elements) implements FxmlElement {} diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ElementContent.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ElementContent.java new file mode 100644 index 0000000..ff0d3c0 --- /dev/null +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ElementContent.java @@ -0,0 +1,19 @@ +package io.github.sheikah45.fx2j.parser.element; + +import io.github.sheikah45.fx2j.parser.attribute.FxmlAttribute; +import io.github.sheikah45.fx2j.parser.property.Value; + +import java.util.List; +import java.util.Objects; + +public record ElementContent(List attributes, + List elements, + Value value) { + public ElementContent { + Objects.requireNonNull(attributes, "attributes cannot be null"); + Objects.requireNonNull(elements, "elements cannot be null"); + Objects.requireNonNull(value, "value cannot be null"); + attributes = List.copyOf(attributes); + elements = List.copyOf(elements); + } +} diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/FactoryElement.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/FactoryElement.java index df1586c..6e21912 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/FactoryElement.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/FactoryElement.java @@ -4,7 +4,7 @@ import java.util.Objects; -public record FactoryElement(String factoryClassName, String factoryMethod, Content content) +public record FactoryElement(String factoryClassName, String factoryMethod, ElementContent content) implements DeclarationElement { public FactoryElement { Objects.requireNonNull(content, "content cannot be null"); diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/FxmlElement.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/FxmlElement.java index 39385ac..0bd27ab 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/FxmlElement.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/FxmlElement.java @@ -1,5 +1,5 @@ package io.github.sheikah45.fx2j.parser.element; public sealed interface FxmlElement - permits ClassInstanceElement, DefineElement, InstancePropertyElement, ScriptElement, + permits AssignableElement, DefineElement, ScriptElement, StaticPropertyElement {} diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/IncludeElement.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/IncludeElement.java index 933e4ec..e1378ea 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/IncludeElement.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/IncludeElement.java @@ -4,7 +4,7 @@ import java.nio.file.Path; import java.util.Objects; -public record IncludeElement(Path source, Path resources, Charset charset, Content content) +public record IncludeElement(Path source, Path resources, Charset charset, ElementContent content) implements ClassInstanceElement { public IncludeElement { Objects.requireNonNull(source, "source cannot be null"); diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/InstanceElement.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/InstanceElement.java index a9562a1..ec2c52c 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/InstanceElement.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/InstanceElement.java @@ -4,7 +4,7 @@ import java.util.Objects; -public record InstanceElement(String className, Content content) +public record InstanceElement(String className, ElementContent content) implements DeclarationElement { public InstanceElement { Objects.requireNonNull(content, "content cannot be null"); diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/InstancePropertyElement.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/InstancePropertyElement.java index 91d7d5e..422b5fb 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/InstancePropertyElement.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/InstancePropertyElement.java @@ -1,13 +1,15 @@ package io.github.sheikah45.fx2j.parser.element; +import io.github.sheikah45.fx2j.parser.attribute.AssignableAttribute; import io.github.sheikah45.fx2j.parser.property.FxmlProperty; -import io.github.sheikah45.fx2j.parser.property.Value; import io.github.sheikah45.fx2j.parser.utils.StringUtils; -public record InstancePropertyElement(String property, Value value) implements FxmlElement, FxmlProperty.Instance { +public record InstancePropertyElement(String propertyName, + ElementContent content) + implements AssignableElement, FxmlProperty.Instance { public InstancePropertyElement { - if (StringUtils.isNullOrBlank(property)) { - throw new IllegalArgumentException("property cannot be blank or null"); + if (StringUtils.isNullOrBlank(propertyName)) { + throw new IllegalArgumentException("propertyName cannot be blank or null"); } } } diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ReferenceElement.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ReferenceElement.java index 72479f2..fd4f719 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ReferenceElement.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ReferenceElement.java @@ -4,7 +4,7 @@ import java.util.Objects; -public record ReferenceElement(String source, Content content) +public record ReferenceElement(String source, ElementContent content) implements ClassInstanceElement { public ReferenceElement { Objects.requireNonNull(content, "content cannot be null"); diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/RootElement.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/RootElement.java index 748e768..3e0cf51 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/RootElement.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/RootElement.java @@ -4,7 +4,7 @@ import java.util.Objects; -public record RootElement(String type, Content content) implements DeclarationElement { +public record RootElement(String type, ElementContent content) implements DeclarationElement { public RootElement { Objects.requireNonNull(content, "content cannot be null"); if (StringUtils.isNullOrBlank(type)) { diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ScriptSource.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ScriptSource.java index 71a76a2..87fc306 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ScriptSource.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ScriptSource.java @@ -7,8 +7,12 @@ import java.util.Objects; public sealed interface ScriptSource { - record Inline(String value) implements ScriptSource { + + Charset charset(); + + record Inline(String value, Charset charset) implements ScriptSource { public Inline { + Objects.requireNonNull(charset, "charset cannot be null"); if (StringUtils.isNullOrBlank(value)) { throw new IllegalArgumentException("source cannot be blank or null"); } @@ -16,6 +20,7 @@ record Inline(String value) implements ScriptSource { } record Reference(Path source, Charset charset) implements ScriptSource { public Reference { + Objects.requireNonNull(charset, "charset cannot be null"); Objects.requireNonNull(source, "source cannot be null"); } } diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/StaticPropertyElement.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/StaticPropertyElement.java index 55cdb71..8e54007 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/StaticPropertyElement.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/StaticPropertyElement.java @@ -1,17 +1,22 @@ package io.github.sheikah45.fx2j.parser.element; +import io.github.sheikah45.fx2j.parser.attribute.AssignableAttribute; import io.github.sheikah45.fx2j.parser.property.FxmlProperty; -import io.github.sheikah45.fx2j.parser.property.Value; import io.github.sheikah45.fx2j.parser.utils.StringUtils; -public record StaticPropertyElement(String className, String property, Value value) +import java.util.Objects; + +public record StaticPropertyElement(String className, + String property, + ElementContent content) implements FxmlElement, FxmlProperty.Static { public StaticPropertyElement { + Objects.requireNonNull(content, "content cannot be null"); if (StringUtils.isNullOrBlank(className)) { throw new IllegalArgumentException("className cannot be blank or null"); } if (StringUtils.isNullOrBlank(property)) { - throw new IllegalArgumentException("property cannot be blank or null"); + throw new IllegalArgumentException("propertyName cannot be blank or null"); } } } diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ValueElement.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ValueElement.java index 768d662..f69033c 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ValueElement.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/element/ValueElement.java @@ -1,11 +1,10 @@ package io.github.sheikah45.fx2j.parser.element; -import io.github.sheikah45.fx2j.parser.property.Value; import io.github.sheikah45.fx2j.parser.utils.StringUtils; import java.util.Objects; -public record ValueElement(String className, Value.Single value, Content content) implements +public record ValueElement(String className, String value, ElementContent content) implements DeclarationElement { public ValueElement { Objects.requireNonNull(content, "content cannot be null"); diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/Concrete.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/Concrete.java deleted file mode 100644 index 8ce1547..0000000 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/Concrete.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.github.sheikah45.fx2j.parser.property; - -import io.github.sheikah45.fx2j.parser.attribute.FxmlAttribute; -import io.github.sheikah45.fx2j.parser.element.FxmlElement; -import io.github.sheikah45.fx2j.parser.utils.StringUtils; - -import java.nio.file.Path; -import java.util.Objects; - -sealed public interface Concrete extends Value.Single { - record Empty() implements Concrete {} - record Literal(String value) implements Concrete { - public Literal { - if (StringUtils.isNullOrBlank(value)) { - throw new IllegalArgumentException("value cannot be blank or null"); - } - } - } - record Location(Path value) implements Concrete { - public Location { - Objects.requireNonNull(value, "location cannot be null"); - } - } - record Resource(String value) implements Concrete { - public Resource { - if (StringUtils.isNullOrBlank(value)) { - throw new IllegalArgumentException("value cannot be blank or null"); - } - } - } - record Element(FxmlElement value) implements Concrete {} - record Attribute(FxmlAttribute.CommonAttribute value) implements Concrete {} - record Reference(String value) implements Concrete { - public Reference { - if (StringUtils.isNullOrBlank(value)) { - throw new IllegalArgumentException("value cannot be blank or null"); - } - } - } -} diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/Expression.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/Expression.java index cd077ea..920be8b 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/Expression.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/Expression.java @@ -11,8 +11,8 @@ import java.util.List; import java.util.Objects; -sealed public interface Expression extends Value.Single { - static Expression parse(String expression) { +sealed public interface Expression extends Value { + static Expression parse(java.lang.String expression) { CodePointCharStream charStream = CharStreams.fromString(expression); BindExpressionLexer expressionLexer = new BindExpressionLexer(charStream); CommonTokenStream commonTokenStream = new CommonTokenStream(expressionLexer); @@ -20,15 +20,15 @@ static Expression parse(String expression) { return expressionParser.expression().accept(new BindExpressionVisitorImpl()); } - record PropertyRead(Expression expression, String property) implements Expression { + record PropertyRead(Expression expression, java.lang.String property) implements Expression { public PropertyRead { Objects.requireNonNull(expression, "expression cannot be null"); if (StringUtils.isNullOrBlank(property)) { - throw new IllegalArgumentException("property cannot be blank or null"); + throw new IllegalArgumentException("propertyName cannot be blank or null"); } } } - record MethodCall(Expression expression, String methodName, List args) implements Expression { + record MethodCall(Expression expression, java.lang.String methodName, List args) implements Expression { public MethodCall { Objects.requireNonNull(expression, "expression cannot be null"); Objects.requireNonNull(args, "args cannot be null"); @@ -131,15 +131,15 @@ record Or(Expression left, Expression right) implements Expression { Objects.requireNonNull(right, "right cannot be null"); } } - record Variable(String value) implements Expression { + record Variable(java.lang.String value) implements Expression { public Variable { if (StringUtils.isNullOrBlank(value)) { throw new IllegalArgumentException("value cannot be null or blank"); } } } - record Str(String value) implements Expression { - public Str { + record String(java.lang.String value) implements Expression { + public String { Objects.requireNonNull(value, "left cannot be null"); } } diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/FxmlProperty.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/FxmlProperty.java index 7fe1a70..39ffeb3 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/FxmlProperty.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/FxmlProperty.java @@ -12,28 +12,14 @@ sealed interface Static extends FxmlProperty permits StaticPropertyAttribute, St String className(); String property(); - - Value value(); } sealed interface Instance extends FxmlProperty - permits InstancePropertyAttribute, InstancePropertyElement, EventHandler { - String property(); - - Value value(); + permits InstancePropertyAttribute, InstancePropertyElement { + String propertyName(); } - sealed interface EventHandler extends Instance permits EventHandlerAttribute { - @Override - default String property() { - return eventName(); - } - - @Override - default Value value() { - return handler(); - } - + sealed interface EventHandler permits EventHandlerAttribute { Handler handler(); String eventName(); diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/Handler.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/Handler.java index c598567..1ad658d 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/Handler.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/Handler.java @@ -2,7 +2,7 @@ import io.github.sheikah45.fx2j.parser.utils.StringUtils; -sealed public interface Handler extends Value { +sealed public interface Handler { record Empty() implements Handler {} record Script(String value) implements Handler { public Script { diff --git a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/Value.java b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/Value.java index d2d14ec..7750423 100644 --- a/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/Value.java +++ b/fx2j-parser/src/main/java/io/github/sheikah45/fx2j/parser/property/Value.java @@ -1,16 +1,31 @@ package io.github.sheikah45.fx2j.parser.property; -import java.util.List; -import java.util.Objects; +import io.github.sheikah45.fx2j.parser.utils.StringUtils; -sealed public interface Value permits Handler, Value.Multi, Value.Single { +import java.nio.file.Path; +import java.util.Objects; - record Multi(List values) implements Value { - public Multi { - Objects.requireNonNull(values, "values cannot be null"); - values = List.copyOf(values); +sealed public interface Value + permits Expression, Value.Empty, Value.Literal, Value.Location, Value.Reference, Value.Resource { + record Empty() implements Value {} + record Literal(String value) implements Value {} + record Location(Path value) implements Value { + public Location { + Objects.requireNonNull(value, "location cannot be null"); + } + } + record Resource(String value) implements Value { + public Resource { + if (StringUtils.isNullOrBlank(value)) { + throw new IllegalArgumentException("value cannot be blank or null"); + } + } + } + record Reference(String value) implements Value { + public Reference { + if (StringUtils.isNullOrBlank(value)) { + throw new IllegalArgumentException("value cannot be blank or null"); + } } } - - sealed interface Single extends Value permits Expression, Concrete {} } diff --git a/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/FxmlParserElementTest.java b/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/FxmlParserElementTest.java index 3ea176a..c80f12a 100644 --- a/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/FxmlParserElementTest.java +++ b/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/FxmlParserElementTest.java @@ -2,11 +2,11 @@ import io.github.sheikah45.fx2j.parser.attribute.InstancePropertyAttribute; import io.github.sheikah45.fx2j.parser.attribute.NameSpaceAttribute; -import io.github.sheikah45.fx2j.parser.element.ClassInstanceElement; import io.github.sheikah45.fx2j.parser.element.ConstantElement; import io.github.sheikah45.fx2j.parser.element.CopyElement; import io.github.sheikah45.fx2j.parser.element.DeclarationElement; import io.github.sheikah45.fx2j.parser.element.DefineElement; +import io.github.sheikah45.fx2j.parser.element.ElementContent; import io.github.sheikah45.fx2j.parser.element.FactoryElement; import io.github.sheikah45.fx2j.parser.element.FxmlElement; import io.github.sheikah45.fx2j.parser.element.IncludeElement; @@ -18,7 +18,6 @@ import io.github.sheikah45.fx2j.parser.element.ScriptSource; import io.github.sheikah45.fx2j.parser.element.StaticPropertyElement; import io.github.sheikah45.fx2j.parser.element.ValueElement; -import io.github.sheikah45.fx2j.parser.property.Concrete; import io.github.sheikah45.fx2j.parser.property.Value; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Execution; @@ -37,13 +36,10 @@ class FxmlParserElementTest { private static final NameSpaceAttribute NAME_SPACE_ATTRIBUTE = new NameSpaceAttribute("fx", URI.create( "http://javafx.com/fxml")); - private static final ClassInstanceElement.Content NAME_SPACE_ONLY_CONTENT = new ClassInstanceElement.Content( - List.of(NAME_SPACE_ATTRIBUTE), - List.of(), - new Concrete.Empty()); - private static final ClassInstanceElement.Content EMPTY_CONTENT = new ClassInstanceElement.Content(List.of(), - List.of(), - new Concrete.Empty()); + private static final ElementContent NAME_SPACE_ONLY_CONTENT = new ElementContent<>( + List.of(NAME_SPACE_ATTRIBUTE), List.of(), new Value.Empty()); + private static final ElementContent EMPTY_CONTENT = new ElementContent<>(List.of(), List.of(), + new Value.Empty()); private static final Path FXML_ROOT = Path.of("src/test/resources/element/valid"); @Test @@ -68,13 +64,11 @@ void testInclude() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List children = rootNode.content().children(); + List children = rootNode.content().elements(); assertEquals(2, children.size()); FxmlElement first = children.getFirst(); - assertEquals(new IncludeElement(Path.of("included1.fxml"), null, StandardCharsets.UTF_8, - EMPTY_CONTENT), - first); + assertEquals(new IncludeElement(Path.of("included1.fxml"), null, StandardCharsets.UTF_8, EMPTY_CONTENT), first); FxmlElement last = children.getLast(); assertEquals(new IncludeElement(Path.of("included2.fxml"), Path.of("resource"), StandardCharsets.US_ASCII, @@ -88,7 +82,7 @@ void testReference() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List children = rootNode.content().children(); + List children = rootNode.content().elements(); assertEquals(2, children.size()); FxmlElement first = children.getFirst(); @@ -105,7 +99,7 @@ void testCopy() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List children = rootNode.content().children(); + List children = rootNode.content().elements(); assertEquals(2, children.size()); FxmlElement first = children.getFirst(); @@ -122,7 +116,7 @@ void testDefine() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List children = rootNode.content().children(); + List children = rootNode.content().elements(); assertEquals(1, children.size()); FxmlElement defined = children.getFirst(); @@ -139,12 +133,30 @@ void testScriptInline() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List children = rootNode.content().children(); + List children = rootNode.content().elements(); assertEquals(1, children.size()); assertEquals(new ScriptElement(new ScriptSource.Inline( - "function handleButtonAction(event) { java.lang.System.out.println('You clicked me!'); }")), - children.getFirst()); + "function handleButtonAction(event) { java.lang.System.out.println('You clicked me!'); }", + StandardCharsets.UTF_8)), children.getFirst()); + } + + @Test + void testScriptInlineWithCharset() { + Path filePath = FXML_ROOT.resolve("script-inline-with-charset.fxml"); + FxmlComponents fxmlComponents = FxmlParser.readFxml(filePath); + + assertEquals(new FxmlProcessingInstruction.Language("javascript"), + fxmlComponents.rootProcessingInstructions().getFirst()); + + DeclarationElement rootNode = fxmlComponents.rootNode(); + + List children = rootNode.content().elements(); + assertEquals(1, children.size()); + + assertEquals(new ScriptElement(new ScriptSource.Inline( + "function handleButtonAction(event) { java.lang.System.out.println('You clicked me!'); }", + StandardCharsets.US_ASCII)), children.getFirst()); } @Test @@ -157,25 +169,44 @@ void testScriptReference() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List children = rootNode.content().children(); + List children = rootNode.content().elements(); assertEquals(1, children.size()); assertEquals(new ScriptElement(new ScriptSource.Reference(Path.of("test.js"), StandardCharsets.UTF_8)), children.getFirst()); } + @Test + void testScriptReferenceWithCharset() { + Path filePath = FXML_ROOT.resolve("script-reference-with-charset.fxml"); + FxmlComponents fxmlComponents = FxmlParser.readFxml(filePath); + + assertEquals(new FxmlProcessingInstruction.Language("javascript"), + fxmlComponents.rootProcessingInstructions().getFirst()); + + DeclarationElement rootNode = fxmlComponents.rootNode(); + + List children = rootNode.content().elements(); + assertEquals(1, children.size()); + + assertEquals(new ScriptElement(new ScriptSource.Reference(Path.of("test.js"), StandardCharsets.US_ASCII)), + children.getFirst()); + } + @Test void testInstanceProperty() { Path filePath = FXML_ROOT.resolve("single-property.fxml"); FxmlComponents fxmlComponents = FxmlParser.readFxml(filePath); DeclarationElement rootNode = fxmlComponents.rootNode(); - assertEquals(new InstanceElement("VBox", new ClassInstanceElement.Content(List.of(NAME_SPACE_ATTRIBUTE), - List.of(new InstancePropertyElement( - "alignment", - new Concrete.Literal( - "TOP_RIGHT"))), - new Concrete.Empty())), rootNode); + assertEquals(new InstanceElement("VBox", new ElementContent<>(List.of(NAME_SPACE_ATTRIBUTE), + List.of(new InstancePropertyElement("alignment", + new ElementContent<>( + List.of(), + List.of(), + new Value.Literal( + "TOP_RIGHT")))), + new Value.Empty())), rootNode); } @Test @@ -184,10 +215,8 @@ void testPropertyText() { FxmlComponents fxmlComponents = FxmlParser.readFxml(filePath); DeclarationElement rootNode = fxmlComponents.rootNode(); - assertEquals( - new InstanceElement("Label", new ClassInstanceElement.Content(List.of(NAME_SPACE_ATTRIBUTE), List.of(), - new Concrete.Literal("test"))), - rootNode); + assertEquals(new InstanceElement("Label", new ElementContent<>(List.of(NAME_SPACE_ATTRIBUTE), List.of(), + new Value.Literal("test"))), rootNode); } @Test @@ -196,13 +225,14 @@ void testMultiPropertyText() { FxmlComponents fxmlComponents = FxmlParser.readFxml(filePath); DeclarationElement rootNode = fxmlComponents.rootNode(); - assertEquals(new InstanceElement("Label", new ClassInstanceElement.Content(List.of(NAME_SPACE_ATTRIBUTE), - List.of(new InstancePropertyElement( - "alignment", - new Concrete.Literal( - "TOP_LEFT"))), - new Concrete.Literal("test2"))), - rootNode); + assertEquals(new InstanceElement("Label", new ElementContent<>(List.of(NAME_SPACE_ATTRIBUTE), + List.of(new InstancePropertyElement("alignment", + new ElementContent<>( + List.of(), + List.of(), + new Value.Literal( + "TOP_LEFT")))), + new Value.Literal("test2"))), rootNode); } @Test @@ -211,16 +241,16 @@ void testPropertyAttribute() { FxmlComponents fxmlComponents = FxmlParser.readFxml(filePath); DeclarationElement rootNode = fxmlComponents.rootNode(); - assertEquals(new InstanceElement("VBox", new ClassInstanceElement.Content(List.of(NAME_SPACE_ATTRIBUTE), - List.of(new InstancePropertyElement( - "properties", - new Concrete.Attribute( - new InstancePropertyAttribute( - "foo", - new Concrete.Literal( - "123"))))), - new Concrete.Empty())), - rootNode); + assertEquals(new InstanceElement("VBox", new ElementContent<>(List.of(NAME_SPACE_ATTRIBUTE), + List.of(new InstancePropertyElement("properties", + new ElementContent<>( + List.of(new InstancePropertyAttribute( + "foo", + new Value.Literal( + "123"))), + List.of(), + new Value.Empty()))), + new Value.Empty())), rootNode); } @Test @@ -229,14 +259,15 @@ void testPropertyElement() { FxmlComponents fxmlComponents = FxmlParser.readFxml(filePath); DeclarationElement rootNode = fxmlComponents.rootNode(); - assertEquals(new InstanceElement("VBox", new ClassInstanceElement.Content(List.of(NAME_SPACE_ATTRIBUTE), - List.of(new InstancePropertyElement( - "children", - new Concrete.Element( - new InstanceElement( - "VBox", - EMPTY_CONTENT)))), - new Concrete.Empty())), rootNode); + assertEquals(new InstanceElement("VBox", new ElementContent<>(List.of(NAME_SPACE_ATTRIBUTE), + List.of(new InstancePropertyElement("children", + new ElementContent<>( + List.of(), + List.of(new InstanceElement( + "VBox", + EMPTY_CONTENT)), + new Value.Empty()))), + new Value.Empty())), rootNode); } @Test @@ -245,22 +276,19 @@ void testMultiProperty() { FxmlComponents fxmlComponents = FxmlParser.readFxml(filePath); DeclarationElement rootNode = fxmlComponents.rootNode(); - assertEquals(new InstanceElement("VBox", new ClassInstanceElement.Content(List.of(NAME_SPACE_ATTRIBUTE), - List.of(new InstancePropertyElement( - "alignment", - new Value.Multi( - List.of(new Concrete.Attribute( - new InstancePropertyAttribute( + assertEquals(new InstanceElement("VBox", new ElementContent<>(List.of(NAME_SPACE_ATTRIBUTE), + List.of(new InstancePropertyElement("alignment", + new ElementContent<>( + List.of(new InstancePropertyAttribute( "value", - new Concrete.Literal( + new Value.Literal( "TOP_RIGHT"))), - new Concrete.Element( - new InstanceElement( + List.of(new InstanceElement( "Label", EMPTY_CONTENT)), - new Concrete.Literal( - "text"))))), - new Concrete.Empty())), rootNode); + new Value.Literal( + "text")))), + new Value.Empty())), rootNode); } @Test @@ -269,12 +297,13 @@ void testEmptyProperty() { FxmlComponents fxmlComponents = FxmlParser.readFxml(filePath); DeclarationElement rootNode = fxmlComponents.rootNode(); - assertEquals(new InstanceElement("VBox", new ClassInstanceElement.Content(List.of(NAME_SPACE_ATTRIBUTE), - List.of(new InstancePropertyElement( - "alignment", - new Concrete.Empty())), - new Concrete.Empty())), - rootNode); + assertEquals(new InstanceElement("VBox", new ElementContent<>(List.of(NAME_SPACE_ATTRIBUTE), + List.of(new InstancePropertyElement("alignment", + new ElementContent<>( + List.of(), + List.of(), + new Value.Empty()))), + new Value.Empty())), rootNode); } @Test @@ -283,12 +312,15 @@ void testStaticProperty() { FxmlComponents fxmlComponents = FxmlParser.readFxml(filePath); DeclarationElement rootNode = fxmlComponents.rootNode(); - assertEquals(new InstanceElement("VBox", new ClassInstanceElement.Content(List.of(NAME_SPACE_ATTRIBUTE), - List.of(new StaticPropertyElement( - "GridPane", "alignment", - new Concrete.Literal( - "TOP_RIGHT"))), - new Concrete.Empty())), rootNode); + assertEquals(new InstanceElement("VBox", new ElementContent<>(List.of(NAME_SPACE_ATTRIBUTE), + List.of(new StaticPropertyElement("GridPane", + "alignment", + new ElementContent<>( + List.of(), + List.of(), + new Value.Literal( + "TOP_RIGHT")))), + new Value.Empty())), rootNode); } @Test @@ -297,13 +329,14 @@ void testQualifiedStaticProperty() { FxmlComponents fxmlComponents = FxmlParser.readFxml(filePath); DeclarationElement rootNode = fxmlComponents.rootNode(); - assertEquals(new InstanceElement("VBox", new ClassInstanceElement.Content(List.of(NAME_SPACE_ATTRIBUTE), - List.of(new StaticPropertyElement( - "javafx.scene.layout.GridPane", - "alignment", - new Concrete.Literal( - "TOP_RIGHT"))), - new Concrete.Empty())), rootNode); + assertEquals(new InstanceElement("VBox", new ElementContent<>(List.of(NAME_SPACE_ATTRIBUTE), + List.of(new StaticPropertyElement( + "javafx.scene.layout.GridPane", + "alignment", + new ElementContent<>(List.of(), List.of(), + new Value.Literal( + "TOP_RIGHT")))), + new Value.Empty())), rootNode); } @Test @@ -321,7 +354,7 @@ void testValue() { FxmlComponents fxmlComponents = FxmlParser.readFxml(filePath); DeclarationElement rootNode = fxmlComponents.rootNode(); - assertEquals(new ValueElement("Double", new Concrete.Literal("1"), NAME_SPACE_ONLY_CONTENT), rootNode); + assertEquals(new ValueElement("Double", "1", NAME_SPACE_ONLY_CONTENT), rootNode); } @Test diff --git a/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/FxmlParserInvalidElementTest.java b/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/FxmlParserInvalidElementTest.java index d7d226d..46d2e6c 100644 --- a/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/FxmlParserInvalidElementTest.java +++ b/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/FxmlParserInvalidElementTest.java @@ -39,8 +39,14 @@ void testMultiDeclaration() { } @Test - void testNonCommonPropertyValue() { - Path filePath = FXML_ROOT.resolve("property-non-common-attribute.fxml"); + void testNonAssignablePropertyAttribute() { + Path filePath = FXML_ROOT.resolve("property-non-assignable-attribute.fxml"); + assertThrows(ParseException.class, () -> FxmlParser.readFxml(filePath)); + } + + @Test + void testNonAssignablePropertyChild() { + Path filePath = FXML_ROOT.resolve("property-non-assignable-child.fxml"); assertThrows(ParseException.class, () -> FxmlParser.readFxml(filePath)); } @@ -63,8 +69,68 @@ void testCopy() { } @Test - void testDefine() { - Path filePath = FXML_ROOT.resolve("define.fxml"); + void testDefineValue() { + Path filePath = FXML_ROOT.resolve("define-value.fxml"); + assertThrows(ParseException.class, () -> FxmlParser.readFxml(filePath)); + } + + @Test + void testDefineProperty() { + Path filePath = FXML_ROOT.resolve("define-property.fxml"); + assertThrows(ParseException.class, () -> FxmlParser.readFxml(filePath)); + } + + @Test + void testDefineAttribute() { + Path filePath = FXML_ROOT.resolve("define-attribute.fxml"); + assertThrows(ParseException.class, () -> FxmlParser.readFxml(filePath)); + } + + @Test + void testScriptSourceChild() { + Path filePath = FXML_ROOT.resolve("script-source-child.fxml"); + assertThrows(ParseException.class, () -> FxmlParser.readFxml(filePath)); + } + + @Test + void testScriptSourceProperty() { + Path filePath = FXML_ROOT.resolve("script-source-property.fxml"); + assertThrows(ParseException.class, () -> FxmlParser.readFxml(filePath)); + } + + @Test + void testScriptSourceValue() { + Path filePath = FXML_ROOT.resolve("script-source-value.fxml"); + assertThrows(ParseException.class, () -> FxmlParser.readFxml(filePath)); + } + + @Test + void testScriptInlineChild() { + Path filePath = FXML_ROOT.resolve("script-inline-child.fxml"); + assertThrows(ParseException.class, () -> FxmlParser.readFxml(filePath)); + } + + @Test + void testScriptInlineProperty() { + Path filePath = FXML_ROOT.resolve("script-inline-property.fxml"); + assertThrows(ParseException.class, () -> FxmlParser.readFxml(filePath)); + } + + @Test + void testScriptInlineValue() { + Path filePath = FXML_ROOT.resolve("script-inline-value.fxml"); + assertThrows(ParseException.class, () -> FxmlParser.readFxml(filePath)); + } + + @Test + void testStaticPropertyAttribute() { + Path filePath = FXML_ROOT.resolve("static-property-attribute.fxml"); + assertThrows(ParseException.class, () -> FxmlParser.readFxml(filePath)); + } + + @Test + void testStaticPropertyChild() { + Path filePath = FXML_ROOT.resolve("static-property-child.fxml"); assertThrows(ParseException.class, () -> FxmlParser.readFxml(filePath)); } } \ No newline at end of file diff --git a/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/FxmlParserPropertyTest.java b/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/FxmlParserPropertyTest.java index d415460..596284b 100644 --- a/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/FxmlParserPropertyTest.java +++ b/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/FxmlParserPropertyTest.java @@ -9,7 +9,6 @@ import io.github.sheikah45.fx2j.parser.attribute.NameSpaceAttribute; import io.github.sheikah45.fx2j.parser.attribute.StaticPropertyAttribute; import io.github.sheikah45.fx2j.parser.element.DeclarationElement; -import io.github.sheikah45.fx2j.parser.property.Concrete; import io.github.sheikah45.fx2j.parser.property.Expression; import io.github.sheikah45.fx2j.parser.property.Handler; import io.github.sheikah45.fx2j.parser.property.Value; @@ -38,7 +37,7 @@ void testDefaultNamespace() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); + List attributes = rootNode.content().attributes(); assertEquals(List.of(new DefaultNameSpaceAttribute(URI.create( "http://javafx.com/fxml"))), attributes); } @@ -50,7 +49,7 @@ void testIdProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); + List attributes = rootNode.content().attributes(); assertEquals(List.of(new IdAttribute("box"), NAME_SPACE_ATTRIBUTE), attributes); } @@ -61,7 +60,7 @@ void testControllerProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); + List attributes = rootNode.content().attributes(); assertEquals( List.of(new ControllerAttribute("io.github.sheikah45.fx2j.parser.FxmlParser"), NAME_SPACE_ATTRIBUTE), attributes); @@ -74,8 +73,8 @@ void testInstanceProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); - assertEquals(List.of(new InstancePropertyAttribute("alignment", new Concrete.Literal("TOP_RIGHT")), + List attributes = rootNode.content().attributes(); + assertEquals(List.of(new InstancePropertyAttribute("alignment", new Value.Literal("TOP_RIGHT")), NAME_SPACE_ATTRIBUTE), attributes); } @@ -86,8 +85,8 @@ void testStaticProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); - assertEquals(List.of(new StaticPropertyAttribute("GridPane", "alignment", new Concrete.Literal("TOP_RIGHT")), + List attributes = rootNode.content().attributes(); + assertEquals(List.of(new StaticPropertyAttribute("GridPane", "alignment", new Value.Literal("TOP_RIGHT")), NAME_SPACE_ATTRIBUTE), attributes); } @@ -99,9 +98,9 @@ void testQualifiedStaticProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); + List attributes = rootNode.content().attributes(); assertEquals(List.of(new StaticPropertyAttribute("javafx.scene.layout.GridPane", "alignment", - new Concrete.Literal("TOP_RIGHT")), NAME_SPACE_ATTRIBUTE), + new Value.Literal("TOP_RIGHT")), NAME_SPACE_ATTRIBUTE), attributes); } @@ -112,7 +111,7 @@ void testEventHandlerProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); + List attributes = rootNode.content().attributes(); assertEquals( List.of(new EventHandlerAttribute("onScroll", new Handler.Script( "java.lang.System.out.println('scrolled')")), NAME_SPACE_ATTRIBUTE), @@ -126,8 +125,8 @@ void testEmptyProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); - assertEquals(List.of(new InstancePropertyAttribute("text", new Concrete.Empty()), NAME_SPACE_ATTRIBUTE), + List attributes = rootNode.content().attributes(); + assertEquals(List.of(new InstancePropertyAttribute("text", new Value.Literal("")), NAME_SPACE_ATTRIBUTE), attributes); } @@ -138,8 +137,8 @@ void testLocationProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); - assertEquals(List.of(new InstancePropertyAttribute("url", new Concrete.Location(Path.of("test.png"))), + List attributes = rootNode.content().attributes(); + assertEquals(List.of(new InstancePropertyAttribute("url", new Value.Location(Path.of("test.png"))), NAME_SPACE_ATTRIBUTE), attributes); } @@ -151,9 +150,9 @@ void testResourceProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); + List attributes = rootNode.content().attributes(); assertEquals( - List.of(new InstancePropertyAttribute("text", new Concrete.Resource("test")), NAME_SPACE_ATTRIBUTE), + List.of(new InstancePropertyAttribute("text", new Value.Resource("test")), NAME_SPACE_ATTRIBUTE), attributes); } @@ -164,9 +163,9 @@ void testReferenceProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); + List attributes = rootNode.content().attributes(); assertEquals( - List.of(new InstancePropertyAttribute("text", new Concrete.Reference("test")), NAME_SPACE_ATTRIBUTE), + List.of(new InstancePropertyAttribute("text", new Value.Reference("test")), NAME_SPACE_ATTRIBUTE), attributes); } @@ -177,9 +176,9 @@ void testEscapeProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); + List attributes = rootNode.content().attributes(); assertEquals( - List.of(new InstancePropertyAttribute("text", new Concrete.Literal("$test")), NAME_SPACE_ATTRIBUTE), + List.of(new InstancePropertyAttribute("text", new Value.Literal("$test")), NAME_SPACE_ATTRIBUTE), attributes); } @@ -190,7 +189,7 @@ void testExpressionProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); + List attributes = rootNode.content().attributes(); assertEquals(List.of(new InstancePropertyAttribute("text", new Expression.PropertyRead( new Expression.Variable("test"), "text")), NAME_SPACE_ATTRIBUTE), attributes); } @@ -202,7 +201,7 @@ void testComplexExpressionProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); + List attributes = rootNode.content().attributes(); assertEquals(List.of(new InstancePropertyAttribute("width", new Expression.Add(new Expression.Add( new Expression.PropertyRead( new Expression.PropertyRead(new Expression.Variable("test"), "text"), "length"), @@ -221,7 +220,7 @@ void testReferenceHandlerProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); + List attributes = rootNode.content().attributes(); assertEquals(List.of(new EventHandlerAttribute("onScroll", new Handler.Reference("scroller")), NAME_SPACE_ATTRIBUTE), attributes); @@ -234,7 +233,7 @@ void testEmptyHandlerProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); + List attributes = rootNode.content().attributes(); assertEquals(List.of(new EventHandlerAttribute("onScroll", new Handler.Empty()), NAME_SPACE_ATTRIBUTE), attributes); } @@ -246,7 +245,7 @@ void testMethodProperty() { DeclarationElement rootNode = fxmlComponents.rootNode(); - List attributes = rootNode.content().attributes(); + List attributes = rootNode.content().attributes(); assertEquals(List.of(new EventHandlerAttribute("onScroll", new Handler.Method("scroll")), NAME_SPACE_ATTRIBUTE), attributes); } diff --git a/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/property/ExpressionTest.java b/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/property/ExpressionTest.java index 2d9e755..782e46a 100644 --- a/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/property/ExpressionTest.java +++ b/fx2j-parser/src/test/java/io/github/sheikah45/fx2j/parser/property/ExpressionTest.java @@ -24,8 +24,8 @@ void testBooleanLiteral() { @Test void testStringLiteral() { - assertEquals(new Expression.Str("\"true\""), Expression.parse("'\"true\"'")); - assertEquals(new Expression.Str("'false'"), Expression.parse("\"'false'\"")); + assertEquals(new Expression.String("\"true\""), Expression.parse("'\"true\"'")); + assertEquals(new Expression.String("'false'"), Expression.parse("\"'false'\"")); } @Test diff --git a/fx2j-parser/src/test/resources/element/invalid/define-attribute.fxml b/fx2j-parser/src/test/resources/element/invalid/define-attribute.fxml new file mode 100644 index 0000000..139d488 --- /dev/null +++ b/fx2j-parser/src/test/resources/element/invalid/define-attribute.fxml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/fx2j-parser/src/test/resources/element/invalid/define.fxml b/fx2j-parser/src/test/resources/element/invalid/define-property.fxml similarity index 100% rename from fx2j-parser/src/test/resources/element/invalid/define.fxml rename to fx2j-parser/src/test/resources/element/invalid/define-property.fxml diff --git a/fx2j-parser/src/test/resources/element/invalid/define-value.fxml b/fx2j-parser/src/test/resources/element/invalid/define-value.fxml new file mode 100644 index 0000000..b2aeea5 --- /dev/null +++ b/fx2j-parser/src/test/resources/element/invalid/define-value.fxml @@ -0,0 +1,6 @@ + + + + value + + \ No newline at end of file diff --git a/fx2j-parser/src/test/resources/element/invalid/property-non-common-attribute.fxml b/fx2j-parser/src/test/resources/element/invalid/property-non-assignable-attribute.fxml similarity index 100% rename from fx2j-parser/src/test/resources/element/invalid/property-non-common-attribute.fxml rename to fx2j-parser/src/test/resources/element/invalid/property-non-assignable-attribute.fxml diff --git a/fx2j-parser/src/test/resources/element/invalid/property-non-assignable-child.fxml b/fx2j-parser/src/test/resources/element/invalid/property-non-assignable-child.fxml new file mode 100644 index 0000000..057d5d1 --- /dev/null +++ b/fx2j-parser/src/test/resources/element/invalid/property-non-assignable-child.fxml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/fx2j-parser/src/test/resources/element/invalid/script-inline-child.fxml b/fx2j-parser/src/test/resources/element/invalid/script-inline-child.fxml new file mode 100644 index 0000000..510f228 --- /dev/null +++ b/fx2j-parser/src/test/resources/element/invalid/script-inline-child.fxml @@ -0,0 +1,11 @@ + + + + + + + function handleButtonAction(event) { + java.lang.System.out.println('You clicked me!'); + } + + \ No newline at end of file diff --git a/fx2j-parser/src/test/resources/element/invalid/script-inline-property.fxml b/fx2j-parser/src/test/resources/element/invalid/script-inline-property.fxml new file mode 100644 index 0000000..58b9224 --- /dev/null +++ b/fx2j-parser/src/test/resources/element/invalid/script-inline-property.fxml @@ -0,0 +1,10 @@ + + + + + + function handleButtonAction(event) { + java.lang.System.out.println('You clicked me!'); + } + + \ No newline at end of file diff --git a/fx2j-parser/src/test/resources/element/invalid/script-source-child.fxml b/fx2j-parser/src/test/resources/element/invalid/script-source-child.fxml new file mode 100644 index 0000000..22c39c6 --- /dev/null +++ b/fx2j-parser/src/test/resources/element/invalid/script-source-child.fxml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/fx2j-parser/src/test/resources/element/invalid/script-source-property.fxml b/fx2j-parser/src/test/resources/element/invalid/script-source-property.fxml new file mode 100644 index 0000000..de307ee --- /dev/null +++ b/fx2j-parser/src/test/resources/element/invalid/script-source-property.fxml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/fx2j-parser/src/test/resources/element/invalid/script-source-value.fxml b/fx2j-parser/src/test/resources/element/invalid/script-source-value.fxml new file mode 100644 index 0000000..6d89a5d --- /dev/null +++ b/fx2j-parser/src/test/resources/element/invalid/script-source-value.fxml @@ -0,0 +1,10 @@ + + + + + + function handleButtonAction(event) { + java.lang.System.out.println('You clicked me!'); + } + + \ No newline at end of file diff --git a/fx2j-parser/src/test/resources/element/invalid/static-property-attribute.fxml b/fx2j-parser/src/test/resources/element/invalid/static-property-attribute.fxml new file mode 100644 index 0000000..d34d3b8 --- /dev/null +++ b/fx2j-parser/src/test/resources/element/invalid/static-property-attribute.fxml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/fx2j-parser/src/test/resources/element/invalid/static-property-child.fxml b/fx2j-parser/src/test/resources/element/invalid/static-property-child.fxml new file mode 100644 index 0000000..2c0ebc5 --- /dev/null +++ b/fx2j-parser/src/test/resources/element/invalid/static-property-child.fxml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/fx2j-parser/src/test/resources/element/valid/script-inline-with-charset.fxml b/fx2j-parser/src/test/resources/element/valid/script-inline-with-charset.fxml new file mode 100644 index 0000000..20796e9 --- /dev/null +++ b/fx2j-parser/src/test/resources/element/valid/script-inline-with-charset.fxml @@ -0,0 +1,11 @@ + + + + + + + function handleButtonAction(event) { + java.lang.System.out.println('You clicked me!'); + } + + \ No newline at end of file diff --git a/fx2j-parser/src/test/resources/element/valid/script-reference-with-charset.fxml b/fx2j-parser/src/test/resources/element/valid/script-reference-with-charset.fxml new file mode 100644 index 0000000..2f55f58 --- /dev/null +++ b/fx2j-parser/src/test/resources/element/valid/script-reference-with-charset.fxml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/fx2j-parser/src/test/resources/element/valid/script-reference.fxml b/fx2j-parser/src/test/resources/element/valid/script-reference.fxml index d37620d..5cf6e79 100644 --- a/fx2j-parser/src/test/resources/element/valid/script-reference.fxml +++ b/fx2j-parser/src/test/resources/element/valid/script-reference.fxml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/Fx2jProcessor.java b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/Fx2jProcessor.java index eae1a6e..8a24acd 100644 --- a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/Fx2jProcessor.java +++ b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/Fx2jProcessor.java @@ -11,7 +11,7 @@ import com.squareup.javapoet.WildcardTypeName; import io.github.sheikah45.fx2j.api.Fx2jBuilder; import io.github.sheikah45.fx2j.api.Fx2jBuilderFinder; -import io.github.sheikah45.fx2j.processor.internal.StringJavaFileObject; +import io.github.sheikah45.fx2j.processor.internal.model.StringJavaFileObject; import io.github.sheikah45.fx2j.processor.internal.utils.JavaFileUtils; import javax.lang.model.element.Modifier; diff --git a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/FxmlProcessor.java b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/FxmlProcessor.java index de2d3e0..f88b6de 100644 --- a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/FxmlProcessor.java +++ b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/FxmlProcessor.java @@ -14,8 +14,10 @@ import io.github.sheikah45.fx2j.parser.FxmlProcessingInstruction; import io.github.sheikah45.fx2j.parser.attribute.ControllerAttribute; import io.github.sheikah45.fx2j.processor.internal.ObjectNodeProcessor; -import io.github.sheikah45.fx2j.processor.internal.ReflectionResolver; import io.github.sheikah45.fx2j.processor.internal.model.ObjectNodeCode; +import io.github.sheikah45.fx2j.processor.internal.resolve.MethodResolver; +import io.github.sheikah45.fx2j.processor.internal.resolve.ResolverContainer; +import io.github.sheikah45.fx2j.processor.internal.resolve.TypeResolver; import io.github.sheikah45.fx2j.processor.internal.utils.JavaFileUtils; import io.github.sheikah45.fx2j.processor.internal.utils.StringUtils; @@ -41,7 +43,9 @@ public class FxmlProcessor { public static final String BUILDER_PROVIDED_CONTROLLER_NAME = "builderProvidedController"; public static final String BUILDER_PROVIDED_ROOT_NAME = "builderProvidedRoot"; - private final ReflectionResolver resolver; + private final ResolverContainer resolverContainer; + private final TypeResolver typeResolver; + private final MethodResolver methodResolver; private final String rootPackage; private final Class controllerClass; private final JavaFile javaFile; @@ -58,7 +62,6 @@ public class FxmlProcessor { * @param rootPackage The root package for the generated Java code. * @param classLoader The class loader to use for resolving imported classes. */ - @SuppressWarnings("unchecked") public FxmlProcessor(Path filePath, Path resourceRootPath, String rootPackage, ClassLoader classLoader) { this.rootPackage = rootPackage; Path absoluteFilePath = filePath.toAbsolutePath(); @@ -70,7 +73,9 @@ public FxmlProcessor(Path filePath, Path resourceRootPath, String rootPackage, C .map(FxmlProcessingInstruction.Import::value) .collect(Collectors.toSet()); - resolver = new ReflectionResolver(imports, classLoader); + resolverContainer = ResolverContainer.from(imports, classLoader); + typeResolver = resolverContainer.getTypeResolver(); + methodResolver = resolverContainer.getMethodResolver(); controllerClass = fxmlComponents.rootNode() .content() @@ -88,13 +93,14 @@ public FxmlProcessor(Path filePath, Path resourceRootPath, String rootPackage, C custom.name())) .map(FxmlProcessingInstruction.Custom::value) .findFirst()) - .map(typeName -> (Class) resolver.resolveRequired(typeName)) + .map(typeResolver::resolve) .orElse(Object.class); Path absoluteResourceRootPath = resourceRootPath.toAbsolutePath(); - objectNodeCode = new ObjectNodeProcessor(fxmlComponents.rootNode(), controllerClass, resolver, absoluteFilePath, + objectNodeCode = new ObjectNodeProcessor(fxmlComponents.rootNode(), controllerClass, resolverContainer, + absoluteFilePath, absoluteResourceRootPath, this.rootPackage).getNodeCode(); - rootClass = resolver.wrapType(objectNodeCode.nodeClass()); + rootClass = typeResolver.wrapType(objectNodeCode.nodeClass()); relativeFilePath = absoluteResourceRootPath.relativize(absoluteFilePath); String relativePackage = StringUtils.fxmlFileToPackageName(relativeFilePath); @@ -153,7 +159,7 @@ private TypeSpec generateTypeSpec() { if (!controllerClass.isInterface() && !java.lang.reflect.Modifier.isAbstract(controllerClass.getModifiers()) && - resolver.hasDefaultConstructor(controllerClass)) { + methodResolver.hasDefaultConstructor(controllerClass)) { setControllerBuilder.nextControlFlow("else") .addStatement("$L = new $T();", CONTROLLER_NAME, controllerClass); } @@ -187,8 +193,9 @@ private TypeSpec generateTypeSpec() { if (controllerClass != Object.class) { - resolver.findMethodRequiredPublicIfExists(controllerClass, "initialize") - .ifPresent(method -> buildMethodBuilder.addStatement("$L.$L()", CONTROLLER_NAME, method.getName())); + methodResolver.findMethodRequiredPublicIfExists(controllerClass, "initialize") + .ifPresent(method -> buildMethodBuilder.addStatement("$L.$L()", CONTROLLER_NAME, + method.getName())); } List methodSpecs = List.of(getControllerMethodSpec, setControllerMethodSpec, getRootMethodSpec, @@ -225,7 +232,7 @@ public JavaFileObject toJavaFileObject() { * @return The set of required modules. */ public Set getRequiredModules() { - return resolver.getResolvedModules(); + return typeResolver.getResolvedModules(); } /** diff --git a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/ObjectNodeProcessor.java b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/ObjectNodeProcessor.java index bb935e1..8bda749 100644 --- a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/ObjectNodeProcessor.java +++ b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/ObjectNodeProcessor.java @@ -2,16 +2,19 @@ import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; -import io.github.sheikah45.fx2j.parser.ParseException; +import io.github.sheikah45.fx2j.parser.attribute.AssignableAttribute; import io.github.sheikah45.fx2j.parser.attribute.EventHandlerAttribute; import io.github.sheikah45.fx2j.parser.attribute.FxmlAttribute; import io.github.sheikah45.fx2j.parser.attribute.IdAttribute; import io.github.sheikah45.fx2j.parser.attribute.InstancePropertyAttribute; +import io.github.sheikah45.fx2j.parser.attribute.SpecialAttribute; import io.github.sheikah45.fx2j.parser.attribute.StaticPropertyAttribute; +import io.github.sheikah45.fx2j.parser.element.AssignableElement; import io.github.sheikah45.fx2j.parser.element.ClassInstanceElement; import io.github.sheikah45.fx2j.parser.element.ConstantElement; import io.github.sheikah45.fx2j.parser.element.CopyElement; import io.github.sheikah45.fx2j.parser.element.DefineElement; +import io.github.sheikah45.fx2j.parser.element.ElementContent; import io.github.sheikah45.fx2j.parser.element.FactoryElement; import io.github.sheikah45.fx2j.parser.element.IncludeElement; import io.github.sheikah45.fx2j.parser.element.InstanceElement; @@ -21,18 +24,24 @@ import io.github.sheikah45.fx2j.parser.element.ScriptElement; import io.github.sheikah45.fx2j.parser.element.StaticPropertyElement; import io.github.sheikah45.fx2j.parser.element.ValueElement; -import io.github.sheikah45.fx2j.parser.property.Concrete; import io.github.sheikah45.fx2j.parser.property.Expression; import io.github.sheikah45.fx2j.parser.property.FxmlProperty; import io.github.sheikah45.fx2j.parser.property.Handler; import io.github.sheikah45.fx2j.parser.property.Value; import io.github.sheikah45.fx2j.processor.FxmlProcessor; import io.github.sheikah45.fx2j.processor.ProcessorException; +import io.github.sheikah45.fx2j.processor.internal.model.CodeValue; +import io.github.sheikah45.fx2j.processor.internal.model.ExpressionResult; import io.github.sheikah45.fx2j.processor.internal.model.NamedArgValue; import io.github.sheikah45.fx2j.processor.internal.model.ObjectNodeCode; -import io.github.sheikah45.fx2j.processor.internal.utils.StringUtils; +import io.github.sheikah45.fx2j.processor.internal.resolve.ExpressionResolver; +import io.github.sheikah45.fx2j.processor.internal.resolve.MethodResolver; +import io.github.sheikah45.fx2j.processor.internal.resolve.NameResolver; +import io.github.sheikah45.fx2j.processor.internal.resolve.ResolverContainer; +import io.github.sheikah45.fx2j.processor.internal.resolve.TypeResolver; +import io.github.sheikah45.fx2j.processor.internal.resolve.ValueResolver; +import io.github.sheikah45.fx2j.processor.internal.utils.CodeBlockUtils; -import java.lang.invoke.MethodType; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -43,27 +52,17 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.SequencedMap; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; public class ObjectNodeProcessor { - private static final Map, Object> DEFAULTS_MAP = Map.of(byte.class, (byte) 0, short.class, (short) 0, - int.class, 0, long.class, 0L, float.class, 0.0f, - double.class, 0.0d, char.class, '\u0000', - boolean.class, false); - - private static final Pattern CHANGE_PROPERTY_PATTERN = Pattern.compile("on(?.+)Change"); private static final String EVENT_HANDLER_CLASS = "javafx.event.EventHandler"; - private static final String ON_CHANGE = "onChange"; private static final String OBSERVABLE_VALUE_CLASS = "javafx.beans.value.ObservableValue"; private static final String OBSERVABLE_LIST_CLASS = "javafx.collections.ObservableList"; private static final String LIST_CHANGE_CLASS = "javafx.collections.ListChangeListener$Change"; @@ -75,67 +74,76 @@ public class ObjectNodeProcessor { OBSERVABLE_SET_CLASS, SET_CHANGE_CLASS, OBSERVABLE_MAP_CLASS, MAP_CHANGE_CLASS); - private final ReflectionResolver resolver; + private final ResolverContainer resolverContainer; + private final TypeResolver typeResolver; + private final ExpressionResolver expressionResolver; + private final ValueResolver valueResolver; + private final MethodResolver methodResolver; + private final NameResolver nameResolver; private final Path filePath; private final Class controllerClass; private final ClassInstanceElement rootNode; private final Path resourceRootPath; private final String rootPackage; private final ObjectNodeCode nodeCode; - private final String id; + private final String providedId; private final CodeBlock.Builder objectInitializationBuilder = CodeBlock.builder(); - private final List defaultPropertyChildren = new ArrayList<>(); - private final Map instanceProperties = new HashMap<>(); + private final List defaultPropertyElements = new ArrayList<>(); + private final SequencedMap instanceProperties = new LinkedHashMap<>(); private final List staticProperties = new ArrayList<>(); private final List handlerProperties = new ArrayList<>(); private final List definedChildren = new ArrayList<>(); private final List scripts = new ArrayList<>(); - private Type objectClass; - private Class[] typeArguments; + private Type objectType; + private Type[] typeArguments; private String objectIdentifier; - public ObjectNodeProcessor(ClassInstanceElement rootNode, Class controllerClass, ReflectionResolver resolver, - Path filePath, Path resourceRootPath, String rootPackage) { + public ObjectNodeProcessor(ClassInstanceElement rootNode, Class controllerClass, + ResolverContainer resolverContainer, Path filePath, Path resourceRootPath, + String rootPackage) { this.resourceRootPath = resourceRootPath; this.rootPackage = rootPackage; - this.resolver = resolver; this.filePath = filePath; this.controllerClass = controllerClass; this.rootNode = rootNode; - List attributes = rootNode.content().attributes(); - this.id = attributes.stream() - .filter(IdAttribute.class::isInstance) - .map(IdAttribute.class::cast) - .map(IdAttribute::value) - .findFirst() - .orElse(null); + this.resolverContainer = resolverContainer; + this.typeResolver = resolverContainer.getTypeResolver(); + this.methodResolver = resolverContainer.getMethodResolver(); + this.nameResolver = resolverContainer.getNameResolver(); + this.expressionResolver = resolverContainer.getExpressionResolver(); + this.valueResolver = resolverContainer.getValueResolver(); + + List attributes = rootNode.content().attributes(); + this.providedId = attributes.stream() + .filter(IdAttribute.class::isInstance) + .map(IdAttribute.class::cast) + .map(IdAttribute::value) + .findFirst() + .orElse(null); attributes.forEach(attribute -> { switch (attribute) { - case InstancePropertyAttribute instance -> instanceProperties.put(instance.property(), instance); + case InstancePropertyAttribute instanceProperty -> + instanceProperties.put(instanceProperty.propertyName(), instanceProperty); case StaticPropertyAttribute staticProperty -> staticProperties.add(staticProperty); case EventHandlerAttribute handler -> handlerProperties.add(handler); - case FxmlAttribute.SpecialAttribute ignored -> {} + case SpecialAttribute ignored -> {} } }); - rootNode.content().children().forEach(child -> { - switch (child) { - case InstancePropertyElement instance -> instanceProperties.put(instance.property(), instance); - case StaticPropertyElement stat -> staticProperties.add(stat); - case DefineElement(List children) -> definedChildren.addAll(children); - case ClassInstanceElement classInstanceElement -> - defaultPropertyChildren.add(new Concrete.Element(classInstanceElement)); + rootNode.content().elements().forEach(element -> { + switch (element) { + case InstancePropertyElement instanceProperty -> + instanceProperties.put(instanceProperty.propertyName(), instanceProperty); + case StaticPropertyElement staticProperty -> staticProperties.add(staticProperty); + case DefineElement(List elements) -> definedChildren.addAll(elements); + case ClassInstanceElement classInstanceElement -> defaultPropertyElements.add(classInstanceElement); case ScriptElement script -> scripts.add(script); } }); - if (!(rootNode.content().body() instanceof Concrete.Empty)) { - defaultPropertyChildren.add(rootNode.content().body()); - } - nodeCode = processNode(); } @@ -145,13 +153,14 @@ private ObjectNodeCode processNodeInternal() { } processObjectInitialization(); - processDefinedChildren(); - processDefaultProperty(); - processInstanceProperties(); - processStaticProperties(); - processHandlerProperties(); + definedChildren.forEach(this::buildChildNode); + processDefaultPropertyElements(); + processDefaultPropertyValue(); + instanceProperties.values().forEach(this::processInstanceProperty); + handlerProperties.forEach(this::processHandlerProperty); + staticProperties.forEach(this::processStaticProperty); - return new ObjectNodeCode(objectIdentifier, objectClass, objectInitializationBuilder.add("\n").build()); + return new ObjectNodeCode(objectIdentifier, objectType, objectInitializationBuilder.add("\n").build()); } private ObjectNodeCode processNode() { @@ -170,10 +179,10 @@ public ObjectNodeCode getNodeCode() { private void processIncludeInitialization(Path source) { FxmlProcessor includedProcessor = new FxmlProcessor(filePath.resolveSibling(source), resourceRootPath, - rootPackage, resolver.getClassLoader()); - objectClass = resolver.checkResolved(includedProcessor.getRootClass()); + rootPackage, typeResolver.getClassLoader()); + objectType = typeResolver.resolveClassFromType(includedProcessor.getRootClass()); - if (objectClass == null) { + if (objectType == null) { throw new IllegalArgumentException( "Unable to determine object class for %s".formatted(filePath.resolveSibling(source))); } @@ -185,11 +194,11 @@ private void processIncludeInitialization(Path source) { objectInitializationBuilder.addStatement("$1T $2L = new $1T()", includedClassName, builderIdentifier) .addStatement("$L.build(null, null, $L, $L)", builderIdentifier, FxmlProcessor.RESOURCES_NAME, FxmlProcessor.CONTROLLER_FACTORY_NAME) - .addStatement("$T $L = $L.getRoot()", objectClass, objectIdentifier, + .addStatement("$T $L = $L.getRoot()", objectType, objectIdentifier, builderIdentifier); - Class includedControllerClass = resolver.checkResolved(includedProcessor.getControllerClass()); - if (includedControllerClass != Object.class && id != null) { + Class includedControllerClass = typeResolver.resolveClassFromType(includedProcessor.getControllerClass()); + if (includedControllerClass != Object.class && providedId != null) { String controllerIdentifier = objectIdentifier + "Controller"; objectInitializationBuilder.addStatement("$T $L = $L.getController()", includedControllerClass, controllerIdentifier, builderIdentifier); @@ -197,12 +206,8 @@ private void processIncludeInitialization(Path source) { } } - private void processHandlerProperties() { - handlerProperties.forEach(this::processHandlerProperty); - } - private void processConstructorInitialization(String className) { - objectClass = resolver.resolveRequired(className); + objectType = typeResolver.resolve(className); resolveIdentifier(); for (List constructorArgs : getMatchingConstructorArgs()) { @@ -215,210 +220,410 @@ private void processConstructorInitialization(String className) { throw new IllegalArgumentException("Unknown constructor"); } - private void processInstanceProperties() { - Collection instanceProperties = this.instanceProperties.values(); - if (resolver.isAssignableFrom(Map.class, objectClass)) { - Class keyTypeBound = typeArguments == null ? Object.class : typeArguments[0]; - Class valueTypeBound = typeArguments == null ? Object.class : typeArguments[1]; + private void processDefaultPropertyElements() { + if (defaultPropertyElements.isEmpty()) { + return; + } - instanceProperties.forEach( - attribute -> addPropertyToMapWithTypeBounds(CodeBlock.of("$L", objectIdentifier), attribute, - keyTypeBound, valueTypeBound)); + String defaultProperty = methodResolver.resolveDefaultProperty(objectType); + if (defaultProperty != null) { + defaultPropertyElements.forEach(element -> processInstancePropertyElement(defaultProperty, element)); return; } - instanceProperties.forEach(property -> processInstanceProperty(property.property(), property.value())); + if (typeResolver.isAssignableFrom(Collection.class, objectType)) { + Type contentTypeBound = typeArguments == null ? Object.class : typeArguments[0]; + CodeBlock objectBlock = CodeBlock.of("$L", objectIdentifier); + defaultPropertyElements.forEach( + element -> addToCollectionWithTypeBound(objectBlock, element, contentTypeBound)); + return; + } + + throw new IllegalArgumentException("Unable to handle default elements for elements %s".formatted(objectType)); } - private void addPropertyToMapWithTypeBounds(CodeBlock mapBlock, FxmlProperty.Instance property, - Class keyTypeBound, Class valueTypeBound) { - if (!(property.value() instanceof Concrete concrete)) { - throw new IllegalArgumentException("Cannot add non-concrete value to map"); + private void processDefaultPropertyValue() { + Value value = rootNode.content().value(); + if (value instanceof Value.Empty) { + return; } - CodeBlock keyValue = coerceValue(keyTypeBound, property.property()); - CodeBlock valueValue = coerceValue(valueTypeBound, concrete); - objectInitializationBuilder.addStatement("$L.put($L, $L)", mapBlock, keyValue, valueValue); - } - private void processStaticProperties() { - staticProperties.forEach( - property -> processStaticProperty(property.className(), property.property(), property.value())); + String defaultProperty = methodResolver.resolveDefaultProperty(objectType); + if (defaultProperty != null) { + processInstancePropertyValue(defaultProperty, value); + return; + } + + if (typeResolver.isAssignableFrom(Collection.class, objectType)) { + Type contentTypeBound = typeArguments == null ? Object.class : typeArguments[0]; + addToCollectionWithTypeBound(CodeBlock.of("$L", objectIdentifier), value, contentTypeBound); + return; + } + + throw new IllegalArgumentException("Unable to handle default value for elements %s".formatted(objectType)); } - private void processDefinedChildren() { - definedChildren.forEach(this::buildChildNode); + private void processInstanceProperty(FxmlProperty.Instance property) { + switch (property) { + case InstancePropertyElement( + String propertyName, ElementContent content + ) -> processInstantPropertyContent(propertyName, content); + case InstancePropertyAttribute(String propertyName, Value value) -> + processInstancePropertyValue(propertyName, value); + } } - private void processInstanceProperty(String property, Value value) { - switch (value) { - case Concrete.Element(ClassInstanceElement classInstanceElement) -> - processPropertyClassInstanceElement(property, classInstanceElement); - case Concrete.Attribute(FxmlProperty.Instance attribute) -> - processPropertiesOnProperty(property, List.of(attribute)); - case Value.Single single -> processInstancePropertySingle(property, single); - case Value.Multi(List values) -> { - List elements = new ArrayList<>(); - List properties = new ArrayList<>(); - - values.forEach(val -> { - switch (val) { - case Concrete.Element(ClassInstanceElement element) -> elements.add(element); - case Concrete.Element(FxmlProperty.Instance element) -> properties.add(element); - case Concrete.Attribute(FxmlProperty.Instance attribute) -> properties.add(attribute); - case Value.Single single -> processInstancePropertySingle(property, single); - } - }); + private void processInstantPropertyContent(String propertyName, + ElementContent content) { + content.attributes().forEach(attribute -> processAttributeOnProperty(propertyName, attribute)); + content.elements().forEach(element -> processElementOnProperty(propertyName, element)); + processInstancePropertyValue(propertyName, content.value()); + } - if (!elements.isEmpty()) { - processPropertyElements(property, elements); - } - if (!properties.isEmpty()) { - processPropertiesOnProperty(property, properties); + private void processElementOnProperty(String propertyName, AssignableElement element) { + switch (element) { + case ClassInstanceElement classInstanceElement -> + processInstancePropertyElement(propertyName, classInstanceElement); + case InstancePropertyElement( + String key, ElementContent innerContent + ) -> { + if (!innerContent.attributes().isEmpty() || !innerContent.elements().isEmpty()) { + throw new UnsupportedOperationException( + "Cannot resolve property value from content with attributes"); } + + addToPropertyMap(propertyName, key, innerContent.value()); } - default -> throw new UnsupportedOperationException( - "Cannot process value %s for property %s".formatted(value, property)); } } - private void processStaticProperty(String className, String property, Value value) { - switch (value) { - case Concrete.Element(ClassInstanceElement element) -> - processStaticPropertyElement(className, property, element); - case Value.Single single -> processStaticPropertySingle(className, property, single); - default -> throw new UnsupportedOperationException( - "Cannot process value %s for static property %s".formatted(value, property)); + private void processAttributeOnProperty(String propertyName, AssignableAttribute attribute) { + switch (attribute) { + case EventHandlerAttribute(String eventName, Handler handler) when "onChange".equals(eventName) -> + processHandlerOnProperty(propertyName, handler); + case EventHandlerAttribute ignored -> + throw new UnsupportedOperationException("Unknown event handler on property"); + case InstancePropertyAttribute(String key, Value value) -> addToPropertyMap(propertyName, key, value); } } - private void processStaticPropertyElement(String className, String property, ClassInstanceElement propertyElement) { - Class staticPropertyClass = resolver.resolveRequired(className); - Method propertySetter = resolver.resolveStaticSetter(staticPropertyClass, property).orElse(null); + private void addToPropertyMap(String propertyName, String key, Value value) { + Method propertyGetter = methodResolver.resolveGetter(objectType, propertyName) + .orElseThrow(() -> new IllegalArgumentException( + "Unable to find getter for property %s on class %s".formatted( + propertyName, objectType))); - if (propertySetter != null) { - processStaticPropertySetter(propertyElement, propertySetter, staticPropertyClass); - } else { - throw new IllegalStateException( - "Cannot find setter for static property %s.%s".formatted(className, property)); + + Type propertyType = propertyGetter.getGenericReturnType(); + Type[] typeArguments = typeResolver.resolveUpperBoundTypeArguments(propertyType); + if (!typeResolver.isAssignableFrom(Map.class, propertyType)) { + throw new IllegalArgumentException("Property %s does not extend map".formatted(propertyName)); } + + if (typeArguments == null || typeArguments.length != 2) { + throw new IllegalArgumentException( + "Property %s does not represent a map with two type arguments".formatted(propertyName)); + } + addToMapWithTypeBounds(CodeBlock.of("$L.$L()", objectIdentifier, propertyGetter.getName()), key, value, + typeArguments[0], typeArguments[1]); } - private void processStaticPropertySetter(ClassInstanceElement staticProperty, Method propertySetter, - Class staticPropertyClass) { - Class parameterType = propertySetter.getParameterTypes()[0]; + private void processInstancePropertyElement(String propertyName, ClassInstanceElement element) { - if (!resolver.isAssignableFrom(parameterType, objectClass)) { - throw new IllegalArgumentException("First parameter of static property setter does not match node type"); + Method propertySetter = methodResolver.resolveSetter(objectType, propertyName).orElse(null); + if (propertySetter != null) { + ObjectNodeCode nodeCode = buildChildNode(element); + Type parameterType = propertySetter.getGenericParameterTypes()[0]; + + if (!typeResolver.isAssignableFrom(parameterType, nodeCode.nodeClass())) { + throw new IllegalArgumentException( + "Property setter %s does not match node type %s".formatted(propertySetter, + nodeCode.nodeClass())); + } + + objectInitializationBuilder.addStatement("$L.$L($L)", objectIdentifier, propertySetter.getName(), + nodeCode.nodeIdentifier()); + return; } - ObjectNodeCode nodeCode = buildChildNode(staticProperty); - if (!resolver.isAssignableFrom(propertySetter.getParameterTypes()[1], nodeCode.nodeClass())) { + Method propertyGetter = methodResolver.resolveGetter(objectType, propertyName).orElse(null); + if (propertyGetter != null) { + Type propertyGenericType = propertyGetter.getGenericReturnType(); + if (typeResolver.isAssignableFrom(Collection.class, propertyGenericType) && + propertyGenericType instanceof ParameterizedType parameterizedType) { + + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + if (actualTypeArguments.length != 1) { + throw new IllegalArgumentException( + "Unable to resolve contained type for type %s".formatted(parameterizedType)); + } + + addToCollectionWithTypeBound(CodeBlock.of("$L.$L()", objectIdentifier, propertyGetter.getName()), + element, actualTypeArguments[0]); + return; + } + throw new IllegalArgumentException( - "Second parameter of static property setter %s does not match node type %s".formatted( - propertySetter, nodeCode.nodeClass())); + "Unable to process read only property %s type %s".formatted(propertyName, propertyGenericType)); } - objectInitializationBuilder.addStatement("$T.$L($L, $L)", staticPropertyClass, propertySetter.getName(), - objectIdentifier, nodeCode.nodeIdentifier()); + CodeBlock objectBlock = CodeBlock.of("$L", objectIdentifier); + Type[] actualTypeArguments = typeArguments == null ? new Type[]{Object.class, Object.class} : typeArguments; + if (typeResolver.isAssignableFrom(Map.class, objectType)) { + + if (actualTypeArguments.length != 2) { + throw new IllegalArgumentException( + "Unable to resolve key and value type for type %s".formatted(objectType)); + } + + addToMapWithTypeBounds(objectBlock, propertyName, element, actualTypeArguments[0], actualTypeArguments[1]); + return; + } + + throw new IllegalStateException("Unknown property %s".formatted(propertyName)); } - private ObjectNodeCode buildChildNode(ClassInstanceElement childNode) { - ObjectNodeCode nodeCode = new ObjectNodeProcessor(childNode, controllerClass, resolver, filePath, - resourceRootPath, rootPackage).getNodeCode(); - objectInitializationBuilder.add(nodeCode.objectInitializationCode()); - return nodeCode; + private void processInstancePropertyValue(String propertyName, Value value) { + switch (value) { + case Value.Empty ignored -> {} + case Expression expression -> { + Method propertyMethod = methodResolver.resolveProperty(objectType, propertyName) + .orElseThrow(() -> new IllegalArgumentException( + "No property found for expression binding %s".formatted( + propertyName))); + Type valueType = propertyMethod.getGenericReturnType(); + ExpressionResult result = expressionResolver.resolveExpression(expression); + Method bindMethod = methodResolver.findMethod(valueType, "bind", result.type()) + .orElseThrow(() -> new IllegalArgumentException( + "Property %s does not have a bind method".formatted( + propertyName))); + result.initializers() + .stream() + .map(CodeBlockUtils::convertToCodeBlock) + .forEach(objectInitializationBuilder::addStatement); + objectInitializationBuilder.addStatement("$L.$L().$L($L)", objectIdentifier, propertyMethod.getName(), + bindMethod.getName(), result.value()); + } + case Value val -> { + Method propertySetter = methodResolver.resolveSetter(objectType, propertyName).orElse(null); + if (propertySetter != null) { + Type valueType = propertySetter.getGenericParameterTypes()[0]; + CodeBlock valueCode = CodeBlockUtils.convertToCodeBlock( + valueResolver.resolveCodeValue(valueType, val)); + objectInitializationBuilder.addStatement("$L.$L($L)", objectIdentifier, propertySetter.getName(), + valueCode); + return; + } + + Method propertyGetter = methodResolver.resolveGetter(objectType, propertyName).orElse(null); + if (propertyGetter != null) { + Type propertyGenericType = propertyGetter.getGenericReturnType(); + CodeBlock propertyBlock = CodeBlock.of("$L.$L()", objectIdentifier, propertyGetter.getName()); + if (typeResolver.isAssignableFrom(Collection.class, propertyGenericType) && + propertyGenericType instanceof ParameterizedType parameterizedType) { + + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + if (actualTypeArguments.length != 1) { + throw new IllegalArgumentException( + "Unable to resolve contained type for type %s".formatted(parameterizedType)); + } + + addToCollectionWithTypeBound(propertyBlock, val, actualTypeArguments[0]); + return; + } + + if (typeResolver.isAssignableFrom(Map.class, propertyGenericType) && + propertyGenericType instanceof ParameterizedType parameterizedType) { + + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + if (actualTypeArguments.length != 2) { + throw new IllegalArgumentException( + "Unable to resolve key and value type for type %s".formatted(parameterizedType)); + } + + addToMapWithTypeBounds(propertyBlock, propertyName, val, actualTypeArguments[0], + actualTypeArguments[1]); + return; + } + + throw new IllegalArgumentException( + "Unable to process read only property %s type %s".formatted(propertyName, + propertyGenericType)); + } + + if (typeResolver.isAssignableFrom(Map.class, objectType)) { + Type[] actualTypeArguments = typeArguments == null ? + new Type[]{Object.class, Object.class} : + typeArguments; + if (typeArguments != null && typeArguments.length != 2) { + throw new IllegalArgumentException( + "Unable to resolve key and value type for type %s".formatted(objectType)); + } + + addToMapWithTypeBounds(CodeBlock.of("$L", objectIdentifier), propertyName, value, + actualTypeArguments[0], actualTypeArguments[1]); + return; + } + + throw new UnsupportedOperationException( + "Unknown property %s for class %s".formatted(propertyName, objectType)); + } + } } - private boolean propertyIsMutable(String property) { - return resolver.isAssignableFrom(Map.class, objectClass) || - resolver.resolveSetter(objectClass, property).isPresent() || - resolver.resolveGetter(objectClass, property) - .map(Method::getReturnType) - .map(returnType -> resolver.isAssignableFrom(Collection.class, returnType) || - resolver.isAssignableFrom(Map.class, returnType)) - .orElse(false); + private void processStaticProperty(FxmlProperty.Static property) { + switch (property) { + case StaticPropertyElement( + String className, String propertyName, + ElementContent content + ) -> processStaticPropertyContent(className, propertyName, content); + case StaticPropertyAttribute(String className, String propertyName, Value value) -> + processStaticPropertyValue(className, propertyName, value); + } } - private CodeBlock coerceUsingValueOfMethodResults(String valueString, Method valueOfMethod, Type valueType) - throws IllegalAccessException, InvocationTargetException { - if (resolver.isPrimitive(valueType)) { - Object value = valueOfMethod.invoke(null, valueString); - if (Objects.equals(value, Double.POSITIVE_INFINITY) || Objects.equals(value, Float.POSITIVE_INFINITY)) { - return CodeBlock.of("$T.POSITIVE_INFINITY", valueType); + private void processStaticPropertyContent(String className, String propertyName, + ElementContent content) { + if (!content.attributes().isEmpty()) { + throw new UnsupportedOperationException("Cannot handle attributes on static property element"); + } + + if ((!content.elements().isEmpty() && !(content.value() instanceof Value.Empty)) || + content.elements().size() > 1) { + throw new UnsupportedOperationException("Cannot handle multiple values for static property element"); + } + + if (content.elements().size() == 1) { + if (!(content.elements().getFirst() instanceof ClassInstanceElement classInstanceElement)) { + throw new UnsupportedOperationException( + "Cannot handle static property element value that is not a class instance"); } + processStaticPropertyElement(className, propertyName, classInstanceElement); + } + + processStaticPropertyValue(className, propertyName, content.value()); + } + + private void processStaticPropertyElement(String className, String property, ClassInstanceElement element) { + Class staticPropertyClass = typeResolver.resolve(className); + Method propertySetter = methodResolver.resolveStaticSetter(staticPropertyClass, property).orElse(null); + + if (propertySetter != null) { + Type parameterType = propertySetter.getGenericParameterTypes()[0]; - if (Objects.equals(value, Double.NEGATIVE_INFINITY) || Objects.equals(value, Float.NEGATIVE_INFINITY)) { - return CodeBlock.of("$T.NEGATIVE_INFINITY", valueType); + if (!typeResolver.isAssignableFrom(parameterType, objectType)) { + throw new IllegalArgumentException( + "First parameter of static property setter does not match node type"); } - if (Objects.equals(value, Double.NaN) || Objects.equals(value, Float.NaN)) { - return CodeBlock.of("$T.NaN", valueType); + ObjectNodeCode nodeCode = buildChildNode(element); + if (!typeResolver.isAssignableFrom(propertySetter.getParameterTypes()[1], nodeCode.nodeClass())) { + throw new IllegalArgumentException( + "Second parameter of static property setter %s does not match node type %s".formatted( + propertySetter, nodeCode.nodeClass())); } - return CodeBlock.of("$L", value); + objectInitializationBuilder.addStatement("$T.$L($L, $L)", staticPropertyClass, propertySetter.getName(), + objectIdentifier, nodeCode.nodeIdentifier()); + } else { + throw new IllegalStateException( + "Cannot find setter for static property %s.%s".formatted(className, property)); } + } - if (resolver.isAssignableFrom(Enum.class, valueType)) { - Object value = valueOfMethod.invoke(null, valueString); - return CodeBlock.of("$T.$L", valueType, value); + private void addToCollectionWithTypeBound(CodeBlock collectionCodeBlock, Value value, Type contentTypeBound) { + switch (value) { + case Value.Empty() -> {} + case Value.Literal(String val) when val.contains(",") -> Arrays.stream(val.split(",\\s*")) + .map(item -> valueResolver.resolveCodeValue( + contentTypeBound, item)) + .forEach( + valueCode -> objectInitializationBuilder.addStatement( + "$L.add($L)", + collectionCodeBlock, + valueCode)); + case Value val -> objectInitializationBuilder.addStatement("$L.add($L)", collectionCodeBlock, + valueResolver.resolveCodeValue(contentTypeBound, + val)); } + } - return CodeBlock.of("$T.$L($S)", valueType, valueOfMethod.getName(), valueString); + private void addToMapWithTypeBounds(CodeBlock mapBlock, String key, Value value, Type keyTypeBound, + Type valueTypeBound) { + CodeBlock keyValue = CodeBlockUtils.convertToCodeBlock(valueResolver.resolveCodeValue(keyTypeBound, key)); + CodeBlock valueValue = CodeBlockUtils.convertToCodeBlock(valueResolver.resolveCodeValue(valueTypeBound, value)); + objectInitializationBuilder.addStatement("$L.put($L, $L)", mapBlock, keyValue, valueValue); } - private CodeBlock resolveParameterValue(NamedArgValue namedArgValue) { - Class paramType = namedArgValue.parameterType(); - String paramName = namedArgValue.name(); - FxmlProperty.Instance property = instanceProperties.get(paramName); - Value value = property == null ? null : property.value(); - return switch (value) { - case Concrete concrete -> coerceValue(paramType, concrete); - case null -> { - String defaultValue = namedArgValue.defaultValue(); - if (defaultValue.isBlank()) { - yield CodeBlock.of("$L", DEFAULTS_MAP.get(namedArgValue.parameterType())); - } - yield coerceValue(paramType, defaultValue); - } - default -> - throw new UnsupportedOperationException("Cannot resolve parameter value from %s".formatted(value)); - }; + private boolean propertyIsMutable(String property) { + return typeResolver.isAssignableFrom(Map.class, objectType) || + methodResolver.resolveSetter(objectType, property).isPresent() || + methodResolver.resolveGetter(objectType, property) + .map(Method::getReturnType) + .map(returnType -> typeResolver.isAssignableFrom(Collection.class, returnType) || + typeResolver.isAssignableFrom(Map.class, returnType)) + .orElse(false); + } + + private void processStaticPropertyValue(String className, String property, Value value) { + if (value instanceof Value.Empty) { + return; + } + + Class staticPropertyClass = typeResolver.resolve(className); + Method propertySetter = methodResolver.resolveStaticSetter(staticPropertyClass, property).orElse(null); + if (propertySetter == null) { + throw new IllegalArgumentException( + "Unable to find static setter for %s on %s".formatted(property, staticPropertyClass)); + } + + Class[] parameterTypes = propertySetter.getParameterTypes(); + if (!typeResolver.isAssignableFrom(parameterTypes[0], objectType)) { + throw new IllegalArgumentException("First parameter of static property setter does not match node type"); + } + + CodeBlock valueCode = CodeBlockUtils.convertToCodeBlock( + valueResolver.resolveCodeValue(parameterTypes[1], value)); + objectInitializationBuilder.addStatement("$T.$L($L, $L)", staticPropertyClass, propertySetter.getName(), + objectIdentifier, valueCode); } private List> getPossibleConstructorArgs() { - return resolver.getConstructors(objectClass).stream() - .filter(resolver::hasAllNamedArgs) - .map(resolver::getNamedArgs) - .toList(); + return methodResolver.getConstructors(objectType) + .stream() + .filter(methodResolver::hasAllNamedArgs) + .map(methodResolver::getNamedArgs) + .toList(); } private void processRootInitialization(String type) { objectIdentifier = FxmlProcessor.BUILDER_PROVIDED_ROOT_NAME; - objectClass = resolver.resolveRequired(type); + objectType = typeResolver.resolve(type); } - private void processValueInitialization(String className, String value) { - objectClass = resolver.resolveRequired(className); - resolveIdentifier(); - if (objectClass == String.class) { - objectInitializationBuilder.addStatement("$T $L = $S", objectClass, objectIdentifier, value); - return; - } - - - Method method = resolver.findMethodRequiredPublicIfExists(objectClass, "valueOf", String.class) - .orElseThrow(() -> new IllegalArgumentException( - "Class %s does not have a valueOf method".formatted(objectClass))); + private CodeBlock resolveParameterValue(NamedArgValue namedArgValue) { + Class paramType = namedArgValue.parameterType(); + String paramName = namedArgValue.name(); + FxmlProperty.Instance property = instanceProperties.get(paramName); + return switch (property) { + case InstancePropertyElement(String ignored, ElementContent content) -> { + if (!content.attributes().isEmpty() || !content.elements().isEmpty()) { + throw new UnsupportedOperationException( + "Cannot resolve property value from content with attributes"); + } + CodeValue value = valueResolver.resolveCodeValue(paramType, content.value()); + if (!(value instanceof CodeValue.Null)) { + yield CodeBlockUtils.convertToCodeBlock(value); + } - try { - CodeBlock valueCode = coerceUsingValueOfMethodResults(value, method, objectClass); - objectInitializationBuilder.addStatement("$T $L = $L", objectClass, objectIdentifier, valueCode); - } catch (InvocationTargetException | IllegalAccessException | RuntimeException e) { - throw new IllegalArgumentException( - "Value %s not parseable by %s.%s".formatted(value, objectClass, method.getName())); - } + yield CodeBlockUtils.convertToCodeBlock(valueResolver.coerceDefaultValue(namedArgValue)); + } + case InstancePropertyAttribute(String ignored, Value value) -> + CodeBlockUtils.convertToCodeBlock(valueResolver.resolveCodeValue(paramType, value)); + case null -> CodeBlockUtils.convertToCodeBlock(valueResolver.coerceDefaultValue(namedArgValue)); + }; } private Set> getMatchingConstructorArgs() { @@ -471,61 +676,81 @@ private Set> getMatchingConstructorArgs() { private void processObjectInitialization() { objectInitializationBuilder.add("\n"); switch (rootNode) { - case RootElement(String type, ClassInstanceElement.Content ignored) -> processRootInitialization(type); - case ReferenceElement(String source, ClassInstanceElement.Content content) -> + case RootElement(String type, ElementContent ignored) -> processRootInitialization(type); + case ReferenceElement(String source, ElementContent content) -> processReferenceInitialization(source, content); case CopyElement( - String source, ClassInstanceElement.Content ignored + String source, ElementContent ignored ) -> processCopyInitialization(source); case IncludeElement( - Path source, Path ignored1, Charset ignored2, ClassInstanceElement.Content ignored3 + Path source, Path ignored1, Charset ignored2, ElementContent ignored3 ) -> processIncludeInitialization(source); case FactoryElement( - String factoryClassName, String methodName, ClassInstanceElement.Content ignored + String factoryClassName, String methodName, ElementContent ignored ) -> processFactoryBasedInitialization(factoryClassName, methodName); - case ConstantElement(String className, String member, ClassInstanceElement.Content ignored) -> + case ConstantElement(String className, String member, ElementContent ignored) -> processConstantInitialization(className, member); - case ValueElement(String className, Concrete.Literal(String value), ClassInstanceElement.Content ignored) -> + case ValueElement(String className, String value, ElementContent ignored) -> processValueInitialization(className, value); - case InstanceElement(String className, ClassInstanceElement.Content ignored) -> + case InstanceElement(String className, ElementContent ignored) -> processConstructorInitialization(className); default -> throw new UnsupportedOperationException("Unable to initialize object"); } - if (id != null) { - processControllerSetter(objectIdentifier, objectClass); - resolver.resolveSetter(objectClass, "id", String.class) - .ifPresent(method -> objectInitializationBuilder.addStatement("$L.$L($S)", objectIdentifier, - method.getName(), objectIdentifier)); + if (providedId != null) { + processControllerSetter(objectIdentifier, objectType); + methodResolver.resolveSetter(objectType, "id", String.class) + .ifPresent(method -> objectInitializationBuilder.addStatement("$L.$L($S)", objectIdentifier, + method.getName(), + objectIdentifier)); typeArguments = extractTypeArguments(); } } - private void processReferenceInitialization(String source, ClassInstanceElement.Content content) { - objectIdentifier = source; - objectClass = resolver.getStoredClassById(source); - if (!content.attributes().isEmpty() || !content.children().isEmpty()) { - throw new UnsupportedOperationException("References with children or attributes not supported"); + private void processValueInitialization(String className, String value) { + objectType = typeResolver.unwrapType(typeResolver.resolve(className)); + resolveIdentifier(); + if (objectType == String.class) { + objectInitializationBuilder.addStatement("$T $L = $S", objectType, objectIdentifier, value); + return; + } + + + Class wrappedType = typeResolver.wrapType(objectType); + Method method = methodResolver.findMethodRequiredPublicIfExists(wrappedType, "valueOf", String.class) + .orElseThrow(() -> new IllegalArgumentException( + "Class %s does not have a valueOf method".formatted(objectType))); + + + try { + CodeBlock valueCode = CodeBlockUtils.convertToCodeBlock(valueResolver.resolveCodeValue(method, value)); + objectInitializationBuilder.addStatement("$T $L = $L", objectType, objectIdentifier, valueCode); + } catch (InvocationTargetException | IllegalAccessException | RuntimeException e) { + throw new IllegalArgumentException( + "Value %s not parseable by %s.%s".formatted(value, objectType, method.getName())); } + } private void processConstantInitialization(String className, String member) { - Class constantContainerClass = resolver.resolveRequired(className); - objectClass = resolver.resolveFieldTypeRequiredPublic(constantContainerClass, member).orElseThrow(); + Class constantContainerClass = typeResolver.resolve(className); + objectType = methodResolver.resolveFieldRequiredPublicIfExists(constantContainerClass, member) + .map(Field::getType) + .orElseThrow(() -> new IllegalArgumentException( + "Field %s of %s is not found or public".formatted(member, + constantContainerClass))); resolveIdentifier(); - objectInitializationBuilder.addStatement("$T $L = $T.$L", objectClass, objectIdentifier, constantContainerClass, + objectInitializationBuilder.addStatement("$T $L = $T.$L", objectType, objectIdentifier, constantContainerClass, member); } - private void processCopyInitialization(String source) { - objectClass = resolver.getStoredClassById(source); - ; - if (!resolver.hasCopyConstructor(objectClass)) { - throw new IllegalArgumentException("No copy constructor found for class %s".formatted(objectClass)); + private void resolveIdentifier() { + if (providedId != null) { + objectIdentifier = providedId; + nameResolver.storeIdType(objectIdentifier, objectType); + } else { + objectIdentifier = nameResolver.resolveUniqueName(objectType); } - - objectIdentifier = source + "Copy"; - objectInitializationBuilder.addStatement("$1T $2L = new $1T($3L)", objectClass, objectIdentifier, source); } private void buildWithConstructorArgs(List namedArgValues) { @@ -535,21 +760,21 @@ private void buildWithConstructorArgs(List namedArgValues) { namedArgValues.stream().map(NamedArgValue::name).forEach(instanceProperties::remove); - objectInitializationBuilder.addStatement("$T $L = new $T($L)", objectClass, objectIdentifier, objectClass, + objectInitializationBuilder.addStatement("$T $L = new $T($L)", objectType, objectIdentifier, objectType, parameterValues); } private void processFactoryBasedInitialization(String factoryClassName, String factoryMethodName) { - Class factoryClass = resolver.resolveRequired(factoryClassName); - Method factoryMethod = resolver.findMethod(factoryClass, factoryMethodName, 0) - .orElseThrow(() -> new IllegalArgumentException( - "Factory method not found %s.%s".formatted(factoryClassName, - factoryMethodName))); + Class factoryClass = typeResolver.resolve(factoryClassName); + Method factoryMethod = methodResolver.findMethod(factoryClass, factoryMethodName, 0) + .orElseThrow(() -> new IllegalArgumentException( + "Factory method not found %s.%s".formatted(factoryClassName, + factoryMethodName))); - objectClass = factoryMethod.getReturnType(); + objectType = factoryMethod.getReturnType(); resolveIdentifier(); - objectInitializationBuilder.addStatement("$T $L = $T.$L()", objectClass, objectIdentifier, factoryClass, + objectInitializationBuilder.addStatement("$T $L = $T.$L()", objectType, objectIdentifier, factoryClass, factoryMethod.getName()); } @@ -559,103 +784,64 @@ private void processControllerSetter(String identifier, Type valueClass) { } } - private Class[] extractTypeArguments() { - return resolver.resolveSetter(controllerClass, objectIdentifier, objectClass) - .map(Method::getGenericParameterTypes) - .map(types -> types[0]) - .or(() -> resolver.resolveField(controllerClass, objectIdentifier).map(Field::getGenericType)) - .map(resolver::resolveUpperBoundTypeArguments) - .orElse(null); + private Type[] extractTypeArguments() { + return methodResolver.resolveSetter(controllerClass, objectIdentifier, objectType) + .map(Method::getGenericParameterTypes) + .map(types -> types[0]) + .or(() -> methodResolver.resolveField(controllerClass, objectIdentifier) + .map(Field::getGenericType)) + .map(typeResolver::resolveUpperBoundTypeArguments) + .orElse(null); } - private void resolveIdentifier() { - if (id != null) { - objectIdentifier = id; - resolver.storeIdType(objectIdentifier, objectClass); - } else { - objectIdentifier = resolver.getDeconflictedName(objectClass); + private void processReferenceInitialization(String source, ElementContent content) { + objectIdentifier = source; + objectType = nameResolver.resolveTypeById(source); + if (!content.attributes().isEmpty() || !content.elements().isEmpty()) { + throw new UnsupportedOperationException("References with elements or attributes not supported"); } } private void processControllerSettersFromKnownClass(String identifier, Type valueClass) { - resolver.resolveSetterRequiredPublicIfExists(controllerClass, identifier, valueClass) - .map(Method::getName) - .map(methodName -> CodeBlock.of("$L.$L($L)", FxmlProcessor.CONTROLLER_NAME, methodName, identifier)) - .or(() -> resolver.resolveFieldRequiredPublic(controllerClass, identifier) - .filter(field -> resolver.isAssignableFrom(field.getType(), valueClass)) - .map(Field::getName) - .map(fieldName -> CodeBlock.of("$1L.$2L = $2L", FxmlProcessor.CONTROLLER_NAME, - fieldName))) - .ifPresent(objectInitializationBuilder::addStatement); - } - - private void processInstancePropertySingle(String property, Value.Single value) { - if (value instanceof Concrete.Empty) { - return; - } - - if (value instanceof Expression expression) { - Method propertyMethod = resolver.resolveProperty(objectClass, property) - .orElseThrow(() -> new IllegalArgumentException( - "No property found for expression binding %s".formatted(property))); - processPropertyExpression(expression, propertyMethod); - return; - } - - if (value instanceof Concrete concrete) { - Method propertySetter = resolver.resolveSetter(objectClass, property).orElse(null); - if (propertySetter != null) { - processPropertySetter(concrete, propertySetter); - return; - } - } - - Method propertyGetter = resolver.resolveGetter(objectClass, property).orElse(null); - - if (propertyGetter != null) { - Type propertyGenericType = propertyGetter.getGenericReturnType(); - Class propertyClass = propertyGetter.getReturnType(); - String propertyName = objectIdentifier + StringUtils.capitalize(property); - if (resolver.isAssignableFrom(Collection.class, propertyClass) && - propertyGenericType instanceof ParameterizedType parameterizedType && - value instanceof Concrete.Literal(String val)) { - processCollectionInitialization(val, parameterizedType, propertyName, propertyGetter); - return; - } - - throw new IllegalArgumentException( - "Unable to process read only attribute of type %s".formatted(propertyClass)); - } - - throw new UnsupportedOperationException("Unknown property %s for class %s".formatted(property, objectClass)); + methodResolver.resolveSetterRequiredPublicIfExists(controllerClass, identifier, valueClass) + .map(Method::getName) + .map(methodName -> CodeBlock.of("$L.$L($L)", FxmlProcessor.CONTROLLER_NAME, methodName, + identifier)) + .or(() -> methodResolver.resolveFieldRequiredPublicIfExists(controllerClass, identifier) + .filter(field -> typeResolver.isAssignableFrom(field.getType(), + valueClass)) + .map(Field::getName) + .map(fieldName -> CodeBlock.of("$1L.$2L = $2L", + FxmlProcessor.CONTROLLER_NAME, fieldName))) + .ifPresent(objectInitializationBuilder::addStatement); } private void processHandlerProperty(FxmlProperty.EventHandler eventHandler) { String eventName = eventHandler.eventName(); Handler handler = eventHandler.handler(); - Method handlerSetter = resolver.resolveSetterRequiredPublicIfExists(objectClass, eventName, - resolver.resolveRequired( - EVENT_HANDLER_CLASS)).orElse(null); + Method handlerSetter = methodResolver.resolveSetterRequiredPublicIfExists(objectType, eventName, + typeResolver.resolve( + EVENT_HANDLER_CLASS)) + .orElse(null); if (handlerSetter != null) { processHandlerSetter(handler, handlerSetter); return; } - Matcher propertyMatcher = CHANGE_PROPERTY_PATTERN.matcher(eventName); - if (propertyMatcher.matches()) { - String property = propertyMatcher.group("property"); + if (eventName.startsWith("on") && eventName.endsWith("Change")) { + String property = eventName.substring(2, eventName.length() - 6); processPropertyChangeListener(handler, property); return; } - throw new UnsupportedOperationException("Unknown event %s for class %s".formatted(eventName, objectClass)); + throw new UnsupportedOperationException("Unknown event %s for class %s".formatted(eventName, objectType)); } private void processHandlerSetter(Handler handler, Method handlerSetter) { Type handlerType = handlerSetter.getGenericParameterTypes()[0]; Class eventClass; if (handlerType instanceof ParameterizedType parameterizedType) { - Class[] typeArgumentBounds = resolver.resolveLowerBoundTypeArguments(parameterizedType); + Class[] typeArgumentBounds = typeResolver.resolveLowerBoundTypeArguments(parameterizedType); if (typeArgumentBounds.length != 1) { throw new IllegalArgumentException( "Unable to determine bounds of handler type %s".formatted(handlerType)); @@ -670,14 +856,15 @@ private void processHandlerSetter(Handler handler, Method handlerSetter) { } private void processPropertyChangeListener(Handler handler, String property) { - Method propertyMethod = resolver.resolveProperty(objectClass, property) - .orElseThrow(() -> new IllegalArgumentException( - "Unable to find property method for %s".formatted(property))); + Method propertyMethod = methodResolver.resolveProperty(objectType, property) + .orElseThrow(() -> new IllegalArgumentException( + "Unable to find property method for %s".formatted(property))); - Class propertyClass = resolver.resolveGetter(objectClass, property) - .map(Method::getReturnType) - .orElseThrow(() -> new IllegalArgumentException( - "Unable to determine the class of property %s".formatted(property))); + Class propertyClass = methodResolver.resolveGetter(objectType, property) + .map(Method::getReturnType) + .orElseThrow(() -> new IllegalArgumentException( + "Unable to determine the class of property %s".formatted( + property))); CodeBlock valueCode = resolveControllerPropertyChangeListener(propertyClass, handler); objectInitializationBuilder.addStatement("$L.$L().addListener($L)", objectIdentifier, propertyMethod.getName(), @@ -689,43 +876,45 @@ private CodeBlock resolveControllerEventHandler(Class eventType, Handler hand throw new UnsupportedOperationException("None method handlers not supported"); } - return resolver.findMethod(controllerClass, methodName, eventType) - .map(method -> { - if (method.getExceptionTypes().length == 0) { - return CodeBlock.of("$L::$L", FxmlProcessor.CONTROLLER_NAME, method.getName()); - } else { - return CodeBlock.builder() - .add("event -> {\n") - .indent() - .beginControlFlow("try") - .add("$L.$L(event);\n", FxmlProcessor.CONTROLLER_NAME, method.getName()) - .nextControlFlow("catch ($T e)", Exception.class) - .add("throw new $T(e);\n", RuntimeException.class) - .endControlFlow() - .unindent() - .add("}") - .build(); - } - }) - .or(() -> resolver.findMethod(controllerClass, methodName).map(method -> { - if (method.getExceptionTypes().length == 0) { - return CodeBlock.of("event -> $L.$L()", FxmlProcessor.CONTROLLER_NAME, method.getName()); - } else { - return CodeBlock.builder() - .add("event -> {\n") - .indent() - .beginControlFlow("try") - .add("$L.$L();\n", FxmlProcessor.CONTROLLER_NAME, method.getName()) - .nextControlFlow("catch ($T e)", Exception.class) - .add("throw new $T(e);\n", RuntimeException.class) - .endControlFlow() - .unindent() - .add("}") - .build(); - } - })) - .orElseThrow(() -> new IllegalArgumentException( - "No method %s on %s".formatted(methodName, controllerClass))); + return methodResolver.findMethod(controllerClass, methodName, eventType) + .map(method -> { + if (method.getExceptionTypes().length == 0) { + return CodeBlock.of("$L::$L", FxmlProcessor.CONTROLLER_NAME, method.getName()); + } else { + return CodeBlock.builder() + .add("event -> {\n") + .indent() + .beginControlFlow("try") + .add("$L.$L(event);\n", FxmlProcessor.CONTROLLER_NAME, + method.getName()) + .nextControlFlow("catch ($T e)", Exception.class) + .add("throw new $T(e);\n", RuntimeException.class) + .endControlFlow() + .unindent() + .add("}") + .build(); + } + }) + .or(() -> methodResolver.findMethod(controllerClass, methodName).map(method -> { + if (method.getExceptionTypes().length == 0) { + return CodeBlock.of("event -> $L.$L()", FxmlProcessor.CONTROLLER_NAME, + method.getName()); + } else { + return CodeBlock.builder() + .add("event -> {\n") + .indent() + .beginControlFlow("try") + .add("$L.$L();\n", FxmlProcessor.CONTROLLER_NAME, method.getName()) + .nextControlFlow("catch ($T e)", Exception.class) + .add("throw new $T(e);\n", RuntimeException.class) + .endControlFlow() + .unindent() + .add("}") + .build(); + } + })) + .orElseThrow(() -> new IllegalArgumentException( + "No method %s on %s".formatted(methodName, controllerClass))); } private CodeBlock resolveControllerPropertyChangeListener(Class valueClass, Handler handler) { @@ -733,16 +922,16 @@ private CodeBlock resolveControllerPropertyChangeListener(Class valueClass, H throw new UnsupportedOperationException("Non method change listeners not supported"); } - Method changeMethod = resolver.findMethod(controllerClass, methodName, - resolver.resolveRequired(OBSERVABLE_VALUE_CLASS), valueClass, - valueClass) - .orElseThrow(() -> new IllegalArgumentException( - "Unable to find change method for name %s and property type %s".formatted( - methodName, valueClass))); + Method changeMethod = methodResolver.findMethod(controllerClass, methodName, + typeResolver.resolve(OBSERVABLE_VALUE_CLASS), valueClass, + valueClass) + .orElseThrow(() -> new IllegalArgumentException( + "Unable to find change method for name %s and property type %s".formatted( + methodName, valueClass))); Type observableType = changeMethod.getGenericParameterTypes()[0]; if (observableType instanceof ParameterizedType parameterizedType && - resolver.hasNonMatchingWildcardUpperBounds(parameterizedType, valueClass)) { + typeResolver.hasNonMatchingWildcardUpperBounds(parameterizedType, valueClass)) { throw new IllegalArgumentException( "Observable value parameter does not match signature of change listener as Observable Value does not have lower bound %s".formatted( valueClass)); @@ -751,282 +940,37 @@ private CodeBlock resolveControllerPropertyChangeListener(Class valueClass, H return CodeBlock.of("$L::$L", FxmlProcessor.CONTROLLER_NAME, changeMethod.getName()); } - private void processPropertySetter(Concrete value, Method propertySetter) { - if (value instanceof Concrete.Empty) { - return; - } - - Class valueType = propertySetter.getParameterTypes()[0]; - CodeBlock valueCode = coerceValue(valueType, value); - objectInitializationBuilder.addStatement("$L.$L($L)", objectIdentifier, propertySetter.getName(), valueCode); - } - - private void processPropertyExpression(Expression value, Method propertyMethod) { - Type valueType = propertyMethod.getGenericReturnType(); - resolver.findMethod(valueType, "bind", 1); - ExpressionResult result = initializeExpression(value); - objectInitializationBuilder.addStatement("$L.$L().bind($L)", objectIdentifier, propertyMethod.getName(), - result.identifier()); - } - - private ExpressionResult initializeExpression(Expression value) { - return switch (value) { - case Expression.Null() -> new ExpressionResult(Object.class, null); - case Expression.Whole(long val) -> { - String identifier = resolver.getDeconflictedName(long.class); - objectInitializationBuilder.addStatement("$T $L = $L", long.class, identifier, val); - yield new ExpressionResult(long.class, identifier); - } - case Expression.Fraction(double val) -> { - String identifier = resolver.getDeconflictedName(double.class); - objectInitializationBuilder.addStatement("$T $L = $L", double.class, identifier, val); - yield new ExpressionResult(double.class, identifier); - } - case Expression.Boolean(boolean val) -> { - String identifier = resolver.getDeconflictedName(boolean.class); - objectInitializationBuilder.addStatement("$T $L = $L", boolean.class, identifier, val); - yield new ExpressionResult(boolean.class, identifier); - } - case Expression.Str(String val) -> { - String identifier = resolver.getDeconflictedName(String.class); - objectInitializationBuilder.addStatement("$T $L = $L", String.class, identifier, val); - yield new ExpressionResult(String.class, identifier); - } - case Expression.Variable(String name) -> new ExpressionResult(resolver.getStoredTypeById(name), name); - case Expression.PropertyRead(Expression expression, String property) -> { - ExpressionResult expressionResult = initializeExpression(expression); - Method readProperty = resolver.resolveProperty(expressionResult.type(), property) - .orElseThrow(() -> new IllegalArgumentException( - "No property found for expression binding %s".formatted( - property))); - Type valueType = readProperty.getGenericReturnType(); - String identifier = resolver.getDeconflictedName(valueType); - objectInitializationBuilder.addStatement("$T $L = $L.$L()", valueType, identifier, - expressionResult.identifier(), readProperty.getName()); - yield new ExpressionResult(valueType, identifier); - } - case Expression.MethodCall( - Expression expression, String methodName, List args - ) -> { - ExpressionResult expressionResult = initializeExpression(expression); - List argResults = args.stream().map(this::initializeExpression).toList(); - Type[] parameterTypes = argResults.stream() - .map(ExpressionResult::type) - .toArray(Type[]::new); - Method method = resolver.findMethod(expressionResult.type(), methodName, - parameterTypes) - .orElseThrow(() -> new IllegalArgumentException( - "No method found for class %s method name %s and parameters %s".formatted( - expressionResult.type(), methodName, parameterTypes))); - Type valueType = method.getGenericReturnType(); - String identifier = resolver.getDeconflictedName(valueType); - CodeBlock methodArgs = argResults.stream() - .map(ExpressionResult::identifier) - .map(ident -> CodeBlock.of("$S", ident)) - .collect(CodeBlock.joining(", ")); - objectInitializationBuilder.addStatement("$T $L = $L.$L($L)", valueType, identifier, - expressionResult.identifier(), method.getName(), methodArgs); - yield new ExpressionResult(valueType, identifier); - } - case Expression.CollectionAccess(Expression expression, Expression key) -> { - ExpressionResult expressionResult = initializeExpression(expression); - ExpressionResult keyResult = initializeExpression(key); - Class bindingsClass = resolver.resolveRequired("javafx.beans.binding.Bindings"); - Method valueAtMethod = resolver.findMethod(bindingsClass, "valueAt", expressionResult.type(), - keyResult.type()) - .orElseThrow(() -> new IllegalArgumentException( - "Unable to find method to access collection")); - Type valueType = valueAtMethod.getGenericReturnType(); - String identifier = resolver.getDeconflictedName(valueType); - objectInitializationBuilder.addStatement("$T $L = $T.$L($L, $L)", valueType, identifier, bindingsClass, - valueAtMethod.getName(), expressionResult.identifier(), - keyResult.identifier()); - yield new ExpressionResult(valueType, identifier); - } - case Expression.Add(Expression left, Expression right) -> - computeExpressionWithMethod(left, right, "add").or( - () -> computeExpressionWithMethod(left, right, "concat")) - .orElseThrow(() -> new IllegalArgumentException( - "Cannot add %s and %s".formatted(left, - right))); - - case Expression.Subtract(Expression left, Expression right) -> - computeExpressionWithMethod(left, right, "subtract").orElseThrow( - () -> new IllegalArgumentException("Cannot subtract %s and %s".formatted(left, right))); - case Expression.Multiply(Expression left, Expression right) -> - computeExpressionWithMethod(left, right, "multiply").orElseThrow( - () -> new IllegalArgumentException("Cannot multiply %s and %s".formatted(left, right))); - case Expression.Divide(Expression left, Expression right) -> - computeExpressionWithMethod(left, right, "divide").orElseThrow( - () -> new IllegalArgumentException("Cannot divide %s and %s".formatted(left, right))); - case Expression.GreaterThan(Expression left, Expression right) -> - computeExpressionWithMethod(left, right, "greaterThan").orElseThrow( - () -> new IllegalArgumentException("Cannot greaterThan %s and %s".formatted(left, right))); - case Expression.GreaterThanEqual(Expression left, Expression right) -> - computeExpressionWithMethod(left, right, "greaterThanEqual").orElseThrow( - () -> new IllegalArgumentException( - "Cannot greaterThanEqual %s and %s".formatted(left, right))); - case Expression.LessThan(Expression left, Expression right) -> - computeExpressionWithMethod(left, right, "lessThan").orElseThrow( - () -> new IllegalArgumentException("Cannot lessThan %s and %s".formatted(left, right))); - case Expression.LessThanEqual(Expression left, Expression right) -> - computeExpressionWithMethod(left, right, "lessThanEqual").orElseThrow( - () -> new IllegalArgumentException( - "Cannot lessThanEqual %s and %s".formatted(left, right))); - case Expression.Equal(Expression left, Expression right) -> - computeExpressionWithMethod(left, right, "equal").orElseThrow( - () -> new IllegalArgumentException("Cannot equal %s and %s".formatted(left, right))); - case Expression.NotEqual(Expression left, Expression right) -> - computeExpressionWithMethod(left, right, "notEqual").orElseThrow( - () -> new IllegalArgumentException("Cannot notEqual %s and %s".formatted(left, right))); - case Expression.And(Expression left, Expression right) -> - computeExpressionWithMethod(left, right, "and").orElseThrow( - () -> new IllegalArgumentException("Cannot and %s and %s".formatted(left, right))); - case Expression.Or(Expression left, Expression right) -> - computeExpressionWithMethod(left, right, "or").orElseThrow( - () -> new IllegalArgumentException("Cannot or %s and %s".formatted(left, right))); - case Expression.Invert(Expression expression) -> computeExpressionWithMethod(expression, "not").orElseThrow( - () -> new IllegalArgumentException("Cannot not %s".formatted(expression))); - case Expression.Negate(Expression expression) -> - computeExpressionWithMethod(expression, "negate").orElseThrow( - () -> new IllegalArgumentException("Cannot negate %s".formatted(expression))); - case Expression.Modulo(Expression left, Expression right) -> - throw new UnsupportedOperationException("Modulo operation not supported"); - }; - } - - private Optional computeExpressionWithMethod(Expression left, Expression right, - String methodName) { - ExpressionResult leftResult = initializeExpression(left); - ExpressionResult rightResult = initializeExpression(right); - - Method directMethod = resolver.findMethod(leftResult.type(), methodName, rightResult.type()).orElse(null); - if (directMethod != null) { - Type valueType = directMethod.getGenericReturnType(); - String identifier = resolver.getDeconflictedName(valueType); - objectInitializationBuilder.addStatement("$T $L = $L.$L($L)", valueType, identifier, - leftResult.identifier(), directMethod.getName(), - rightResult.identifier()); - return Optional.of(new ExpressionResult(valueType, identifier)); - } - - Class bindingsClass = resolver.resolveRequired("javafx.beans.binding.Bindings"); - Method indirectMethod = resolver.findMethod(bindingsClass, methodName, leftResult.type(), rightResult.type()) - .orElseThrow(() -> new IllegalArgumentException( - "Unable to find method to combine expressions")); - if (indirectMethod != null) { - Type valueType = indirectMethod.getGenericReturnType(); - String identifier = resolver.getDeconflictedName(valueType); - objectInitializationBuilder.addStatement("$T $L = $T.$L($L, $L)", valueType, identifier, bindingsClass, - indirectMethod.getName(), leftResult.identifier(), - rightResult.identifier()); - return Optional.of(new ExpressionResult(valueType, identifier)); - } - - return Optional.empty(); - } - - private Optional computeExpressionWithMethod(Expression value, String methodName) { - ExpressionResult result = initializeExpression(value); - - Method directMethod = resolver.findMethod(result.type(), methodName).orElse(null); - if (directMethod != null) { - Type valueType = directMethod.getGenericReturnType(); - String identifier = resolver.getDeconflictedName(valueType); - objectInitializationBuilder.addStatement("$T $L = $L.$L()", valueType, identifier, result.identifier(), - directMethod.getName()); - return Optional.of(new ExpressionResult(valueType, identifier)); - } - - Class bindingsClass = resolver.resolveRequired("javafx.beans.binding.Bindings"); - Method indirectMethod = resolver.findMethod(bindingsClass, methodName, result.type()).orElse(null); - if (indirectMethod != null) { - Type valueType = indirectMethod.getGenericReturnType(); - String identifier = resolver.getDeconflictedName(valueType); - objectInitializationBuilder.addStatement("$T $L = $T.$L($L)", valueType, identifier, bindingsClass, - indirectMethod.getName(), result.identifier()); - return Optional.of(new ExpressionResult(valueType, identifier)); - } - - return Optional.empty(); - } - - private void processStaticPropertySingle(String className, String property, Value value) { - if (!(value instanceof Concrete concrete)) { - throw new IllegalArgumentException("Cannot set static property with non concrete value"); - } - - Class staticPropertyClass = resolver.resolveRequired(className); - Method propertySetter = resolver.resolveStaticSetter(staticPropertyClass, property).orElse(null); - if (propertySetter == null) { - throw new IllegalArgumentException( - "Unable to find static setter for %s on %s".formatted(property, staticPropertyClass)); - } - Class objectType = propertySetter.getParameterTypes()[0]; - - if (!resolver.isAssignableFrom(objectType, objectClass)) { - throw new IllegalArgumentException("First parameter of static property setter does not match node type"); - } - - Class valueType = propertySetter.getParameterTypes()[1]; - CodeBlock valueCode = coerceValue(valueType, concrete); - objectInitializationBuilder.addStatement("$T.$L($L, $L)", staticPropertyClass, propertySetter.getName(), - objectIdentifier, valueCode); - } - - private void processPropertiesOnProperty(String property, Collection properties) { - Method propertyGetter = resolver.resolveGetter(objectClass, property) - .orElseThrow(() -> new IllegalArgumentException( - "Unable to find getter for property %s on class %s".formatted(property, - objectClass))); - - - properties.forEach(prop -> { - switch (prop) { - case EventHandlerAttribute eventHandler when ON_CHANGE.equals(eventHandler.eventName()) -> { - Handler handler = eventHandler.handler(); - - resolver.resolveProperty(objectClass, property) - .ifPresentOrElse(propertyMethod -> processPropertyChangeListener(handler, property), - () -> processPropertyContainerListener(propertyGetter, handler)); - } - case FxmlProperty.Instance instance -> { - Type propertyClass = propertyGetter.getGenericReturnType(); - Class[] typeArguments = resolver.resolveUpperBoundTypeArguments(propertyClass); - if (typeArguments == null || typeArguments.length != 2) { - throw new IllegalArgumentException( - "Property %s does not represent a map with two type arguments".formatted(property)); - } - - Class keyClass = typeArguments[0]; - Class valueClass = typeArguments[1]; - addPropertyToMapWithTypeBounds(CodeBlock.of("$L.$L()", objectIdentifier, propertyGetter.getName()), - instance, keyClass, valueClass); - } - } - }); + private void processHandlerOnProperty(String propertyName, Handler handler) { + methodResolver.resolveProperty(objectType, propertyName) + .ifPresentOrElse(propertyMethod -> processPropertyChangeListener(handler, propertyName), () -> { + Method propertyGetter = methodResolver.resolveGetter(objectType, propertyName) + .orElseThrow(() -> new IllegalArgumentException( + "Unable to find getter for %s on class %s".formatted( + propertyName, objectType))); + processPropertyContainerListener(propertyGetter, handler); + }); } private CodeBlock resolveControllerContainerChangeListener(Type valueType, Handler handler) { if (!(handler instanceof Handler.Method(String methodName))) { throw new UnsupportedOperationException("Non method handlers not supported"); } - Class valueClass = resolver.resolveClassFromType(valueType); - Class[] boundTypeArguments = resolver.resolveUpperBoundTypeArguments(valueType); + Class valueClass = typeResolver.resolveClassFromType(valueType); + Type[] boundTypeArguments = typeResolver.resolveUpperBoundTypeArguments(valueType); return COLLECTION_LISTENER_MAP.entrySet() .stream() - .filter(entry -> resolver.resolveRequired(entry.getKey()) - .isAssignableFrom(valueClass)) + .filter(entry -> typeResolver.resolve(entry.getKey()) + .isAssignableFrom(valueClass)) .map(Map.Entry::getValue) - .map(resolver::resolveRequired) + .map(typeResolver::resolve) .findFirst() - .flatMap(changeClass -> resolver.findMethod(controllerClass, methodName, - changeClass)) + .flatMap(changeClass -> methodResolver.findMethod(controllerClass, methodName, + changeClass)) .filter(method -> { Type parameterType = method.getGenericParameterTypes()[0]; - return resolver.parameterTypeArgumentsMeetBounds(parameterType, - boundTypeArguments); + return typeResolver.typeArgumentsMeetBounds(parameterType, + boundTypeArguments); }) .map(method -> CodeBlock.of("$L::$L", FxmlProcessor.CONTROLLER_NAME, method.getName())) @@ -1035,206 +979,52 @@ private CodeBlock resolveControllerContainerChangeListener(Type valueType, Handl methodName, valueType))); } - private void processPropertyClassInstanceElement(String property, ClassInstanceElement element) { - Method propertySetter = resolver.resolveSetter(objectClass, property).orElse(null); - if (propertySetter != null) { - processPropertyChildSetter(element, propertySetter); - } else { - processPropertyElements(property, List.of(element)); - } - } - private void processPropertyContainerListener(Method propertyGetter, Handler handler) { CodeBlock listener = resolveControllerContainerChangeListener(propertyGetter.getGenericReturnType(), handler); objectInitializationBuilder.addStatement("$L.$L().addListener($L)", objectIdentifier, propertyGetter.getName(), listener); } - private void processCollectionInitialization(String value, ParameterizedType parameterizedType, String propertyName, - Method propertyGetter) { - objectInitializationBuilder.addStatement("$T $L = $L.$L()", - resolver.resolveTypeNameWithoutVariables(parameterizedType), - propertyName, objectIdentifier, propertyGetter.getName()); - Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); - if (actualTypeArguments.length != 1) { + private void addToCollectionWithTypeBound(CodeBlock collectionCodeBlock, ClassInstanceElement element, + Type contentTypeBound) { + ObjectNodeCode nodeCode = buildChildNode(element); + if (!typeResolver.isAssignableFrom(contentTypeBound, nodeCode.nodeClass())) { throw new IllegalArgumentException( - "Unable to resolve contained type for type %s".formatted(parameterizedType)); + "Content type bound %s does not match node type %s".formatted(contentTypeBound, + nodeCode.nodeClass())); } - Class containedClassBound = resolver.resolveTypeUpperBound(actualTypeArguments[0]); - Arrays.stream(value.split(",\\s*")) - .forEach(val -> addToCollectionWithTypeBound( - CodeBlock.of("$L.$L()", objectIdentifier, propertyGetter.getName()), new Concrete.Literal(val), - containedClassBound)); - } - private void processPropertyChildSetter(ClassInstanceElement propertyNode, Method propertySetter) { - ObjectNodeCode nodeCode = buildChildNode(propertyNode); - - Class parameterType = propertySetter.getParameterTypes()[0]; - if (!resolver.isAssignableFrom(parameterType, nodeCode.nodeClass())) { - throw new IllegalArgumentException( - "Parameter type `%s` does not match node type `%s`".formatted(parameterType, nodeCode.nodeClass())); - } - - objectInitializationBuilder.addStatement("$L.$L($L)", objectIdentifier, propertySetter.getName(), - nodeCode.nodeIdentifier()); + objectInitializationBuilder.addStatement("$L.add($L)", collectionCodeBlock, nodeCode.nodeIdentifier()); } - private void processDefaultProperty() { - if (defaultPropertyChildren.isEmpty()) { - return; - } - - if (resolver.isAssignableFrom(Collection.class, objectClass)) { - Class contentTypeBound = typeArguments == null ? Object.class : typeArguments[0]; - defaultPropertyChildren.forEach(value -> { - if (!(value instanceof Concrete concrete)) { - throw new IllegalArgumentException("Cannot add non concrete value to collection"); - } - addToCollectionWithTypeBound(CodeBlock.of("$L", objectIdentifier), concrete, contentTypeBound); - }); - return; - } - - if (resolver.isAssignableFrom(Map.class, objectClass)) { - List properties = defaultPropertyChildren.stream().map(value -> { - if (value instanceof Concrete.Element(FxmlProperty.Instance element)) { - return element; - } - - if (value instanceof Concrete.Attribute(FxmlProperty.Instance attribute)) { - return attribute; - } - - throw new ParseException("Map property contains a non property element"); - }).toList(); - Class keyTypeBound = typeArguments == null ? Object.class : typeArguments[0]; - Class valueTypeBound = typeArguments == null ? Object.class : typeArguments[1]; - properties.forEach( - attribute -> addPropertyToMapWithTypeBounds(CodeBlock.of("$L", objectIdentifier), attribute, - keyTypeBound, valueTypeBound)); - return; - } + private void processCopyInitialization(String source) { + objectType = nameResolver.resolveTypeById(source); - String defaultProperty = resolver.getDefaultProperty(objectClass); - if (defaultProperty != null) { - processInstanceProperty(defaultProperty, new Value.Multi(defaultPropertyChildren)); - return; + if (!methodResolver.hasCopyConstructor(objectType)) { + throw new IllegalArgumentException("No copy constructor found for class %s".formatted(objectType)); } - - throw new IllegalArgumentException("Unable to handle default children for class %s".formatted(objectClass)); + objectIdentifier = source + "Copy"; + objectInitializationBuilder.addStatement("$1T $2L = new $1T($3L)", objectType, objectIdentifier, source); } - private void processPropertyElements(String property, Collection childNodes) { - Method propertyGetter = resolver.resolveGetter(objectClass, property) - .orElseThrow(() -> new IllegalStateException( - "Cannot find getter for property %s".formatted(property))); - Class propertyClass = propertyGetter.getReturnType(); - - if (resolver.isAssignableFrom(Collection.class, propertyClass)) { - List classInstanceElements = childNodes.stream().map(element -> { - if (!(element instanceof ClassInstanceElement classInstanceElement)) { - throw new ParseException("property element contains a non common element"); - } - - return classInstanceElement; - }).toList(); - - Type genericReturnType = propertyGetter.getGenericReturnType(); - if (!(genericReturnType instanceof ParameterizedType parameterizedType)) { - throw new IllegalArgumentException("Cannot determine bounds of collection"); - } - - Class[] collectionTypeBounds = resolver.resolveUpperBoundTypeArguments(parameterizedType); - if (collectionTypeBounds.length != 1) { - throw new IllegalArgumentException("Cannot determine bounds of collection contents"); - } - - Class contentTypeBound = collectionTypeBounds[0]; - classInstanceElements.forEach(element -> addToCollectionWithTypeBound( - CodeBlock.of("$L.$L()", objectIdentifier, propertyGetter.getName()), new Concrete.Element(element), - contentTypeBound)); - return; + private void addToMapWithTypeBounds(CodeBlock mapBlock, String key, ClassInstanceElement element, Type keyTypeBound, + Type valueTypeBound) { + ObjectNodeCode nodeCode = buildChildNode(element); + if (!typeResolver.isAssignableFrom(valueTypeBound, nodeCode.nodeClass())) { + throw new IllegalArgumentException( + "Content type bound %s does not match node type %s".formatted(valueTypeBound, + nodeCode.nodeClass())); } - throw new UnsupportedOperationException("Unknown read only property type %s".formatted(propertyClass)); - } - - private void addToCollectionWithTypeBound(CodeBlock collectionCodeBlock, Concrete value, - Class contentTypeBound) { - objectInitializationBuilder.addStatement("$L.add($L)", collectionCodeBlock, - coerceValue(contentTypeBound, value)); - } - - private CodeBlock coerceValue(Type valueType, Concrete value) { - return switch (value) { - case Concrete.Element(ClassInstanceElement element) -> { - ObjectNodeCode nodeCode = buildChildNode(element); - if (!resolver.isAssignableFrom(resolver.resolveClassFromType(valueType), nodeCode.nodeClass())) { - throw new IllegalArgumentException( - "Cannot assign %s to %s".formatted(nodeCode.nodeClass(), valueType)); - } - yield CodeBlock.of("$L", nodeCode.nodeIdentifier()); - } - case Concrete.Location ignored -> - throw new UnsupportedOperationException("Location resolution not yet supported"); - case Concrete.Reference(String reference) -> { - Class referenceClass = resolver.getStoredClassById(reference); - if (!resolver.isAssignableFrom(resolver.resolveClassFromType(valueType), referenceClass)) { - throw new IllegalArgumentException("Cannot assign %s to %s".formatted(referenceClass, valueType)); - } - - yield CodeBlock.of("$L", reference); - } - case Concrete.Resource(String resource) when valueType == String.class -> - CodeBlock.of("$1L.getString($2S)", FxmlProcessor.RESOURCES_NAME, resource); - case Concrete.Literal(String val) when valueType == String.class -> CodeBlock.of("$S", val); - case Concrete.Literal(String val) -> coerceValue(valueType, val); - default -> throw new UnsupportedOperationException( - "Cannot create type %s from %s".formatted(valueType, value)); - }; + CodeBlock keyValue = CodeBlockUtils.convertToCodeBlock(valueResolver.resolveCodeValue(keyTypeBound, key)); + objectInitializationBuilder.addStatement("$L.put($L, $L)", mapBlock, keyValue, nodeCode.nodeIdentifier()); } - private CodeBlock coerceValue(Type valueType, String value) { - Class rawType = resolver.resolveClassFromType(valueType); - if (rawType.isArray()) { - Class componentType = rawType.getComponentType(); - CodeBlock arrayInitializer = Arrays.stream(value.split(",")) - .map(componentString -> coerceValue(componentType, componentString)) - .collect(CodeBlock.joining(", ")); - return CodeBlock.of("new $T{$L}", valueType, arrayInitializer); - } - - if (rawType == String.class) { - return CodeBlock.of("$S", value); - } - - if (rawType.isPrimitive()) { - Class boxedType = MethodType.methodType(rawType).wrap().returnType(); - Method method = resolver.findMethod(boxedType, "parse%s".formatted(boxedType.getSimpleName()), String.class) - .orElse(null); - if (method != null) { - try { - return coerceUsingValueOfMethodResults(value, method, boxedType); - } catch (IllegalAccessException | InvocationTargetException ignored) {} - } - } - - Class boxedType = MethodType.methodType(rawType).wrap().returnType(); - Method method = resolver.findMethod(boxedType, "valueOf", String.class).orElse(null); - if (method != null) { - try { - return coerceUsingValueOfMethodResults(value, method, boxedType); - } catch (IllegalAccessException | InvocationTargetException ignored) {} - } - - if (valueType == Object.class) { - return CodeBlock.of("$S", value); - } - - throw new UnsupportedOperationException("Cannot create type %s from %s".formatted(valueType, value)); + private ObjectNodeCode buildChildNode(ClassInstanceElement element) { + ObjectNodeCode nodeCode = new ObjectNodeProcessor(element, controllerClass, resolverContainer, filePath, + resourceRootPath, rootPackage).getNodeCode(); + objectInitializationBuilder.add(nodeCode.objectInitializationCode()); + return nodeCode; } - - private record ExpressionResult(Type type, String identifier) {} } diff --git a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/ReflectionResolver.java b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/ReflectionResolver.java deleted file mode 100644 index 73bf5ca..0000000 --- a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/ReflectionResolver.java +++ /dev/null @@ -1,488 +0,0 @@ -package io.github.sheikah45.fx2j.processor.internal; - -import com.squareup.javapoet.ClassName; -import com.squareup.javapoet.ParameterizedTypeName; -import com.squareup.javapoet.TypeName; -import com.squareup.javapoet.WildcardTypeName; -import io.github.sheikah45.fx2j.processor.internal.model.NamedArgValue; -import io.github.sheikah45.fx2j.processor.internal.utils.StringUtils; - -import java.lang.annotation.Annotation; -import java.lang.invoke.MethodType; -import java.lang.reflect.Constructor; -import java.lang.reflect.Executable; -import java.lang.reflect.Field; -import java.lang.reflect.GenericDeclaration; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.Parameter; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; -import java.lang.reflect.WildcardType; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -@SuppressWarnings("unchecked") -public class ReflectionResolver { - - private static final Set> ALLOWED_LITERALS = Set.of(Byte.class, Short.class, Integer.class, Long.class, - Character.class, Float.class, Double.class, - Boolean.class); - - public static final String NAMED_ARG_CLASS = "javafx.beans.NamedArg"; - public static final String DEFAULT_PROPERTY_CLASS = "javafx.beans.DefaultProperty"; - private final ClassLoader classLoader; - - private final Map nameCounts = new HashMap<>(); - private final Map idTypeMap = new HashMap<>(); - private final Set importPrefixes = new HashSet<>(); - private final Map> resolvedClassesMap = new HashMap<>(); - - public ReflectionResolver(Set imports, ClassLoader classLoader) { - this.classLoader = classLoader; - resolveImports(imports); - } - - public boolean isResolvable(String typeName) { - return resolve(typeName) != null; - } - - public Class checkResolved(Type type) { - Class clazz = extractClass(type); - Class previousValue = resolvedClassesMap.put(clazz.getCanonicalName(), clazz); - if (previousValue != null && previousValue != clazz) { - throw new IllegalArgumentException("Class canonical name already resolved with a different class"); - } - return clazz; - } - - public Class resolveRequired(String typeName) { - Class clazz = resolve(typeName); - if (clazz == null) { - throw new IllegalArgumentException("Unable to find class for %s".formatted(typeName)); - } - - return clazz; - } - - private Class resolve(String typeName) { - return resolvedClassesMap.computeIfAbsent(typeName, this::resolveInternal); - } - - private Class resolveInternal(String typeName) { - try { - return Class.forName(typeName, false, classLoader); - } catch (ClassNotFoundException ignored) { - } - - for (String importPrefix : importPrefixes) { - String fullName = importPrefix + "." + typeName; - try { - return Class.forName(fullName, false, classLoader); - } catch (ClassNotFoundException ignored) { - } - } - - String subclassName = typeName; - while (subclassName.contains(".")) { - subclassName = StringUtils.replaceLast(subclassName, ".", "$"); - Class clazz = resolveInternal(subclassName); - if (clazz != null) { - return clazz; - } - } - - return null; - } - - private void resolveImports(Set imports) { - Map> splitImportsMap = imports.stream() - .collect(Collectors.partitioningBy( - importString -> importString.endsWith(".*"))); - - splitImportsMap.getOrDefault(true, List.of()) - .stream() - .map(importString -> importString.substring(0, importString.length() - 2)) - .forEach(importPrefixes::add); - - splitImportsMap.getOrDefault(false, List.of()).forEach(importString -> { - Class type = resolveRequired(importString); - String simpleName = StringUtils.substringAfterLast(importString, "."); - resolvedClassesMap.put(simpleName, type); - }); - } - - public boolean hasCopyConstructor(Type type) { - try { - Class clazz = extractClass(type); - return Modifier.isPublic(clazz.getConstructor(clazz).getModifiers()); - } catch (NoSuchMethodException e) { - return false; - } - } - - public boolean hasDefaultConstructor(Type type) { - try { - return Modifier.isPublic(extractClass(type).getConstructor().getModifiers()); - } catch (NoSuchMethodException e) { - return false; - } - } - - public Optional> resolveFieldTypeRequiredPublic(Type type, String fieldName) { - return resolveFieldRequiredPublic(type, fieldName).map(Field::getType); - } - - public Optional resolveField(Type type, String fieldName) { - try { - return Optional.of(extractClass(type).getField(fieldName)); - } catch (NoSuchFieldException e) { - return Optional.empty(); - } - } - - public Optional resolveFieldRequiredPublic(Type type, String fieldName) { - Class clazz = extractClass(type); - try { - Field field = clazz.getDeclaredField(fieldName); - if (!Modifier.isPublic(field.getModifiers())) { - throw new IllegalArgumentException("%s is not public from %s".formatted(field, clazz)); - } - - return Optional.of(field); - } catch (NoSuchFieldException ignored) {} - - try { - return Optional.of(clazz.getField(fieldName)); - } catch (NoSuchFieldException e) { - return Optional.empty(); - } - } - - public Class resolveClassFromType(Type type) { - return switch (type) { - case Class clazz -> clazz; - case ParameterizedType parameterizedType -> resolveClassFromType(parameterizedType.getRawType()); - case WildcardType wildcardType -> { - Type[] upperBounds = wildcardType.getUpperBounds(); - if (upperBounds.length != 1) { - throw new IllegalArgumentException("Type does not have exactly one upper bound"); - } - yield resolveClassFromType(upperBounds[0]); - } - case null, default -> - throw new UnsupportedOperationException("Unable to get class from type %s".formatted(type)); - }; - } - - public Class[] resolveUpperBoundTypeArguments(Type type) { - if (type instanceof ParameterizedType parameterizedType) { - return Arrays.stream(parameterizedType.getActualTypeArguments()) - .map(this::resolveTypeUpperBound) - .toArray(Class[]::new); - } else { - return null; - } - } - - public Class resolveTypeUpperBound(Type type) { - return switch (type) { - case Class clazz -> clazz; - case ParameterizedType parameterizedType -> resolveTypeUpperBound(parameterizedType.getRawType()); - case WildcardType wildcardType -> { - Type[] upperBounds = wildcardType.getUpperBounds(); - if (upperBounds.length != 1) { - throw new IllegalArgumentException("Type does not have exactly one upper bound"); - } - yield resolveTypeUpperBound(upperBounds[0]); - } - case null, default -> - throw new UnsupportedOperationException("Cannot resolve upper bound of type %s".formatted(type)); - }; - } - - public Class[] resolveLowerBoundTypeArguments(Type type) { - if (type instanceof ParameterizedType parameterizedType) { - return Arrays.stream(parameterizedType.getActualTypeArguments()) - .map(this::resolveTypeLowerBound) - .toArray(Class[]::new); - } else { - return null; - } - } - - public Class resolveTypeLowerBound(Type type) { - return switch (type) { - case Class clazz -> clazz; - case ParameterizedType parameterizedType -> resolveTypeLowerBound(parameterizedType.getRawType()); - case WildcardType wildcardType -> { - Type[] lowerBounds = wildcardType.getLowerBounds(); - if (lowerBounds.length != 1) { - throw new IllegalArgumentException("Type does not have exactly one lower bound"); - } - yield resolveTypeUpperBound(lowerBounds[0]); - } - case null, default -> - throw new UnsupportedOperationException("Cannot resolve lower bound of type %s".formatted(type)); - }; - } - - public boolean parameterTypeArgumentsMeetBounds(Type parameterType, Class[] boundTypeArguments) { - if (!(parameterType instanceof ParameterizedType parameterizedType)) { - return true; - } - - Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); - if (actualTypeArguments.length != boundTypeArguments.length) { - return false; - } - - for (int i = 0; i < actualTypeArguments.length; i++) { - Type typeArgument = actualTypeArguments[i]; - Class wildcardBound = boundTypeArguments[i]; - if (typeArgument instanceof ParameterizedType parameterizedTypeArgument && - hasNonMatchingWildcardUpperBounds(parameterizedTypeArgument, wildcardBound)) { - return false; - } - } - - return true; - } - - public boolean hasNonMatchingWildcardUpperBounds(ParameterizedType parameterizedType, Type desiredUpperBound) { - Type actualTypeArgument = parameterizedType.getActualTypeArguments()[0]; - if (!(actualTypeArgument instanceof WildcardType wildcardType)) { - return true; - } - - Type[] upperBounds = wildcardType.getUpperBounds(); - if (upperBounds.length != 1) { - return true; - } - - Type upperBound = upperBounds[0]; - return upperBound != desiredUpperBound; - } - - public Optional findMethod(Type type, String name, Type... parameterTypes) { - int numParameters = parameterTypes.length; - return Arrays.stream(extractClass(type).getMethods()) - .filter(method -> method.getName().equals(name)) - .filter(method -> method.getParameterCount() == numParameters) - .filter(method -> { - Map> typeParameters = new HashMap<>(); - Arrays.stream(method.getDeclaringClass().getTypeParameters()) - .forEach(typeVariable -> typeParameters.put(typeVariable.getName(), typeVariable)); - Arrays.stream(method.getTypeParameters()) - .forEach(typeVariable -> typeParameters.put(typeVariable.getName(), typeVariable)); - Type[] methodParameterTypes = method.getGenericParameterTypes(); - for (int i = 0; i < numParameters; i++) { - if (!(isAssignableFrom(methodParameterTypes[i], parameterTypes[i]))) { - return false; - } - } - return true; - }) - .findFirst(); - } - - public Optional resolveGetter(Type type, String name) { - return findMethod(type, "get%s".formatted(StringUtils.capitalize(name)), 0); - } - - public Optional findMethod(Type type, String name, int paramCount) { - return Arrays.stream(extractClass(type).getMethods()) - .filter(method -> method.getName().equals(name)) - .filter(method -> method.getParameterCount() == paramCount) - .findFirst(); - } - - public Optional resolveProperty(Type type, String name) { - return findMethod(type, "%sProperty".formatted(StringUtils.camelCase(name)), 0); - } - - public Optional resolveSetter(Type type, String name) { - return findMethod(type, "set%s".formatted(StringUtils.capitalize(name)), 1); - } - - public Optional resolveSetter(Type type, String name, Type valueClass) { - return findMethod(type, "set%s".formatted(StringUtils.capitalize(name)), valueClass); - } - - public Optional resolveSetterRequiredPublicIfExists(Type type, String name, Type valueClass) { - return findMethodRequiredPublicIfExists(type, "set%s".formatted(StringUtils.capitalize(name)), valueClass); - } - - public Optional findMethodRequiredPublicIfExists(Type type, String name, Type... parameterTypes) { - Class clazz = extractClass(type); - Class[] parameterClasses = Arrays.stream(parameterTypes).map(this::extractClass).toArray(Class[]::new); - try { - Method method = clazz.getDeclaredMethod(name, parameterClasses); - if (!Modifier.isPublic(method.getModifiers())) { - throw new IllegalArgumentException("%s is not public from %s".formatted(method, clazz)); - } - - return Optional.of(method); - } catch (NoSuchMethodException ignored) {} - - try { - return Optional.of(clazz.getMethod(name, parameterClasses)); - } catch (NoSuchMethodException e) { - return Optional.empty(); - } - } - - public Optional resolveStaticSetter(Type type, String name) { - return findMethod(type, "set%s".formatted(StringUtils.capitalize(name)), 2); - } - - public String getDefaultProperty(Type type) { - Class clazz = extractClass(type); - Class defaultPropertyClass = (Class) resolveRequired(DEFAULT_PROPERTY_CLASS); - Annotation defaultProperty = clazz.getAnnotation(defaultPropertyClass); - if (defaultProperty == null) { - return null; - } - - try { - Method valueMethod = defaultProperty.getClass().getMethod("value"); - return (String) valueMethod.invoke(defaultProperty); - } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - - public List getNamedArgs(Executable executable) { - return Arrays.stream(executable.getParameters()).map(this::getNamedArg).toList(); - } - - public NamedArgValue getNamedArg(Parameter parameter) { - Class namedArgClass = (Class) resolveRequired(NAMED_ARG_CLASS); - Annotation namedArg = parameter.getAnnotation(namedArgClass); - if (namedArg == null) { - throw new IllegalArgumentException( - "Parameter does not have a NamedArg annotation: %s".formatted(parameter.getDeclaringExecutable())); - } - - try { - Method valueMethod = namedArg.getClass().getMethod("value"); - Method defaultValueMethod = namedArg.getClass().getMethod("defaultValue"); - return new NamedArgValue(parameter.getType(), (String) valueMethod.invoke(namedArg), - (String) defaultValueMethod.invoke(namedArg)); - } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - - public boolean hasAllNamedArgs(Constructor constructor) { - return Arrays.stream(constructor.getParameters()).allMatch(parameter -> { - Class defaultPropertyClass = (Class) resolveRequired(NAMED_ARG_CLASS); - return parameter.getAnnotation(defaultPropertyClass) != null; - }); - } - - public ClassLoader getClassLoader() { - return classLoader; - } - - public Set getResolvedModules() { - return resolvedClassesMap.values() - .stream() - .map(Class::getModule) - .filter(Objects::nonNull) - .map(Module::getName) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - } - - public TypeName resolveTypeNameWithoutVariables(Type type) { - return switch (type) { - case Class clazz -> ClassName.get(clazz); - case ParameterizedType parameterizedType -> { - TypeName[] typeNames = Arrays.stream(parameterizedType.getActualTypeArguments()) - .map(this::resolveTypeNameWithoutVariables) - .toArray(TypeName[]::new); - Type rawType = parameterizedType.getRawType(); - if (!(rawType instanceof Class rawClass)) { - throw new UnsupportedOperationException( - "Unable to resolve type name for parameterized type that isn't a class %s".formatted( - rawType)); - } - yield ParameterizedTypeName.get(ClassName.get(rawClass), typeNames); - } - case WildcardType wildcardType -> WildcardTypeName.get(wildcardType); - case TypeVariable typeVariable -> { - Type[] bounds = typeVariable.getBounds(); - if (bounds.length != 1) { - throw new UnsupportedOperationException( - "Unable to resolve type name for multiple bounds for type %s".formatted(typeVariable)); - } - - yield WildcardTypeName.subtypeOf(resolveTypeNameWithoutVariables(bounds[0])); - } - case null -> null; - default -> TypeName.get(type); - }; - } - - public String getDeconflictedName(Type type) { - Class clazz = extractClass(type); - String rawIdentifier = StringUtils.camelCase(clazz.getSimpleName()); - Integer nameCount = nameCounts.compute(rawIdentifier, (key, value) -> value == null ? 0 : value + 1); - String identifier = rawIdentifier + nameCount; - storeIdType(identifier, type); - return identifier; - } - - public void storeIdType(String id, Type type) { - if (idTypeMap.put(id, type) != null) { - throw new IllegalStateException("Multiple objects have the same id %s".formatted(id)); - } - } - - public Type getStoredTypeById(String id) { - return idTypeMap.computeIfAbsent(id, key -> { - throw new IllegalStateException("No type known for id %s".formatted(id)); - }); - } - - public Class getStoredClassById(String id) { - return extractClass(idTypeMap.computeIfAbsent(id, key -> { - throw new IllegalStateException("No type known for id %s".formatted(id)); - })); - } - - public List> getConstructors(Type type) { - return List.of(extractClass(type).getConstructors()); - } - - public boolean isPrimitive(Type type) { - Class clazz = extractClass(type); - return clazz.isPrimitive() || MethodType.methodType(clazz).hasWrappers(); - } - - public Class wrapType(Type type) { - return MethodType.methodType(extractClass(type)).wrap().returnType(); - } - - private Class extractClass(Type type) { - return switch (type) { - case Class clazz -> clazz; - case ParameterizedType parameterizedType -> extractClass(parameterizedType.getRawType()); - default -> throw new IllegalArgumentException("Unable to extract class from type %s".formatted(type)); - }; - } - - public boolean isAssignableFrom(Type baseType, Type checkedType) { - return extractClass(baseType).isAssignableFrom(extractClass(checkedType)); - } -} diff --git a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/model/CodeValue.java b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/model/CodeValue.java new file mode 100644 index 0000000..ea5b0e3 --- /dev/null +++ b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/model/CodeValue.java @@ -0,0 +1,68 @@ +package io.github.sheikah45.fx2j.processor.internal.model; + +import java.util.List; +import java.util.Objects; + +public sealed interface CodeValue { + + sealed interface Statement extends CodeValue {} + sealed interface Expression extends CodeValue {} + + sealed interface ArrayInitialization extends CodeValue { + record Declared(java.lang.reflect.Type componentType, List values) implements ArrayInitialization { + public Declared { + Objects.requireNonNull(componentType, "componentType cannot be null"); + Objects.requireNonNull(values, "values cannot be null"); + values = List.copyOf(values); + } + } + record Sized(java.lang.reflect.Type componentType, int size) implements ArrayInitialization { + public Sized { + Objects.requireNonNull(componentType, "componentType cannot be null"); + } + } + } + record Null() implements CodeValue {} + record Char(char value) implements CodeValue {} + record Literal(java.lang.String value) implements CodeValue { + public Literal { + Objects.requireNonNull(value, "value cannot be null"); + } + } + record String(java.lang.String value) implements CodeValue { + public String { + Objects.requireNonNull(value, "value cannot be null"); + } + } + record Type(java.lang.reflect.Type type) implements CodeValue { + public Type { + Objects.requireNonNull(type, "type cannot be null"); + } + } + record Enum(java.lang.Enum value) implements CodeValue { + public Enum { + Objects.requireNonNull(value, "value cannot be null"); + } + } + record FieldAccess(CodeValue receiver, java.lang.String field) implements CodeValue { + public FieldAccess { + Objects.requireNonNull(receiver, "receiver cannot be null"); + Objects.requireNonNull(field, "field cannot be null"); + } + } + record MethodCall(CodeValue receiver, java.lang.String method, List args) implements Expression { + public MethodCall { + Objects.requireNonNull(receiver, "receiver cannot be null"); + Objects.requireNonNull(method, "method cannot be null"); + Objects.requireNonNull(args, "args cannot be null"); + args = List.copyOf(args); + } + } + record Assignment(java.lang.reflect.Type type, java.lang.String identifier, CodeValue value) implements Statement { + public Assignment { + Objects.requireNonNull(type, "type cannot be null"); + Objects.requireNonNull(identifier, "value cannot be null"); + Objects.requireNonNull(value, "value cannot be null"); + } + } +} diff --git a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/model/ExpressionResult.java b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/model/ExpressionResult.java new file mode 100644 index 0000000..51bd365 --- /dev/null +++ b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/model/ExpressionResult.java @@ -0,0 +1,14 @@ +package io.github.sheikah45.fx2j.processor.internal.model; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Objects; + +public record ExpressionResult(Type type, String value, List initializers) { + public ExpressionResult { + Objects.requireNonNull(type, "type cannot be null"); + Objects.requireNonNull(value, "value cannot be null"); + Objects.requireNonNull(initializers, "initializers cannot be null"); + initializers = List.copyOf(initializers); + } +} diff --git a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/StringJavaFileObject.java b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/model/StringJavaFileObject.java similarity index 89% rename from fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/StringJavaFileObject.java rename to fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/model/StringJavaFileObject.java index 89217fe..08ffa3d 100644 --- a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/StringJavaFileObject.java +++ b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/model/StringJavaFileObject.java @@ -1,4 +1,4 @@ -package io.github.sheikah45.fx2j.processor.internal; +package io.github.sheikah45.fx2j.processor.internal.model; import javax.tools.SimpleJavaFileObject; import java.net.URI; diff --git a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/ExpressionResolver.java b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/ExpressionResolver.java new file mode 100644 index 0000000..eaad2b5 --- /dev/null +++ b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/ExpressionResolver.java @@ -0,0 +1,206 @@ +package io.github.sheikah45.fx2j.processor.internal.resolve; + +import io.github.sheikah45.fx2j.parser.property.Expression; +import io.github.sheikah45.fx2j.processor.internal.model.CodeValue; +import io.github.sheikah45.fx2j.processor.internal.model.ExpressionResult; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +public class ExpressionResolver { + + private static final String BINDINGS_CLASS_NAME = "javafx.beans.binding.Bindings"; + + private final TypeResolver typeResolver; + private final MethodResolver methodResolver; + private final NameResolver nameResolver; + + ExpressionResolver(TypeResolver typeResolver, MethodResolver methodResolver, NameResolver nameResolver) { + this.typeResolver = typeResolver; + this.methodResolver = methodResolver; + this.nameResolver = nameResolver; + } + + public ExpressionResult resolveExpression(Expression value) { + return switch (value) { + case Expression.Null() -> new ExpressionResult(Object.class, "null", List.of()); + case Expression.Whole(long val) when val > Integer.MAX_VALUE || val < Integer.MIN_VALUE -> + new ExpressionResult(long.class, String.valueOf(val), List.of()); + case Expression.Whole(long val) -> new ExpressionResult(int.class, String.valueOf(val), List.of()); + case Expression.Fraction(double val) when val > Float.MAX_VALUE || val < Float.MIN_VALUE -> + new ExpressionResult(double.class, String.valueOf(val), List.of()); + case Expression.Fraction(double val) -> new ExpressionResult(float.class, String.valueOf(val), List.of()); + case Expression.Boolean(boolean val) -> new ExpressionResult(boolean.class, String.valueOf(val), List.of()); + case Expression.String(String val) -> new ExpressionResult(String.class, "\"" + val + "\"", List.of()); + case Expression.Variable(String name) -> + new ExpressionResult(nameResolver.resolveTypeById(name), name, List.of()); + case Expression.PropertyRead(Expression expression, String property) -> { + ExpressionResult expressionResult = resolveExpression(expression); + List initializers = new ArrayList<>(expressionResult.initializers()); + + Method readProperty = methodResolver.resolveProperty(expressionResult.type(), property) + .orElseThrow(() -> new IllegalArgumentException( + "No property found for expression binding %s".formatted( + property))); + Type valueType = readProperty.getGenericReturnType(); + String identifier = nameResolver.resolveUniqueName(valueType); + + initializers.add(new CodeValue.Assignment(valueType, identifier, new CodeValue.MethodCall( + new CodeValue.Literal(expressionResult.value()), readProperty.getName(), List.of()))); + yield new ExpressionResult(valueType, identifier, initializers); + } + case Expression.MethodCall( + Expression expression, String methodName, List args + ) -> { + ExpressionResult expressionResult = resolveExpression(expression); + List initializers = new ArrayList<>(expressionResult.initializers()); + List parameterTypes = new ArrayList<>(); + List methodArgs = new ArrayList<>(); + for (Expression arg : args) { + ExpressionResult argResult = resolveExpression(arg); + parameterTypes.add(argResult.type()); + methodArgs.add(new CodeValue.Literal(argResult.value())); + initializers.addAll(argResult.initializers()); + } + + Method method = methodResolver.findMethod(expressionResult.type(), methodName, + parameterTypes.toArray(Type[]::new)) + .orElseThrow(() -> new IllegalArgumentException( + "No method found for class %s method name %s and parameters %s".formatted( + expressionResult.type(), methodName, parameterTypes))); + Type valueType = method.getGenericReturnType(); + String identifier = nameResolver.resolveUniqueName(valueType); + + initializers.add(new CodeValue.Assignment(valueType, identifier, new CodeValue.MethodCall( + new CodeValue.Literal(expressionResult.value()), method.getName(), methodArgs))); + yield new ExpressionResult(valueType, identifier, initializers); + } + case Expression.CollectionAccess(Expression expression, Expression key) -> { + ExpressionResult expressionResult = resolveExpression(expression); + ExpressionResult keyResult = resolveExpression(key); + List initializers = new ArrayList<>(); + initializers.addAll(expressionResult.initializers()); + initializers.addAll(keyResult.initializers()); + Class bindingsClass = typeResolver.resolve(BINDINGS_CLASS_NAME); + Method valueAtMethod = methodResolver.findMethod(bindingsClass, "valueAt", expressionResult.type(), + keyResult.type()) + .orElseThrow(() -> new IllegalArgumentException( + "Unable to find method to access collection")); + Type valueType = valueAtMethod.getGenericReturnType(); + String identifier = nameResolver.resolveUniqueName(valueType); + initializers.add(new CodeValue.Assignment(valueType, identifier, new CodeValue.MethodCall( + new CodeValue.Type(bindingsClass), valueAtMethod.getName(), + List.of(new CodeValue.Literal(expressionResult.value()), + new CodeValue.Literal(keyResult.value()))))); + yield new ExpressionResult(valueType, identifier, initializers); + } + case Expression.Add(Expression left, Expression right) -> + computeExpressionWithMethod(left, right, "add", "concat"); + case Expression.Subtract(Expression left, Expression right) -> + computeExpressionWithMethod(left, right, "subtract"); + case Expression.Multiply(Expression left, Expression right) -> + computeExpressionWithMethod(left, right, "multiply"); + case Expression.Divide(Expression left, Expression right) -> + computeExpressionWithMethod(left, right, "divide"); + case Expression.GreaterThan(Expression left, Expression right) -> + computeExpressionWithMethod(left, right, "greaterThan"); + case Expression.GreaterThanEqual(Expression left, Expression right) -> + computeExpressionWithMethod(left, right, "greaterThanOrEqualTo", "greaterThanOrEqual"); + case Expression.LessThan(Expression left, Expression right) -> + computeExpressionWithMethod(left, right, "lessThan"); + case Expression.LessThanEqual(Expression left, Expression right) -> + computeExpressionWithMethod(left, right, "lessThanOrEqualTo", "lessThanOrEqual"); + case Expression.Equal(Expression left, Expression right) -> + computeExpressionWithMethod(left, right, "isEqualTo", "equal"); + case Expression.NotEqual(Expression left, Expression right) -> + computeExpressionWithMethod(left, right, "isNotEqualTo", "notEqual"); + case Expression.And(Expression left, Expression right) -> computeExpressionWithMethod(left, right, "and"); + case Expression.Or(Expression left, Expression right) -> computeExpressionWithMethod(left, right, "or"); + case Expression.Invert(Expression expression) -> computeExpressionWithMethod(expression, "not"); + case Expression.Negate(Expression expression) -> computeExpressionWithMethod(expression, "negate"); + case Expression.Modulo ignored -> + throw new UnsupportedOperationException("Modulo operation in expression not supported"); + }; + } + + private ExpressionResult computeExpressionWithMethod(Expression left, Expression right, String... methodNames) { + ExpressionResult leftResult = resolveExpression(left); + ExpressionResult rightResult = resolveExpression(right); + List initializers = new ArrayList<>(); + initializers.addAll(leftResult.initializers()); + initializers.addAll(rightResult.initializers()); + + Method directMethod = Arrays.stream(methodNames) + .map(methodName -> methodResolver.findMethod(leftResult.type(), methodName, + rightResult.type())) + .flatMap(Optional::stream) + .findFirst() + .orElse(null); + if (directMethod != null) { + Type valueType = directMethod.getGenericReturnType(); + String identifier = nameResolver.resolveUniqueName(valueType); + initializers.add(new CodeValue.Assignment(valueType, identifier, new CodeValue.MethodCall( + new CodeValue.Literal(leftResult.value()), directMethod.getName(), + List.of(new CodeValue.Literal(rightResult.value()))))); + return new ExpressionResult(valueType, identifier, initializers); + } + + Class bindingsClass = typeResolver.resolve(BINDINGS_CLASS_NAME); + Method indirectMethod = Arrays.stream(methodNames) + .map(methodName -> methodResolver.findMethod(bindingsClass, methodName, + leftResult.type(), + rightResult.type())) + .flatMap(Optional::stream) + .findFirst() + .orElse(null); + if (indirectMethod != null) { + Type valueType = indirectMethod.getGenericReturnType(); + String identifier = nameResolver.resolveUniqueName(valueType); + initializers.add(new CodeValue.Assignment(valueType, identifier, + new CodeValue.MethodCall(new CodeValue.Type(bindingsClass), + indirectMethod.getName(), + List.of(new CodeValue.Literal( + leftResult.value()), + new CodeValue.Literal( + rightResult.value()))))); + return new ExpressionResult(valueType, identifier, initializers); + } + + throw new IllegalArgumentException( + "Cannot %s %s and %s".formatted(String.join(" or ", methodNames), left, right)); + } + + private ExpressionResult computeExpressionWithMethod(Expression value, String methodName) { + ExpressionResult result = resolveExpression(value); + List initializers = new ArrayList<>(result.initializers()); + + Method directMethod = methodResolver.findMethod(result.type(), methodName).orElse(null); + if (directMethod != null) { + Type valueType = directMethod.getGenericReturnType(); + String identifier = nameResolver.resolveUniqueName(valueType); + initializers.add(new CodeValue.Assignment(valueType, identifier, + new CodeValue.MethodCall(new CodeValue.Literal(result.value()), + directMethod.getName(), List.of()))); + return new ExpressionResult(valueType, identifier, initializers); + } + + Class bindingsClass = typeResolver.resolve(BINDINGS_CLASS_NAME); + Method indirectMethod = methodResolver.findMethod(bindingsClass, methodName, result.type()).orElse(null); + if (indirectMethod != null) { + Type valueType = indirectMethod.getGenericReturnType(); + String identifier = nameResolver.resolveUniqueName(valueType); + initializers.add(new CodeValue.Assignment(valueType, identifier, + new CodeValue.MethodCall(new CodeValue.Type(bindingsClass), + indirectMethod.getName(), + List.of(new CodeValue.Literal( + result.value()))))); + return new ExpressionResult(valueType, identifier, initializers); + } + + throw new IllegalArgumentException("Cannot %s %s".formatted(methodName, value)); + } +} diff --git a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/MethodResolver.java b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/MethodResolver.java new file mode 100644 index 0000000..ba36ba5 --- /dev/null +++ b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/MethodResolver.java @@ -0,0 +1,260 @@ +package io.github.sheikah45.fx2j.processor.internal.resolve; + +import io.github.sheikah45.fx2j.processor.internal.model.NamedArgValue; +import io.github.sheikah45.fx2j.processor.internal.utils.StringUtils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class MethodResolver { + + public static final String DEFAULT_PROPERTY_CLASS = "javafx.beans.DefaultProperty"; + private static final String NAMED_ARG_CLASS = "javafx.beans.NamedArg"; + + private final TypeResolver typeResolver; + + private final Map> methodCache = new HashMap<>(); + private final Map> fieldCache = new HashMap<>(); + private final Map defaultPropertyCache = new HashMap<>(); + + MethodResolver(TypeResolver typeResolver) { + this.typeResolver = typeResolver; + } + + public boolean hasCopyConstructor(Type type) { + try { + Class clazz = typeResolver.resolveClassFromType(type); + return Modifier.isPublic(clazz.getConstructor(clazz).getModifiers()); + } catch (NoSuchMethodException e) { + return false; + } + } + + public boolean hasDefaultConstructor(Type type) { + try { + return Modifier.isPublic(typeResolver.resolveClassFromType(type).getConstructor().getModifiers()); + } catch (NoSuchMethodException e) { + return false; + } + } + + public String resolveDefaultProperty(Type type) { + return defaultPropertyCache.computeIfAbsent(type, key -> { + Class clazz = typeResolver.resolveClassFromType(key); + Class defaultPropertyClass = typeResolver.resolve(DEFAULT_PROPERTY_CLASS); + Annotation defaultProperty = clazz.getAnnotation(defaultPropertyClass); + if (defaultProperty == null) { + return null; + } + + try { + Method valueMethod = defaultProperty.getClass().getMethod("value"); + return (String) valueMethod.invoke(defaultProperty); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + }); + } + + public Optional resolveField(Type type, String fieldName) { + return fieldCache.computeIfAbsent(new FieldCacheKey(type, fieldName), this::resolveFieldWithCacheKey); + } + + private Optional resolveFieldWithCacheKey(FieldCacheKey fieldCacheKey) { + return resolvePublicFieldWithCacheKey(fieldCacheKey); + } + + private Optional resolvePublicFieldWithCacheKey(FieldCacheKey fieldCacheKey) { + Class clazz = typeResolver.resolveClassFromType(fieldCacheKey.type()); + String fieldName = fieldCacheKey.fieldName(); + try { + return Optional.of(clazz.getField(fieldName)); + } catch (NoSuchFieldException ignored) { + return Optional.empty(); + } + } + + public Optional resolveFieldRequiredPublicIfExists(Type type, String fieldName) { + return fieldCache.computeIfAbsent(new FieldCacheKey(type, fieldName), key -> resolveFieldWithCacheKey(key).or( + () -> resolveDeclaredFieldWithCacheKey(key).map(field -> { + if (!Modifier.isPublic(field.getModifiers())) { + throw new IllegalArgumentException("%s is not public in %s".formatted(field, type)); + } + + return field; + }))); + } + + private Optional resolveDeclaredFieldWithCacheKey(FieldCacheKey fieldCacheKey) { + Class clazz = typeResolver.resolveClassFromType(fieldCacheKey.type()); + String fieldName = fieldCacheKey.fieldName(); + try { + Field field = clazz.getDeclaredField(fieldName); + return Optional.of(field); + } catch (NoSuchFieldException ignored) { + return Optional.empty(); + } + } + + public Optional findMethod(Type type, String name, Type... parameterTypes) { + return methodCache.computeIfAbsent(new MethodCacheKey.ParamTypes(type, name, parameterTypes), + this::findMethodWithCacheKey); + } + + private Optional findMethodWithCacheKey(MethodCacheKey methodCacheKey) { + return switch (methodCacheKey) { + case MethodCacheKey.Count(Type type, String name, int paramCount) -> { + Method[] methods = typeResolver.resolveClassFromType(type).getMethods(); + yield findMatchingMethod(name, paramCount, methods); + } + case MethodCacheKey.ParamTypes(Type type, String name, Type[] parameterTypes) -> { + Method[] methods = typeResolver.resolveClassFromType(type).getMethods(); + yield findMatchingMethod(name, parameterTypes, methods); + } + }; + } + + private static Optional findMatchingMethod(String name, int paramCount, Method[] methods) { + List matchingMethods = Arrays.stream(methods) + .filter(method -> method.getName().equals(name)) + .filter(method -> method.getParameterCount() == paramCount) + .filter(method -> !method.isBridge()) + .toList(); + + if (matchingMethods.size() > 1) { + throw new IllegalArgumentException( + "Multiple matching methods found for name %s and count %d: %s".formatted(name, paramCount, + matchingMethods)); + } else if (matchingMethods.size() == 1) { + return Optional.of(matchingMethods.getFirst()); + } else { + return Optional.empty(); + } + } + + private Optional findMatchingMethod(String name, Type[] parameterTypes, Method[] methods) { + int numParameters = parameterTypes.length; + return Arrays.stream(methods) + .filter(method -> method.getName().equals(name)) + .filter(method -> method.getParameterCount() == numParameters) + .filter(method -> !method.isBridge()) + .filter(method -> { + Type[] methodParameterTypes = method.getGenericParameterTypes(); + for (int i = 0; i < numParameters; i++) { + if (!(typeResolver.isAssignableFrom(methodParameterTypes[i], parameterTypes[i]))) { + return false; + } + } + return true; + }) + .findFirst(); + } + + public Optional findMethod(Type type, String name, int paramCount) { + return methodCache.computeIfAbsent(new MethodCacheKey.Count(type, name, paramCount), + this::findMethodWithCacheKey); + } + + public Optional findMethodRequiredPublicIfExists(Type type, String name, Type... parameterTypes) { + return methodCache.computeIfAbsent(new MethodCacheKey.ParamTypes(type, name, parameterTypes), + key -> findMethodWithCacheKey(key).or( + () -> findDeclaredMethodWithCacheKey(key).map(method -> { + if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException( + "%s is not public in %s".formatted(method, type)); + } + + return method; + }))); + } + + private Optional findDeclaredMethodWithCacheKey(MethodCacheKey methodCacheKey) { + return switch (methodCacheKey) { + case MethodCacheKey.Count(Type type, String name, int paramCount) -> { + Method[] methods = typeResolver.resolveClassFromType(type).getDeclaredMethods(); + yield findMatchingMethod(name, paramCount, methods); + } + case MethodCacheKey.ParamTypes(Type type, String name, Type[] parameterTypes) -> { + Method[] methods = typeResolver.resolveClassFromType(type).getDeclaredMethods(); + yield findMatchingMethod(name, parameterTypes, methods); + } + }; + } + + public Optional resolveGetter(Type type, String name) { + return findMethod(type, "get%s".formatted(StringUtils.capitalize(name)), 0); + } + + public Optional resolveProperty(Type type, String name) { + return findMethod(type, "%sProperty".formatted(StringUtils.camelCase(name)), 0); + } + + public Optional resolveSetter(Type type, String name) { + return findMethod(type, "set%s".formatted(StringUtils.capitalize(name)), 1); + } + + public Optional resolveSetter(Type type, String name, Type valueClass) { + return findMethod(type, "set%s".formatted(StringUtils.capitalize(name)), valueClass); + } + + public Optional resolveSetterRequiredPublicIfExists(Type type, String name, Type valueClass) { + return findMethodRequiredPublicIfExists(type, "set%s".formatted(StringUtils.capitalize(name)), valueClass); + } + + public Optional resolveStaticSetter(Type type, String name) { + return findMethod(type, "set%s".formatted(StringUtils.capitalize(name)), 2); + } + + + public List getNamedArgs(Executable executable) { + return Arrays.stream(executable.getParameters()).map(this::getNamedArg).toList(); + } + + public NamedArgValue getNamedArg(Parameter parameter) { + Class namedArgClass = typeResolver.resolve(NAMED_ARG_CLASS); + Annotation namedArg = parameter.getAnnotation(namedArgClass); + if (namedArg == null) { + throw new IllegalArgumentException( + "Parameter does not have a NamedArg annotation: %s".formatted(parameter.getDeclaringExecutable())); + } + + try { + Method valueMethod = namedArg.getClass().getMethod("value"); + Method defaultValueMethod = namedArg.getClass().getMethod("defaultValue"); + return new NamedArgValue(parameter.getType(), (String) valueMethod.invoke(namedArg), + (String) defaultValueMethod.invoke(namedArg)); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + public boolean hasAllNamedArgs(Executable executable) { + return Arrays.stream(executable.getParameters()).allMatch(parameter -> { + Class namedArgClass = typeResolver.resolve(NAMED_ARG_CLASS); + return parameter.getAnnotation(namedArgClass) != null; + }); + } + + + public List> getConstructors(Type type) { + return List.of(typeResolver.resolveClassFromType(type).getConstructors()); + } + + private sealed interface MethodCacheKey { + record Count(Type receiverType, String methodName, int paramCount) implements MethodCacheKey {} + record ParamTypes(Type receiverType, String methodName, Type... paramTypes) implements MethodCacheKey {} + } + private record FieldCacheKey(Type type, String fieldName) {} +} diff --git a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/NameResolver.java b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/NameResolver.java new file mode 100644 index 0000000..2022e9c --- /dev/null +++ b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/NameResolver.java @@ -0,0 +1,43 @@ +package io.github.sheikah45.fx2j.processor.internal.resolve; + +import io.github.sheikah45.fx2j.processor.internal.utils.StringUtils; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + +public class NameResolver { + + private final TypeResolver typeResolver; + + private final Map idCounts = new HashMap<>(); + private final Map idTypeMap = new HashMap<>(); + + NameResolver(TypeResolver typeResolver) { + this.typeResolver = typeResolver; + } + + public String resolveUniqueName(Type type) { + Class clazz = typeResolver.resolveClassFromType(type); + String rawIdentifier = StringUtils.camelCase(clazz.getSimpleName()); + Integer nameCount = idCounts.compute(rawIdentifier, (key, value) -> value == null ? 0 : value + 1); + String identifier = rawIdentifier + nameCount; + storeIdType(identifier, type); + return identifier; + } + + public void storeIdType(String id, Type type) { + if (idTypeMap.containsKey(id)) { + throw new IllegalArgumentException( + "Type mapping to %s already exists for id %s".formatted(idTypeMap.get(id), id)); + } + + idTypeMap.put(id, type); + } + + public Type resolveTypeById(String id) { + return idTypeMap.computeIfAbsent(id, key -> { + throw new IllegalArgumentException("No type known for id %s".formatted(id)); + }); + } +} diff --git a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/ResolverContainer.java b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/ResolverContainer.java new file mode 100644 index 0000000..186c5b9 --- /dev/null +++ b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/ResolverContainer.java @@ -0,0 +1,50 @@ +package io.github.sheikah45.fx2j.processor.internal.resolve; + +import java.util.Set; + +public class ResolverContainer { + + private final TypeResolver typeResolver; + private final MethodResolver methodResolver; + private final NameResolver nameResolver; + private final ValueResolver valueResolver; + private final ExpressionResolver expressionResolver; + + private ResolverContainer(TypeResolver typeResolver, MethodResolver methodResolver, NameResolver nameResolver, + ValueResolver valueResolver, ExpressionResolver expressionResolver) { + this.typeResolver = typeResolver; + this.methodResolver = methodResolver; + this.nameResolver = nameResolver; + this.valueResolver = valueResolver; + this.expressionResolver = expressionResolver; + } + + public static ResolverContainer from(Set imports, ClassLoader classLoader) { + TypeResolver typeResolver = new TypeResolver(imports, classLoader); + MethodResolver methodResolver = new MethodResolver(typeResolver); + NameResolver nameResolver = new NameResolver(typeResolver); + ValueResolver valueResolver = new ValueResolver(typeResolver, methodResolver, nameResolver); + ExpressionResolver expressionResolver = new ExpressionResolver(typeResolver, methodResolver, nameResolver); + return new ResolverContainer(typeResolver, methodResolver, nameResolver, valueResolver, expressionResolver); + } + + public TypeResolver getTypeResolver() { + return typeResolver; + } + + public MethodResolver getMethodResolver() { + return methodResolver; + } + + public NameResolver getNameResolver() { + return nameResolver; + } + + public ValueResolver getValueResolver() { + return valueResolver; + } + + public ExpressionResolver getExpressionResolver() { + return expressionResolver; + } +} diff --git a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/TypeResolver.java b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/TypeResolver.java new file mode 100644 index 0000000..829e775 --- /dev/null +++ b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/TypeResolver.java @@ -0,0 +1,212 @@ +package io.github.sheikah45.fx2j.processor.internal.resolve; + +import io.github.sheikah45.fx2j.processor.internal.utils.StringUtils; + +import java.lang.invoke.MethodType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +@SuppressWarnings("unchecked") +public class TypeResolver { + + private final ClassLoader classLoader; + + private final Set importPrefixes = new HashSet<>(); + private final Map> resolvedClassesMap = new HashMap<>(); + private final Map> resolvedClassMap = new HashMap<>(); + + TypeResolver(Set imports, ClassLoader classLoader) { + this.classLoader = classLoader; + resolveImports(imports); + } + + private void resolveImports(Set imports) { + imports.forEach(importString -> { + if (importString.endsWith(".*")) { + importPrefixes.add(importString.substring(0, importString.length() - 2)); + } else { + Class type = resolve(importString); + String simpleName = StringUtils.substringAfterLast(importString, "."); + Class previousType = resolvedClassesMap.put(simpleName, type); + if (previousType != null) { + throw new IllegalArgumentException( + "Name collision between imports %s and %s".formatted(previousType, type)); + } + } + }); + } + + public Class resolve(String typeName) { + Class clazz = (Class) resolvedClassesMap.computeIfAbsent(typeName, this::resolveWithoutCache); + if (clazz == null) { + throw new IllegalArgumentException("Unable to find class for %s".formatted(typeName)); + } + + return clazz; + } + + private Class resolveWithoutCache(String typeName) { + try { + return Class.forName(typeName, false, classLoader); + } catch (ClassNotFoundException ignored) { + } + + for (String importPrefix : importPrefixes) { + String fullName = importPrefix + "." + typeName; + try { + return Class.forName(fullName, false, classLoader); + } catch (ClassNotFoundException ignored) { + } + } + + String subclassName = typeName; + Class clazz = null; + while (clazz == null && subclassName.contains(".")) { + subclassName = StringUtils.replaceLast(subclassName, ".", "$"); + clazz = resolveWithoutCache(subclassName); + } + + return clazz; + } + + public Class unwrapType(Type type) { + return MethodType.methodType(resolveClassFromType(type)).unwrap().returnType(); + } + + public Type[] resolveUpperBoundTypeArguments(Type type) { + if (type instanceof ParameterizedType parameterizedType) { + return parameterizedType.getActualTypeArguments(); + } else { + return new Type[0]; + } + } + + public Class resolveTypeUpperBound(Type type) { + return switch (type) { + case Class clazz -> clazz; + case ParameterizedType parameterizedType -> resolveTypeUpperBound(parameterizedType.getRawType()); + case WildcardType wildcardType -> { + Type[] upperBounds = wildcardType.getUpperBounds(); + if (upperBounds.length != 1) { + throw new IllegalArgumentException("Type does not have exactly one upper bound"); + } + yield resolveTypeUpperBound(upperBounds[0]); + } + case null, default -> + throw new UnsupportedOperationException("Cannot resolve upper bound of type %s".formatted(type)); + }; + } + + public Class[] resolveLowerBoundTypeArguments(Type type) { + if (type instanceof ParameterizedType parameterizedType) { + return Arrays.stream(parameterizedType.getActualTypeArguments()) + .map(this::resolveTypeLowerBound) + .toArray(Class[]::new); + } else { + return null; + } + } + + public Class resolveTypeLowerBound(Type type) { + return switch (type) { + case Class clazz -> clazz; + case ParameterizedType parameterizedType -> resolveTypeLowerBound(parameterizedType.getRawType()); + case WildcardType wildcardType -> { + Type[] lowerBounds = wildcardType.getLowerBounds(); + if (lowerBounds.length != 1) { + throw new IllegalArgumentException("Type does not have exactly one lower bound"); + } + yield resolveTypeUpperBound(lowerBounds[0]); + } + case null, default -> + throw new UnsupportedOperationException("Cannot resolve lower bound of type %s".formatted(type)); + }; + } + + public boolean typeArgumentsMeetBounds(Type type, Type[] boundTypeArguments) { + if (!(type instanceof ParameterizedType parameterizedType)) { + return true; + } + + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + if (actualTypeArguments.length != boundTypeArguments.length) { + return false; + } + + for (int i = 0; i < actualTypeArguments.length; i++) { + Type typeArgument = actualTypeArguments[i]; + Type wildcardBound = boundTypeArguments[i]; + if (typeArgument instanceof ParameterizedType parameterizedTypeArgument && + hasNonMatchingWildcardUpperBounds(parameterizedTypeArgument, wildcardBound)) { + return false; + } + } + + return true; + } + + public boolean hasNonMatchingWildcardUpperBounds(ParameterizedType parameterizedType, Type desiredUpperBound) { + Type actualTypeArgument = parameterizedType.getActualTypeArguments()[0]; + if (!(actualTypeArgument instanceof WildcardType wildcardType)) { + return true; + } + + Type[] upperBounds = wildcardType.getUpperBounds(); + if (upperBounds.length != 1) { + return true; + } + + Type upperBound = upperBounds[0]; + return upperBound != desiredUpperBound; + } + + public ClassLoader getClassLoader() { + return classLoader; + } + + public Set getResolvedModules() { + return resolvedClassesMap.values() + .stream() + .map(Class::getModule) + .filter(Objects::nonNull) + .map(Module::getName) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + public boolean isArray(Type type) { + Class clazz = resolveClassFromType(type); + return clazz.isArray(); + } + + public Type getComponentType(Type type) { + return resolveClassFromType(type).getComponentType(); + } + + public boolean isPrimitive(Type type) { + Class clazz = resolveClassFromType(type); + return clazz.isPrimitive() || MethodType.methodType(clazz).hasWrappers(); + } + + public Class wrapType(Type type) { + return MethodType.methodType(resolveClassFromType(type)).wrap().returnType(); + } + + public Class resolveClassFromType(Type type) { + return resolvedClassMap.computeIfAbsent(type, this::resolveTypeUpperBound); + } + + public boolean isAssignableFrom(Type baseType, Type checkedType) { + Class baseClass = resolveClassFromType(baseType); + Class checkedClass = resolveClassFromType(checkedType); + return baseClass.isAssignableFrom(checkedClass) || baseClass.isAssignableFrom(wrapType(checkedClass)); + } +} diff --git a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/ValueResolver.java b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/ValueResolver.java new file mode 100644 index 0000000..b0c0485 --- /dev/null +++ b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/resolve/ValueResolver.java @@ -0,0 +1,154 @@ +package io.github.sheikah45.fx2j.processor.internal.resolve; + +import io.github.sheikah45.fx2j.parser.property.Expression; +import io.github.sheikah45.fx2j.parser.property.Value; +import io.github.sheikah45.fx2j.processor.FxmlProcessor; +import io.github.sheikah45.fx2j.processor.internal.model.CodeValue; +import io.github.sheikah45.fx2j.processor.internal.model.NamedArgValue; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class ValueResolver { + static final Map, Object> DEFAULTS_MAP = Map.of(byte.class, 0, short.class, 0, int.class, 0, long.class, + 0L, float.class, 0f, double.class, 0d, char.class, + '\u0000', boolean.class, false); + + private final TypeResolver typeResolver; + private final MethodResolver methodResolver; + private final NameResolver nameResolver; + + ValueResolver(TypeResolver typeResolver, MethodResolver methodResolver, NameResolver nameResolver) { + this.typeResolver = typeResolver; + this.methodResolver = methodResolver; + this.nameResolver = nameResolver; + } + + public CodeValue coerceDefaultValue(NamedArgValue namedArgValue) { + String defaultValue = namedArgValue.defaultValue(); + if (defaultValue.isBlank()) { + Object typeDefault = DEFAULTS_MAP.get(namedArgValue.parameterType()); + if (typeDefault != null) { + return new CodeValue.Literal(typeDefault.toString()); + } + + return new CodeValue.Null(); + } + return resolveCodeValue(namedArgValue.parameterType(), defaultValue); + } + + public CodeValue resolveCodeValue(Type valueType, String value) { + if (typeResolver.isAssignableFrom(char.class, valueType) || + typeResolver.isAssignableFrom(Character.class, valueType)) { + if (value.length() != 1) { + throw new IllegalArgumentException("Cannot coerce char from non single character string"); + } + + return new CodeValue.Char(value.charAt(0)); + } + + if (typeResolver.isAssignableFrom(String.class, valueType)) { + return new CodeValue.String(value); + } + + if (typeResolver.isArray(valueType)) { + Type componentType = typeResolver.getComponentType(valueType); + List arrayValues = Arrays.stream(value.split(",")) + .map(componentString -> resolveCodeValue(componentType, + componentString)) + .toList(); + return new CodeValue.ArrayInitialization.Declared(componentType, arrayValues); + } + + if (typeResolver.isPrimitive(valueType)) { + Class boxedType = typeResolver.wrapType(valueType); + Method method = methodResolver.findMethod(boxedType, "parse%s".formatted(boxedType.getSimpleName()), + String.class).orElse(null); + if (method != null) { + try { + return resolveCodeValue(method, value); + } catch (IllegalAccessException | InvocationTargetException ignored) {} + } + } + + Class boxedType = typeResolver.wrapType(valueType); + Method method = methodResolver.findMethod(boxedType, "valueOf", String.class).orElse(null); + if (method != null) { + try { + return resolveCodeValue(method, value); + } catch (IllegalAccessException | InvocationTargetException ignored) {} + } + + if (valueType == Object.class) { + return new CodeValue.String(value); + } + + throw new UnsupportedOperationException("Cannot create type %s from %s".formatted(valueType, value)); + } + + public CodeValue resolveCodeValue(Method staticMethod, String valueString) + throws IllegalAccessException, InvocationTargetException { + if (!Modifier.isStatic(staticMethod.getModifiers())) { + throw new IllegalArgumentException("Provided method is not static"); + } + + Type[] parameterTypes = staticMethod.getGenericParameterTypes(); + if (parameterTypes.length != 1 || !typeResolver.isAssignableFrom(String.class, parameterTypes[0])) { + throw new IllegalArgumentException( + "Provided method %s does not accept only one argument of type string".formatted(staticMethod)); + } + + Type valueType = staticMethod.getGenericReturnType(); + + return switch (staticMethod.invoke(null, valueString)) { + case null -> new CodeValue.Null(); + case Double number when number == Double.POSITIVE_INFINITY -> + new CodeValue.FieldAccess(new CodeValue.Type(Double.class), "POSITIVE_INFINITY"); + case Double number when number == Double.NEGATIVE_INFINITY -> + new CodeValue.FieldAccess(new CodeValue.Type(Double.class), "NEGATIVE_INFINITY"); + case Double number when Objects.equals(number, Double.NaN) -> + new CodeValue.FieldAccess(new CodeValue.Type(Double.class), "NaN"); + case Float number when number == Float.POSITIVE_INFINITY -> + new CodeValue.FieldAccess(new CodeValue.Type(Float.class), "POSITIVE_INFINITY"); + case Float number when number == Float.NEGATIVE_INFINITY -> + new CodeValue.FieldAccess(new CodeValue.Type(Float.class), "NEGATIVE_INFINITY"); + case Float number when Objects.equals(number, Float.NaN) -> + new CodeValue.FieldAccess(new CodeValue.Type(Float.class), "NaN"); + case Enum enumValue -> new CodeValue.Enum(enumValue); + case Object val when typeResolver.isPrimitive(val.getClass()) -> + new CodeValue.Literal(staticMethod.invoke(null, valueString).toString()); + default -> new CodeValue.MethodCall(new CodeValue.Type(valueType), staticMethod.getName(), + List.of(new CodeValue.String(valueString))); + }; + } + + public CodeValue resolveCodeValue(Type valueType, Value value) { + return switch (value) { + case Value.Empty() -> new CodeValue.Null(); + case Value.Reference(String reference) -> { + Type referenceType = nameResolver.resolveTypeById(reference); + if (!typeResolver.isAssignableFrom(typeResolver.resolveClassFromType(valueType), referenceType)) { + throw new IllegalArgumentException("Cannot assign %s to %s".formatted(referenceType, valueType)); + } + + yield new CodeValue.Literal(reference); + } + case Value.Resource(String resource) when valueType == String.class -> + new CodeValue.MethodCall(new CodeValue.Literal(FxmlProcessor.RESOURCES_NAME), "getString", + List.of(new CodeValue.String(resource))); + case Value.Literal(String val) -> resolveCodeValue(valueType, val); + case Value.Location ignored -> + throw new UnsupportedOperationException("Location resolution not yet supported"); + case Value.Resource ignored -> throw new UnsupportedOperationException( + "Non string resource types not supported"); + case Expression ignored -> + throw new UnsupportedOperationException("Cannot resolve an expression to a code value"); + }; + } +} diff --git a/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/utils/CodeBlockUtils.java b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/utils/CodeBlockUtils.java new file mode 100644 index 0000000..589312c --- /dev/null +++ b/fx2j-processor/src/main/java/io/github/sheikah45/fx2j/processor/internal/utils/CodeBlockUtils.java @@ -0,0 +1,40 @@ +package io.github.sheikah45.fx2j.processor.internal.utils; + +import com.squareup.javapoet.CodeBlock; +import io.github.sheikah45.fx2j.processor.internal.model.CodeValue; + +import java.lang.reflect.Type; +import java.util.List; + +public class CodeBlockUtils { + + public static CodeBlock convertToCodeBlock(CodeValue codeValue) { + return switch (codeValue) { + case CodeValue.Null() -> CodeBlock.of("null"); + case CodeValue.Char(char value) -> CodeBlock.of("$L", value); + case CodeValue.Literal(String value) -> CodeBlock.of("$L", value); + case CodeValue.String(String value) -> CodeBlock.of("$S", value); + case CodeValue.Type(Type type) -> CodeBlock.of("$T", type); + case CodeValue.Enum(Enum value) -> CodeBlock.of("$T.$L", value.getDeclaringClass(), value.name()); + case CodeValue.FieldAccess(CodeValue receiver, String field) -> + CodeBlock.of("$L.$L", convertToCodeBlock(receiver), field); + case CodeValue.ArrayInitialization.Declared(Type componentType, List values) -> { + CodeBlock valuesBlock = values.stream() + .map(CodeBlockUtils::convertToCodeBlock) + .collect(CodeBlock.joining(", ")); + yield CodeBlock.of("new $T[]{$L}", componentType, valuesBlock); + } + case CodeValue.ArrayInitialization.Sized(Type componentType, int size) -> + CodeBlock.of("new $T[$L]", componentType, size); + case CodeValue.MethodCall(CodeValue receiver, String methodName, List args) -> { + CodeBlock argsBlock = args.stream() + .map(CodeBlockUtils::convertToCodeBlock) + .collect(CodeBlock.joining(", ")); + yield CodeBlock.of("$L.$L($L)", convertToCodeBlock(receiver), methodName, argsBlock); + } + case CodeValue.Assignment(Type type, String identifier, CodeValue value) -> + CodeBlock.of("$T $L = $L", type, identifier, convertToCodeBlock(value)); + }; + } + +} diff --git a/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/internal/resolve/AbstractResolverTest.java b/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/internal/resolve/AbstractResolverTest.java new file mode 100644 index 0000000..df859ad --- /dev/null +++ b/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/internal/resolve/AbstractResolverTest.java @@ -0,0 +1,13 @@ +package io.github.sheikah45.fx2j.processor.internal.resolve; + +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +import java.util.Set; + +@Execution(ExecutionMode.CONCURRENT) +abstract class AbstractResolverTest { + + protected final ResolverContainer resolverContainer = ResolverContainer.from(Set.of(), getClass().getClassLoader()); + +} diff --git a/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/internal/resolve/ExpressionResolverTest.java b/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/internal/resolve/ExpressionResolverTest.java new file mode 100644 index 0000000..fd19ea8 --- /dev/null +++ b/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/internal/resolve/ExpressionResolverTest.java @@ -0,0 +1,543 @@ +package io.github.sheikah45.fx2j.processor.internal.resolve; + +import io.github.sheikah45.fx2j.parser.property.Expression; +import io.github.sheikah45.fx2j.processor.internal.model.CodeValue; +import io.github.sheikah45.fx2j.processor.internal.model.ExpressionResult; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.binding.IntegerBinding; +import javafx.beans.binding.NumberBinding; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ObservableBooleanValue; +import javafx.beans.value.ObservableNumberValue; +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.control.Label; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ExpressionResolverTest extends AbstractResolverTest { + + private final ExpressionResolver expressionResolver = resolverContainer.getExpressionResolver(); + + @Test + void testResolveNull() { + assertEquals(new ExpressionResult(Object.class, "null", List.of()), + expressionResolver.resolveExpression(new Expression.Null())); + } + + @Test + void testResolveWhole() { + assertEquals(new ExpressionResult(int.class, "1", List.of()), + expressionResolver.resolveExpression(new Expression.Whole(1))); + } + + @Test + void testResolveWholeLarge() { + assertEquals(new ExpressionResult(long.class, String.valueOf(Integer.MAX_VALUE + 1L), List.of()), + expressionResolver.resolveExpression(new Expression.Whole(Integer.MAX_VALUE + 1L))); + } + + @Test + void testResolveFraction() { + assertEquals(new ExpressionResult(float.class, "1.0", List.of()), + expressionResolver.resolveExpression(new Expression.Fraction(1))); + } + + @Test + void testResolveFractionLarge() { + assertEquals(new ExpressionResult(double.class, String.valueOf(Float.MAX_VALUE * 2d), List.of()), + expressionResolver.resolveExpression(new Expression.Fraction(Float.MAX_VALUE * 2d))); + } + + @Test + void testResolveBoolean() { + assertEquals(new ExpressionResult(boolean.class, "true", List.of()), + expressionResolver.resolveExpression(new Expression.Boolean(true))); + } + + @Test + void testResolveString() { + assertEquals(new ExpressionResult(String.class, "\"hello\"", List.of()), + expressionResolver.resolveExpression(new Expression.String("hello"))); + } + + @Test + void testResolveVariable() { + resolverContainer.getNameResolver().storeIdType("a", Integer.class); + assertEquals(new ExpressionResult(Integer.class, "a", List.of()), + expressionResolver.resolveExpression(new Expression.Variable("a"))); + } + + @Test + void testResolvePropertyRead() { + resolverContainer.getNameResolver().storeIdType("a", Label.class); + assertEquals(new ExpressionResult(StringProperty.class, "stringProperty0", + List.of(new CodeValue.Assignment(StringProperty.class, "stringProperty0", + new CodeValue.MethodCall( + new CodeValue.Literal("a"), + "textProperty", List.of())))), + expressionResolver.resolveExpression( + new Expression.PropertyRead(new Expression.Variable("a"), "text"))); + } + + @Test + void testResolvePropertyReadUnknownProperty() { + resolverContainer.getNameResolver().storeIdType("a", Label.class); + assertThrows(IllegalArgumentException.class, () -> expressionResolver.resolveExpression( + new Expression.PropertyRead(new Expression.Variable("a"), "blank"))); + } + + @Test + void testResolveMethodCall() { + resolverContainer.getNameResolver().storeIdType("a", Label.class); + assertEquals(new ExpressionResult(Node.class, "node0", List.of( + new CodeValue.Assignment(Node.class, "node0", + new CodeValue.MethodCall( + new CodeValue.Literal( + "a"), + "lookup", + List.of(new CodeValue.Literal( + "\"parent\"")))))), + expressionResolver.resolveExpression( + new Expression.MethodCall(new Expression.Variable("a"), "lookup", + List.of(new Expression.String("parent"))))); + } + + @Test + void testResolveMethodCallUnknownProperty() { + resolverContainer.getNameResolver().storeIdType("a", Label.class); + assertThrows(IllegalArgumentException.class, () -> expressionResolver.resolveExpression( + new Expression.MethodCall(new Expression.Variable("a"), "getBlank", List.of()))); + } + + @Test + void testResolveCollectionAccess() { + Method valueAt = resolverContainer.getMethodResolver() + .findMethod(Bindings.class, "valueAt", ObservableList.class, int.class) + .orElseThrow(); + resolverContainer.getNameResolver().storeIdType("a", ObservableList.class); + Type bindingType = valueAt.getGenericReturnType(); + assertEquals(new ExpressionResult(bindingType, "objectBinding0", + List.of(new CodeValue.Assignment(bindingType, "objectBinding0", + new CodeValue.MethodCall( + new CodeValue.Type(Bindings.class), + "valueAt", + List.of(new CodeValue.Literal("a"), + new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.CollectionAccess(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveCollectionAccessUnknownProperty() { + resolverContainer.getNameResolver().storeIdType("a", Label.class); + assertThrows(IllegalArgumentException.class, () -> expressionResolver.resolveExpression( + new Expression.MethodCall(new Expression.Variable("a"), "getBlank", List.of()))); + } + + @Test + void testResolveNegate() { + resolverContainer.getNameResolver().storeIdType("a", IntegerProperty.class); + assertEquals(new ExpressionResult(IntegerBinding.class, "integerBinding0", + List.of(new CodeValue.Assignment(IntegerBinding.class, "integerBinding0", + new CodeValue.MethodCall( + new CodeValue.Literal("a"), "negate", + List.of())))), + expressionResolver.resolveExpression(new Expression.Negate(new Expression.Variable("a")))); + } + + @Test + void testResolveNegateObservable() { + resolverContainer.getNameResolver().storeIdType("a", ObservableNumberValue.class); + assertEquals(new ExpressionResult(NumberBinding.class, "numberBinding0", + List.of(new CodeValue.Assignment(NumberBinding.class, "numberBinding0", + new CodeValue.MethodCall( + new CodeValue.Type(Bindings.class), + "negate", + List.of(new CodeValue.Literal( + "a")))))), + expressionResolver.resolveExpression(new Expression.Negate(new Expression.Variable("a")))); + } + + @Test + void testResolveAdd() { + resolverContainer.getNameResolver().storeIdType("a", IntegerProperty.class); + assertEquals(new ExpressionResult(IntegerBinding.class, "integerBinding0", + List.of(new CodeValue.Assignment(IntegerBinding.class, "integerBinding0", + new CodeValue.MethodCall( + new CodeValue.Literal("a"), "add", + List.of(new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.Add(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveAddObservable() { + resolverContainer.getNameResolver().storeIdType("a", ObservableNumberValue.class); + assertEquals(new ExpressionResult(NumberBinding.class, "numberBinding0", + List.of(new CodeValue.Assignment(NumberBinding.class, "numberBinding0", + new CodeValue.MethodCall( + new CodeValue.Type(Bindings.class), + "add", + List.of(new CodeValue.Literal("a"), + new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.Add(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveSubtract() { + resolverContainer.getNameResolver().storeIdType("a", IntegerProperty.class); + assertEquals(new ExpressionResult(IntegerBinding.class, "integerBinding0", + List.of(new CodeValue.Assignment(IntegerBinding.class, "integerBinding0", + new CodeValue.MethodCall( + new CodeValue.Literal("a"), + "subtract", + List.of(new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.Subtract(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveSubtractObservable() { + resolverContainer.getNameResolver().storeIdType("a", ObservableNumberValue.class); + assertEquals(new ExpressionResult(NumberBinding.class, "numberBinding0", + List.of(new CodeValue.Assignment(NumberBinding.class, "numberBinding0", + new CodeValue.MethodCall( + new CodeValue.Type(Bindings.class), + "subtract", + List.of(new CodeValue.Literal("a"), + new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.Subtract(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveMultiply() { + resolverContainer.getNameResolver().storeIdType("a", IntegerProperty.class); + assertEquals(new ExpressionResult(IntegerBinding.class, "integerBinding0", + List.of(new CodeValue.Assignment(IntegerBinding.class, "integerBinding0", + new CodeValue.MethodCall( + new CodeValue.Literal("a"), + "multiply", + List.of(new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.Multiply(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveMultiplyObservable() { + resolverContainer.getNameResolver().storeIdType("a", ObservableNumberValue.class); + assertEquals(new ExpressionResult(NumberBinding.class, "numberBinding0", + List.of(new CodeValue.Assignment(NumberBinding.class, "numberBinding0", + new CodeValue.MethodCall( + new CodeValue.Type(Bindings.class), + "multiply", + List.of(new CodeValue.Literal("a"), + new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.Multiply(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveDivide() { + resolverContainer.getNameResolver().storeIdType("a", IntegerProperty.class); + assertEquals(new ExpressionResult(IntegerBinding.class, "integerBinding0", + List.of(new CodeValue.Assignment(IntegerBinding.class, "integerBinding0", + new CodeValue.MethodCall( + new CodeValue.Literal("a"), "divide", + List.of(new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.Divide(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveDivideObservable() { + resolverContainer.getNameResolver().storeIdType("a", ObservableNumberValue.class); + assertEquals(new ExpressionResult(NumberBinding.class, "numberBinding0", + List.of(new CodeValue.Assignment(NumberBinding.class, "numberBinding0", + new CodeValue.MethodCall( + new CodeValue.Type(Bindings.class), + "divide", + List.of(new CodeValue.Literal("a"), + new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.Divide(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveGreaterThan() { + resolverContainer.getNameResolver().storeIdType("a", IntegerProperty.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Literal("a"), + "greaterThan", + List.of(new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.GreaterThan(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveGreaterThanObservable() { + resolverContainer.getNameResolver().storeIdType("a", ObservableNumberValue.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Type(Bindings.class), + "greaterThan", + List.of(new CodeValue.Literal("a"), + new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.GreaterThan(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveGreaterThanEqual() { + resolverContainer.getNameResolver().storeIdType("a", IntegerProperty.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Literal("a"), + "greaterThanOrEqualTo", + List.of(new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.GreaterThanEqual(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveGreaterThanOrEqualObservable() { + resolverContainer.getNameResolver().storeIdType("a", ObservableNumberValue.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Type(Bindings.class), + "greaterThanOrEqual", + List.of(new CodeValue.Literal("a"), + new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.GreaterThanEqual(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveLessThan() { + resolverContainer.getNameResolver().storeIdType("a", IntegerProperty.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Literal("a"), + "lessThan", + List.of(new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.LessThan(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveLessThanObservable() { + resolverContainer.getNameResolver().storeIdType("a", ObservableNumberValue.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Type(Bindings.class), + "lessThan", + List.of(new CodeValue.Literal("a"), + new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.LessThan(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveLessThanEqual() { + resolverContainer.getNameResolver().storeIdType("a", IntegerProperty.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Literal("a"), + "lessThanOrEqualTo", + List.of(new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.LessThanEqual(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveLessThanOrEqualObservable() { + resolverContainer.getNameResolver().storeIdType("a", ObservableNumberValue.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Type(Bindings.class), + "lessThanOrEqual", + List.of(new CodeValue.Literal("a"), + new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.LessThanEqual(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveEqual() { + resolverContainer.getNameResolver().storeIdType("a", IntegerProperty.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Literal("a"), + "isEqualTo", + List.of(new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.Equal(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveEqualObservable() { + resolverContainer.getNameResolver().storeIdType("a", ObservableNumberValue.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Type(Bindings.class), + "equal", + List.of(new CodeValue.Literal("a"), + new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.Equal(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveNotEqual() { + resolverContainer.getNameResolver().storeIdType("a", IntegerProperty.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Literal("a"), + "isNotEqualTo", + List.of(new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.NotEqual(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveNotEqualObservable() { + resolverContainer.getNameResolver().storeIdType("a", ObservableNumberValue.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Type(Bindings.class), + "notEqual", + List.of(new CodeValue.Literal("a"), + new CodeValue.Literal( + "0")))))), + expressionResolver.resolveExpression( + new Expression.NotEqual(new Expression.Variable("a"), new Expression.Whole(0)))); + } + + @Test + void testResolveAnd() { + resolverContainer.getNameResolver().storeIdType("a", BooleanProperty.class); + resolverContainer.getNameResolver().storeIdType("b", BooleanProperty.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Literal("a"), "and", + List.of(new CodeValue.Literal( + "b")))))), + expressionResolver.resolveExpression( + new Expression.And(new Expression.Variable("a"), new Expression.Variable("b")))); + } + + @Test + void testResolveAndObservable() { + resolverContainer.getNameResolver().storeIdType("a", ObservableBooleanValue.class); + resolverContainer.getNameResolver().storeIdType("b", ObservableBooleanValue.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Type(Bindings.class), + "and", + List.of(new CodeValue.Literal("a"), + new CodeValue.Literal( + "b")))))), + expressionResolver.resolveExpression( + new Expression.And(new Expression.Variable("a"), new Expression.Variable("b")))); + } + + @Test + void testResolveOr() { + resolverContainer.getNameResolver().storeIdType("a", BooleanProperty.class); + resolverContainer.getNameResolver().storeIdType("b", BooleanProperty.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Literal("a"), "or", + List.of(new CodeValue.Literal( + "b")))))), + expressionResolver.resolveExpression( + new Expression.Or(new Expression.Variable("a"), new Expression.Variable("b")))); + } + + @Test + void testResolveOrObservable() { + resolverContainer.getNameResolver().storeIdType("a", ObservableBooleanValue.class); + resolverContainer.getNameResolver().storeIdType("b", ObservableBooleanValue.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Type(Bindings.class), + "or", + List.of(new CodeValue.Literal("a"), + new CodeValue.Literal( + "b")))))), + expressionResolver.resolveExpression( + new Expression.Or(new Expression.Variable("a"), new Expression.Variable("b")))); + } + + @Test + void testResolveNot() { + resolverContainer.getNameResolver().storeIdType("a", BooleanProperty.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Literal("a"), "not", + List.of())))), + expressionResolver.resolveExpression(new Expression.Invert(new Expression.Variable("a")))); + } + + @Test + void testResolveNotObservable() { + resolverContainer.getNameResolver().storeIdType("a", ObservableBooleanValue.class); + assertEquals(new ExpressionResult(BooleanBinding.class, "booleanBinding0", + List.of(new CodeValue.Assignment(BooleanBinding.class, "booleanBinding0", + new CodeValue.MethodCall( + new CodeValue.Type(Bindings.class), + "not", List.of(new CodeValue.Literal( + "a")))))), + expressionResolver.resolveExpression(new Expression.Invert(new Expression.Variable("a")))); + } + +} diff --git a/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/internal/resolve/NameResolverTest.java b/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/internal/resolve/NameResolverTest.java new file mode 100644 index 0000000..9583581 --- /dev/null +++ b/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/internal/resolve/NameResolverTest.java @@ -0,0 +1,33 @@ +package io.github.sheikah45.fx2j.processor.internal.resolve; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class NameResolverTest extends AbstractResolverTest { + + private final NameResolver nameResolver = resolverContainer.getNameResolver(); + + @Test + void testResolveUniqueName() { + assertEquals("object0", nameResolver.resolveUniqueName(Object.class)); + assertEquals("object1", nameResolver.resolveUniqueName(Object.class)); + assertEquals("int0", nameResolver.resolveUniqueName(int.class)); + assertEquals(Object.class, nameResolver.resolveTypeById("object0")); + assertEquals(Object.class, nameResolver.resolveTypeById("object1")); + assertEquals(int.class, nameResolver.resolveTypeById("int0")); + } + + @Test + void testStoreIdType() { + nameResolver.storeIdType("obj", Object.class); + assertEquals(Object.class, nameResolver.resolveTypeById("obj")); + assertThrows(IllegalArgumentException.class, () -> nameResolver.storeIdType("obj", Object.class)); + } + + @Test + void testResolveTypeByIdFails() { + assertThrows(IllegalArgumentException.class, () -> nameResolver.resolveTypeById("obj")); + } +} diff --git a/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/internal/resolve/ValueResolverTest.java b/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/internal/resolve/ValueResolverTest.java new file mode 100644 index 0000000..6f65aa6 --- /dev/null +++ b/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/internal/resolve/ValueResolverTest.java @@ -0,0 +1,190 @@ +package io.github.sheikah45.fx2j.processor.internal.resolve; + +import io.github.sheikah45.fx2j.parser.property.Expression; +import io.github.sheikah45.fx2j.parser.property.Value; +import io.github.sheikah45.fx2j.processor.FxmlProcessor; +import io.github.sheikah45.fx2j.processor.internal.model.CodeValue; +import io.github.sheikah45.fx2j.processor.internal.model.NamedArgValue; +import javafx.geometry.VPos; +import javafx.util.Duration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ValueResolverTest extends AbstractResolverTest { + + private final ValueResolver valueResolver = resolverContainer.getValueResolver(); + + + @ParameterizedTest + @ArgumentsSource(DefaultValueProvider.class) + void testCoerceDefaultValueBlankPrimitive(Class clazz, Object value) { + assertEquals(new CodeValue.Literal(value.toString()), + valueResolver.coerceDefaultValue(new NamedArgValue(clazz, "", ""))); + } + + @ParameterizedTest + @ArgumentsSource(DefaultValueProvider.class) + void testCoerceDefaultValueBlankBoxed(Class clazz) { + assertEquals(new CodeValue.Null(), valueResolver.coerceDefaultValue( + new NamedArgValue(resolverContainer.getTypeResolver().wrapType(clazz), "", ""))); + } + + @Test + void testCoerceDefaultValueNotBlank() { + assertEquals(new CodeValue.String("hello"), + valueResolver.coerceDefaultValue(new NamedArgValue(String.class, "", "hello"))); + } + + @Test + void testCoerceImpossible() { + assertThrows(UnsupportedOperationException.class, () -> valueResolver.resolveCodeValue(Class.class, "aa")); + } + + + @Test + void testCoerceObject() { + assertEquals(new CodeValue.String("hello"), valueResolver.resolveCodeValue(Object.class, "hello")); + } + + @Test + void testCoerceChar() { + assertEquals(new CodeValue.Char('a'), valueResolver.resolveCodeValue(char.class, "a")); + assertEquals(new CodeValue.Char('a'), valueResolver.resolveCodeValue(Character.class, "a")); + assertThrows(IllegalArgumentException.class, () -> valueResolver.resolveCodeValue(Character.class, "aa")); + assertThrows(IllegalArgumentException.class, () -> valueResolver.resolveCodeValue(Character.class, "")); + } + + @Test + void testCoerceString() { + assertEquals(new CodeValue.String("hello"), valueResolver.resolveCodeValue(String.class, "hello")); + } + + @Test + void testCoerceArray() { + assertEquals(new CodeValue.ArrayInitialization.Declared(String.class, List.of(new CodeValue.String("hello"), + new CodeValue.String("world"))), + valueResolver.resolveCodeValue(String[].class, "hello,world")); + } + + @Test + void testParseMethod() { + assertEquals(new CodeValue.Literal("100.0"), valueResolver.resolveCodeValue(double.class, "100")); + assertEquals(new CodeValue.Literal("100.0"), valueResolver.resolveCodeValue(Double.class, "100")); + } + + @Test + void testValueOfMethod() { + assertEquals(new CodeValue.MethodCall(new CodeValue.Type(Duration.class), "valueOf", + List.of(new CodeValue.String("1s"))), + valueResolver.resolveCodeValue(Duration.class, "1s")); + assertThrows(UnsupportedOperationException.class, () -> valueResolver.resolveCodeValue(Duration.class, "")); + } + + @Test + void testStringParsingMethods() throws Exception { + assertEquals(new CodeValue.Literal("100.0"), + valueResolver.resolveCodeValue(Double.class.getMethod("parseDouble", String.class), "100")); + assertEquals(new CodeValue.FieldAccess(new CodeValue.Type(Double.class), "POSITIVE_INFINITY"), + valueResolver.resolveCodeValue(Double.class.getMethod("parseDouble", String.class), "Infinity")); + assertEquals(new CodeValue.FieldAccess(new CodeValue.Type(Double.class), "NEGATIVE_INFINITY"), + valueResolver.resolveCodeValue(Double.class.getMethod("parseDouble", String.class), "-Infinity")); + assertEquals(new CodeValue.FieldAccess(new CodeValue.Type(Double.class), "NaN"), + valueResolver.resolveCodeValue(Double.class.getMethod("parseDouble", String.class), "NaN")); + assertEquals(new CodeValue.FieldAccess(new CodeValue.Type(Float.class), "POSITIVE_INFINITY"), + valueResolver.resolveCodeValue(Float.class.getMethod("parseFloat", String.class), "Infinity")); + assertEquals(new CodeValue.FieldAccess(new CodeValue.Type(Float.class), "NEGATIVE_INFINITY"), + valueResolver.resolveCodeValue(Float.class.getMethod("parseFloat", String.class), "-Infinity")); + assertEquals(new CodeValue.FieldAccess(new CodeValue.Type(Float.class), "NaN"), + valueResolver.resolveCodeValue(Float.class.getMethod("parseFloat", String.class), "NaN")); + assertEquals(new CodeValue.Enum(VPos.BASELINE), + valueResolver.resolveCodeValue(VPos.class.getMethod("valueOf", String.class), "BASELINE")); + } + + @Test + void testStringParsingMethodFails() { + assertThrows(InvocationTargetException.class, + () -> valueResolver.resolveCodeValue(Double.class.getMethod("parseDouble", String.class), "")); + } + + @Test + void testStringParsingMethodNotStatic() { + assertThrows(IllegalArgumentException.class, + () -> valueResolver.resolveCodeValue(Double.class.getMethod("doubleValue"), "")); + } + + @Test + void testStringParsingMethodNotString() { + assertThrows(IllegalArgumentException.class, + () -> valueResolver.resolveCodeValue(Character.class.getMethod("valueOf", char.class), "")); + } + + @Test + void testStringParsingMethodMultipleParameters() { + assertThrows(IllegalArgumentException.class, + () -> valueResolver.resolveCodeValue( + String.class.getMethod("format", String.class, Object[].class), "")); + } + + @Test + void testResolveEmptyValue() { + assertEquals(new CodeValue.Null(), valueResolver.resolveCodeValue(Object.class, new Value.Empty())); + } + + @Test + void testResolveReference() { + resolverContainer.getNameResolver().storeIdType("a", Double.class); + assertThrows(IllegalArgumentException.class, + () -> valueResolver.resolveCodeValue(Float.class, new Value.Reference("a"))); + assertEquals(new CodeValue.Literal("a"), + valueResolver.resolveCodeValue(Double.class, new Value.Reference("a"))); + } + + @Test + void testResolveResource() { + assertThrows(UnsupportedOperationException.class, + () -> valueResolver.resolveCodeValue(Float.class, new Value.Resource("a"))); + assertEquals(new CodeValue.MethodCall(new CodeValue.Literal(FxmlProcessor.RESOURCES_NAME), "getString", + List.of(new CodeValue.String("a"))), + valueResolver.resolveCodeValue(String.class, new Value.Resource("a"))); + } + + @Test + void testResolveLiteral() { + assertEquals(new CodeValue.String("a"), + valueResolver.resolveCodeValue(String.class, new Value.Literal("a"))); + } + + @Test + void testResolveExpression() { + assertThrows(UnsupportedOperationException.class, + () -> valueResolver.resolveCodeValue(Float.class, new Expression.Null())); + } + + @Test + void testResolveLocation() { + assertThrows(UnsupportedOperationException.class, + () -> valueResolver.resolveCodeValue(Float.class, new Value.Location(Path.of("")))); + } + + private static class DefaultValueProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return ValueResolver.DEFAULTS_MAP.entrySet() + .stream() + .map(entry -> Arguments.of(entry.getKey(), entry.getValue())); + } + } + +} diff --git a/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/processor/FxmlProcessorTest.java b/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/processor/FxmlProcessorTest.java index 3f86777..b9a9bd6 100644 --- a/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/processor/FxmlProcessorTest.java +++ b/fx2j-processor/src/test/java/io/github/sheikah45/fx2j/processor/processor/FxmlProcessorTest.java @@ -11,6 +11,7 @@ import javafx.scene.control.Label; import javafx.scene.control.SpinnerValueFactory; import javafx.scene.control.SplitPane; +import javafx.scene.control.Tab; import javafx.scene.control.TableView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; @@ -28,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertSame; @@ -106,6 +108,12 @@ void testPropertyInnerText() throws Exception { assertEquals("test", button.getText()); } + @Test + void testDefaultPropertyElement() throws Exception { + Tab root = buildAndRetrieveRoot(PROCESS_FXML.resolve("default-property-element.fxml")); + assertInstanceOf(Button.class, root.getContent()); + } + @Test void testPropertyAttribute() throws Exception { AnchorPane root = buildAndRetrieveRoot(PROCESS_FXML.resolve("attribute-property.fxml")); @@ -134,6 +142,12 @@ void testBasicList() throws Exception { assertEquals(List.of("item1", 2), root); } + @Test + void testObservableList() throws Exception { + ObservableList root = buildAndRetrieveRoot(PROCESS_FXML.resolve("observable-list.fxml")); + assertEquals(List.of("item1", 2), root); + } + @Test void testInferredMap() throws Exception { Map root = buildAndRetrieveRoot(PROCESS_FXML.resolve("inferred-map.fxml")); diff --git a/fx2j-processor/src/test/resources/fxml/process/default-property-element.fxml b/fx2j-processor/src/test/resources/fxml/process/default-property-element.fxml new file mode 100644 index 0000000..9b46fa4 --- /dev/null +++ b/fx2j-processor/src/test/resources/fxml/process/default-property-element.fxml @@ -0,0 +1,5 @@ + + + +