diff --git a/api/src/main/java/com/github/gtache/fxml/compiler/Generator.java b/api/src/main/java/com/github/gtache/fxml/compiler/Generator.java index c3351dd..9bfd8d4 100644 --- a/api/src/main/java/com/github/gtache/fxml/compiler/Generator.java +++ b/api/src/main/java/com/github/gtache/fxml/compiler/Generator.java @@ -3,6 +3,7 @@ package com.github.gtache.fxml.compiler; /** * Generates compiled FXML code */ +@FunctionalInterface public interface Generator { /** diff --git a/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedConstant.java b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedConstant.java index b376a8c..05d2597 100644 --- a/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedConstant.java +++ b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedConstant.java @@ -1,6 +1,7 @@ package com.github.gtache.fxml.compiler.parsing; import java.util.LinkedHashMap; +import java.util.List; import java.util.SequencedCollection; import java.util.SequencedMap; @@ -27,4 +28,9 @@ public interface ParsedConstant extends ParsedObject { default SequencedMap> properties() { return new LinkedHashMap<>(); } + + @Override + default SequencedCollection children() { + return List.of(); + } } diff --git a/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedCopy.java b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedCopy.java new file mode 100644 index 0000000..9575d61 --- /dev/null +++ b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedCopy.java @@ -0,0 +1,42 @@ +package com.github.gtache.fxml.compiler.parsing; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.SequencedCollection; +import java.util.SequencedMap; + +/** + * Special {@link ParsedObject} for fx:copy + */ +@FunctionalInterface +public interface ParsedCopy extends ParsedObject { + + /** + * Returns the source from fx:copy + * + * @return The source + */ + default String source() { + final var attribute = attributes().get("source"); + if (attribute == null) { + throw new IllegalStateException("Missing source"); + } else { + return attribute.value(); + } + } + + @Override + default String className() { + return ParsedCopy.class.getName(); + } + + @Override + default SequencedMap> properties() { + return new LinkedHashMap<>(); + } + + @Override + default SequencedCollection children() { + return List.of(); + } +} diff --git a/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedDefine.java b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedDefine.java new file mode 100644 index 0000000..6d2005c --- /dev/null +++ b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedDefine.java @@ -0,0 +1,39 @@ +package com.github.gtache.fxml.compiler.parsing; + +import java.util.Map; +import java.util.SequencedCollection; +import java.util.SequencedMap; + +/** + * Special {@link ParsedObject} for fx:define + */ +@FunctionalInterface +public interface ParsedDefine extends ParsedObject { + + /** + * Returns the object defined by this fx:define + * + * @return The object + */ + ParsedObject object(); + + @Override + default String className() { + return object().className(); + } + + @Override + default Map attributes() { + return object().attributes(); + } + + @Override + default SequencedMap> properties() { + return object().properties(); + } + + @Override + default SequencedCollection children() { + return object().children(); + } +} diff --git a/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedFactory.java b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedFactory.java new file mode 100644 index 0000000..e753dc1 --- /dev/null +++ b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedFactory.java @@ -0,0 +1,37 @@ +package com.github.gtache.fxml.compiler.parsing; + +import java.util.LinkedHashMap; +import java.util.SequencedCollection; +import java.util.SequencedMap; + +/** + * Special {@link ParsedObject} for fx:factory + */ +public interface ParsedFactory extends ParsedObject { + + /** + * Returns the factory value from fx:factory + * + * @return The value + */ + default String factory() { + final var attribute = attributes().get("fx:factory"); + if (attribute == null) { + throw new IllegalStateException("Missing fx:factory"); + } else { + return attribute.value(); + } + } + + @Override + default SequencedMap> properties() { + return new LinkedHashMap<>(); + } + + /** + * Returns the arguments for the factory + * + * @return The arguments + */ + SequencedCollection arguments(); +} diff --git a/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedInclude.java b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedInclude.java index 50868b1..e67a611 100644 --- a/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedInclude.java +++ b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedInclude.java @@ -1,6 +1,7 @@ package com.github.gtache.fxml.compiler.parsing; import java.util.LinkedHashMap; +import java.util.List; import java.util.SequencedCollection; import java.util.SequencedMap; @@ -61,4 +62,9 @@ public interface ParsedInclude extends ParsedObject { default SequencedMap> properties() { return new LinkedHashMap<>(); } + + @Override + default SequencedCollection children() { + return List.of(); + } } diff --git a/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedObject.java b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedObject.java index 65b09a5..78ef8a7 100644 --- a/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedObject.java +++ b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedObject.java @@ -29,4 +29,11 @@ public interface ParsedObject { * @return The properties */ SequencedMap> properties(); + + /** + * Returns the children (fx:define, fx:copy, etc.) contained in this object + * + * @return The children + */ + SequencedCollection children(); } diff --git a/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedReference.java b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedReference.java new file mode 100644 index 0000000..a6361a0 --- /dev/null +++ b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedReference.java @@ -0,0 +1,42 @@ +package com.github.gtache.fxml.compiler.parsing; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.SequencedCollection; +import java.util.SequencedMap; + +/** + * Special {@link ParsedObject} for fx:reference + */ +@FunctionalInterface +public interface ParsedReference extends ParsedObject { + + /** + * Returns the source from fx:reference + * + * @return The source + */ + default String source() { + final var attribute = attributes().get("source"); + if (attribute == null) { + throw new IllegalStateException("Missing source"); + } else { + return attribute.value(); + } + } + + @Override + default String className() { + return ParsedReference.class.getName(); + } + + @Override + default SequencedMap> properties() { + return new LinkedHashMap<>(); + } + + @Override + default SequencedCollection children() { + return List.of(); + } +} diff --git a/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedText.java b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedText.java new file mode 100644 index 0000000..dba2ff4 --- /dev/null +++ b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedText.java @@ -0,0 +1,41 @@ +package com.github.gtache.fxml.compiler.parsing; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.SequencedCollection; +import java.util.SequencedMap; + +/** + * Special {@link ParsedObject} for simple text + */ +@FunctionalInterface +public interface ParsedText extends ParsedObject { + + /** + * Returns the text value + * + * @return The value + */ + String text(); + + @Override + default String className() { + return "java.lang.String"; + } + + @Override + default SequencedCollection children() { + return List.of(); + } + + @Override + default SequencedMap> properties() { + return new LinkedHashMap<>(); + } + + @Override + default Map attributes() { + return Map.of(); + } +} diff --git a/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedValue.java b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedValue.java new file mode 100644 index 0000000..9cbd819 --- /dev/null +++ b/api/src/main/java/com/github/gtache/fxml/compiler/parsing/ParsedValue.java @@ -0,0 +1,36 @@ +package com.github.gtache.fxml.compiler.parsing; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.SequencedCollection; +import java.util.SequencedMap; + +/** + * Special {@link ParsedObject} for fx:value + */ +public interface ParsedValue extends ParsedObject { + + /** + * Returns the value from fx:value + * + * @return The value + */ + default String value() { + final var attribute = attributes().get("fx:value"); + if (attribute == null) { + throw new IllegalStateException("Missing fx:value"); + } else { + return attribute.value(); + } + } + + @Override + default SequencedMap> properties() { + return new LinkedHashMap<>(); + } + + @Override + default SequencedCollection children() { + return List.of(); + } +} diff --git a/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestFXMLParser.java b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestFXMLParser.java new file mode 100644 index 0000000..2dabca0 --- /dev/null +++ b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestFXMLParser.java @@ -0,0 +1,53 @@ +package com.github.gtache.fxml.compiler.parsing; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.nio.file.Files; +import java.nio.file.Paths; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class TestFXMLParser { + + private final FXMLParser parser; + private final String content; + private final ParsedObject object; + + TestFXMLParser(@Mock final ParsedObject object) { + this.parser = spy(FXMLParser.class); + this.content = "content"; + this.object = requireNonNull(object); + } + + @BeforeEach + void beforeEach() throws ParseException { + when(parser.parse(content)).thenReturn(object); + } + + @Test + void testParse() throws Exception { + final var file = Files.createTempFile("test", ".fxml"); + try { + Files.writeString(file, content); + assertEquals(object, parser.parse(file)); + verify(parser).parse(content); + } finally { + Files.deleteIfExists(file); + } + } + + @Test + void testParseIOException() throws Exception { + final var file = Paths.get("whatever"); + assertThrows(ParseException.class, () -> parser.parse(file)); + verify(parser, never()).parse(content); + } +} diff --git a/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedConstant.java b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedConstant.java index bf484fe..a65c446 100644 --- a/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedConstant.java +++ b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedConstant.java @@ -9,6 +9,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -53,4 +54,9 @@ class TestParsedConstant { void testProperties() { assertEquals(new LinkedHashMap<>(), constant.properties()); } + + @Test + void testChildren() { + assertEquals(List.of(), constant.children()); + } } diff --git a/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedCopy.java b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedCopy.java new file mode 100644 index 0000000..e8be11c --- /dev/null +++ b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedCopy.java @@ -0,0 +1,67 @@ +package com.github.gtache.fxml.compiler.parsing; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TestParsedCopy { + + private final Map attributes; + private final ParsedProperty property; + private final String string; + private final ParsedCopy reference; + + TestParsedCopy(@Mock final ParsedProperty property) { + this.attributes = new HashMap<>(); + this.property = Objects.requireNonNull(property); + this.string = "str/ing"; + this.reference = spy(ParsedCopy.class); + } + + @BeforeEach + void beforeEach() { + when(reference.attributes()).thenReturn(attributes); + when(property.value()).thenReturn(string); + } + + @Test + void testSourceNull() { + assertThrows(IllegalStateException.class, reference::source); + } + + @Test + void testSource() { + attributes.put("source", property); + assertEquals(string, reference.source()); + } + + @Test + void testClassName() { + assertEquals(ParsedCopy.class.getName(), reference.className()); + } + + @Test + void testProperties() { + assertEquals(new LinkedHashMap<>(), reference.properties()); + } + + @Test + void testChildren() { + assertEquals(List.of(), reference.children()); + } +} diff --git a/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedDefine.java b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedDefine.java new file mode 100644 index 0000000..b585935 --- /dev/null +++ b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedDefine.java @@ -0,0 +1,80 @@ +package com.github.gtache.fxml.compiler.parsing; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.SequencedCollection; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class TestParsedDefine { + + private final ParsedProperty property; + private final ParsedObject object; + private final String string; + private final ParsedDefine define; + + TestParsedDefine(@Mock final ParsedProperty property, @Mock final ParsedObject object) { + this.property = requireNonNull(property); + this.object = requireNonNull(object); + this.string = "str/ing"; + this.define = spy(ParsedDefine.class); + } + + @BeforeEach + void beforeEach() { + when(property.value()).thenReturn(string); + when(define.object()).thenReturn(object); + when(object.className()).thenReturn(string); + when(object.children()).thenReturn(List.of(define)); + final var map = new LinkedHashMap>(); + map.put(property, List.of(object)); + when(object.properties()).thenReturn(map); + when(object.attributes()).thenReturn(Map.of(string, property)); + } + + @Test + void testObject() { + assertEquals(object, define.object()); + } + + @Test + void testClassName() { + assertEquals(string, define.className()); + verify(define).object(); + verify(object).className(); + } + + @Test + void testAttributes() { + assertEquals(Map.of(string, property), define.attributes()); + verify(define).object(); + verify(object).attributes(); + } + + @Test + void testProperties() { + final var map = new LinkedHashMap>(); + map.put(property, List.of(object)); + assertEquals(map, define.properties()); + verify(define).object(); + verify(object).properties(); + } + + @Test + void testChildren() { + assertEquals(List.of(define), define.children()); + verify(define).object(); + verify(object).children(); + } +} diff --git a/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedFactory.java b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedFactory.java new file mode 100644 index 0000000..9561ad3 --- /dev/null +++ b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedFactory.java @@ -0,0 +1,56 @@ +package com.github.gtache.fxml.compiler.parsing; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TestParsedFactory { + + private final Map attributes; + private final ParsedProperty property; + private final String string; + private final ParsedFactory value; + + TestParsedFactory(@Mock final ParsedProperty property) { + this.attributes = new HashMap<>(); + this.property = Objects.requireNonNull(property); + this.string = "str/ing"; + this.value = spy(ParsedFactory.class); + } + + @BeforeEach + void beforeEach() { + when(value.attributes()).thenReturn(attributes); + when(property.value()).thenReturn(string); + } + + @Test + void testConstantNull() { + assertThrows(IllegalStateException.class, value::factory); + } + + @Test + void testConstant() { + attributes.put("fx:factory", property); + assertEquals(string, value.factory()); + } + + @Test + void testProperties() { + assertEquals(new LinkedHashMap<>(), value.properties()); + } +} diff --git a/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedInclude.java b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedInclude.java index 056feb7..5219b7f 100644 --- a/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedInclude.java +++ b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedInclude.java @@ -9,6 +9,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -79,4 +80,9 @@ class TestParsedInclude { void testProperties() { assertEquals(new LinkedHashMap<>(), include.properties()); } + + @Test + void testChildren() { + assertEquals(List.of(), include.children()); + } } diff --git a/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedReference.java b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedReference.java new file mode 100644 index 0000000..d63f3b4 --- /dev/null +++ b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedReference.java @@ -0,0 +1,67 @@ +package com.github.gtache.fxml.compiler.parsing; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TestParsedReference { + + private final Map attributes; + private final ParsedProperty property; + private final String string; + private final ParsedReference reference; + + TestParsedReference(@Mock final ParsedProperty property) { + this.attributes = new HashMap<>(); + this.property = Objects.requireNonNull(property); + this.string = "str/ing"; + this.reference = spy(ParsedReference.class); + } + + @BeforeEach + void beforeEach() { + when(reference.attributes()).thenReturn(attributes); + when(property.value()).thenReturn(string); + } + + @Test + void testSourceNull() { + assertThrows(IllegalStateException.class, reference::source); + } + + @Test + void testSource() { + attributes.put("source", property); + assertEquals(string, reference.source()); + } + + @Test + void testClassName() { + assertEquals(ParsedReference.class.getName(), reference.className()); + } + + @Test + void testProperties() { + assertEquals(new LinkedHashMap<>(), reference.properties()); + } + + @Test + void testChildren() { + assertEquals(List.of(), reference.children()); + } +} diff --git a/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedText.java b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedText.java new file mode 100644 index 0000000..bda2d15 --- /dev/null +++ b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedText.java @@ -0,0 +1,39 @@ +package com.github.gtache.fxml.compiler.parsing; + +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.spy; + +class TestParsedText { + + private final ParsedText parsedText; + + TestParsedText() { + this.parsedText = spy(ParsedText.class); + } + + @Test + void testClassName() { + assertEquals(String.class.getName(), parsedText.className()); + } + + @Test + void testChildren() { + assertEquals(List.of(), parsedText.children()); + } + + @Test + void testProperties() { + assertEquals(new LinkedHashMap<>(), parsedText.properties()); + } + + @Test + void testAttributes() { + assertEquals(Map.of(), parsedText.attributes()); + } +} diff --git a/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedValue.java b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedValue.java new file mode 100644 index 0000000..5f289f6 --- /dev/null +++ b/api/src/test/java/com/github/gtache/fxml/compiler/parsing/TestParsedValue.java @@ -0,0 +1,62 @@ +package com.github.gtache.fxml.compiler.parsing; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TestParsedValue { + + private final Map attributes; + private final ParsedProperty property; + private final String string; + private final ParsedValue value; + + TestParsedValue(@Mock final ParsedProperty property) { + this.attributes = new HashMap<>(); + this.property = Objects.requireNonNull(property); + this.string = "str/ing"; + this.value = spy(ParsedValue.class); + } + + @BeforeEach + void beforeEach() { + when(value.attributes()).thenReturn(attributes); + when(property.value()).thenReturn(string); + } + + @Test + void testConstantNull() { + assertThrows(IllegalStateException.class, value::value); + } + + @Test + void testConstant() { + attributes.put("fx:value", property); + assertEquals(string, value.value()); + } + + @Test + void testProperties() { + assertEquals(new LinkedHashMap<>(), value.properties()); + } + + @Test + void testChildren() { + assertEquals(List.of(), value.children()); + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/GeneratorImpl.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/GeneratorImpl.java index 2631ded..d9f8a20 100644 --- a/core/src/main/java/com/github/gtache/fxml/compiler/impl/GeneratorImpl.java +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/GeneratorImpl.java @@ -1,83 +1,59 @@ package com.github.gtache.fxml.compiler.impl; -import com.github.gtache.fxml.compiler.ControllerInjection; import com.github.gtache.fxml.compiler.GenerationException; import com.github.gtache.fxml.compiler.GenerationRequest; import com.github.gtache.fxml.compiler.Generator; -import com.github.gtache.fxml.compiler.parsing.ParsedConstant; -import com.github.gtache.fxml.compiler.parsing.ParsedInclude; -import com.github.gtache.fxml.compiler.parsing.ParsedObject; -import com.github.gtache.fxml.compiler.parsing.ParsedProperty; -import javafx.event.EventHandler; +import com.github.gtache.fxml.compiler.impl.internal.GenerationProgress; +import com.github.gtache.fxml.compiler.impl.internal.HelperMethodsProvider; -import java.lang.reflect.Constructor; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.regex.Pattern; -import java.util.stream.Collectors; +import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.getControllerInjection; +import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.getVariablePrefix; +import static com.github.gtache.fxml.compiler.impl.internal.ObjectFormatter.format; -import static com.github.gtache.fxml.compiler.impl.ReflectionHelper.*; +//TODO handle binding (${}) /** * Implementation of {@link Generator} */ public class GeneratorImpl implements Generator { - private static final Pattern INT_PATTERN = Pattern.compile("\\d+"); - private static final Pattern DECIMAL_PATTERN = Pattern.compile("\\d+(?:\\.\\d+)?"); - - private final Collection controllerFactoryPostAction; - private final Map variableNameCounters; - - /** - * Instantiates a new generator - */ - public GeneratorImpl() { - this.controllerFactoryPostAction = new ArrayList<>(); - this.variableNameCounters = new ConcurrentHashMap<>(); - } @Override public String generate(final GenerationRequest request) throws GenerationException { - controllerFactoryPostAction.clear(); - variableNameCounters.clear(); + final var progress = new GenerationProgress(request); final var className = request.outputClassName(); final var pkgName = className.substring(0, className.lastIndexOf('.')); final var simpleClassName = className.substring(className.lastIndexOf('.') + 1); - final var loadMethod = getLoadMethod(request); - final var controllerInjection = getControllerInjection(request); + final var loadMethod = getLoadMethod(progress); + final var controllerInjection = getControllerInjection(progress); final var controllerInjectionType = controllerInjection.fieldInjectionType(); + final var controllerInjectionClass = controllerInjection.injectionClass(); final String constructorArgument; final String constructorControllerJavadoc; final String controllerArgumentType; final String controllerMapType; - final var controllerInjectionClass = controllerInjection.injectionClass(); - final var imports = getImports(request); if (controllerInjectionType == ControllerFieldInjectionTypes.FACTORY) { constructorArgument = "controllerFactory"; constructorControllerJavadoc = "controller factory"; - controllerArgumentType = "ControllerFactory<" + controllerInjectionClass + ">"; - controllerMapType = "ControllerFactory"; + controllerArgumentType = "com.github.gtache.fxml.compiler.ControllerFactory<" + controllerInjectionClass + ">"; + controllerMapType = "com.github.gtache.fxml.compiler.ControllerFactory"; } else { constructorArgument = "controller"; constructorControllerJavadoc = "controller"; controllerArgumentType = controllerInjectionClass; controllerMapType = "Object"; } - final var helperMethods = getHelperMethods(request); + final var helperMethods = HelperMethodsProvider.getHelperMethods(progress); return """ package %1$s; - %9$s - /** * Generated code, not thread-safe */ public final class %2$s { - private final Map, %7$s> controllersMap; - private final Map, ResourceBundle> resourceBundlesMap; + private final java.util.Map, %7$s> controllersMap; + private final java.util.Map, java.util.ResourceBundle> resourceBundlesMap; private boolean loaded; private %3$s controller; @@ -86,7 +62,7 @@ public class GeneratorImpl implements Generator { * @param %4$s The %5$s */ public %2$s(final %8$s %4$s) { - this(Map.of(%3$s.class, %4$s), Map.of()); + this(java.util.Map.of(%3$s.class, %4$s), java.util.Map.of()); } /** @@ -94,8 +70,8 @@ public class GeneratorImpl implements Generator { * @param %4$s The %5$s * @param resourceBundle The resource bundle */ - public %2$s(final %8$s %4$s, final ResourceBundle resourceBundle) { - this(Map.of(%3$s.class, %4$s), Map.of(%3$s.class, resourceBundle)); + public %2$s(final %8$s %4$s, final java.util.ResourceBundle resourceBundle) { + this(java.util.Map.of(%3$s.class, %4$s), java.util.Map.of(%3$s.class, resourceBundle)); } /** @@ -103,9 +79,9 @@ public class GeneratorImpl implements Generator { * @param controllersMap The map of controller class to %5$s * @param resourceBundlesMap The map of controller class to resource bundle */ - public %2$s(final Map, %7$s> controllersMap, final Map, ResourceBundle> resourceBundlesMap) { - this.controllersMap = Map.copyOf(controllersMap); - this.resourceBundlesMap = Map.copyOf(resourceBundlesMap); + public %2$s(final java.util.Map, %7$s> controllersMap, final java.util.Map, java.util.ResourceBundle> resourceBundlesMap) { + this.controllersMap = java.util.Map.copyOf(controllersMap); + this.resourceBundlesMap = java.util.Map.copyOf(resourceBundlesMap); } /** @@ -115,7 +91,7 @@ public class GeneratorImpl implements Generator { */ %6$s - %10$s + %9$s /** * @return The controller @@ -129,113 +105,30 @@ public class GeneratorImpl implements Generator { } } """.formatted(pkgName, simpleClassName, controllerInjectionClass, constructorArgument, constructorControllerJavadoc, - loadMethod, controllerMapType, controllerArgumentType, imports, helperMethods); + loadMethod, controllerMapType, controllerArgumentType, helperMethods); } - /** - * Gets helper methods string for the given generation request - * - * @param request The generation request - * @return The helper methods - */ - private static String getHelperMethods(final GenerationRequest request) throws GenerationException { - final var injection = getControllerInjection(request); - final var methodInjectionType = injection.methodInjectionType(); - final var sb = new StringBuilder(); - if (methodInjectionType == ControllerMethodsInjectionType.REFLECTION) { - sb.append(""" - private void callMethod(final String methodName, final T event) { - try { - final Method method; - final var methods = Arrays.stream(controller.getClass().getDeclaredMethods()) - .filter(m -> m.getName().equals(methodName)).toList(); - if (methods.size() > 1) { - final var eventMethods = methods.stream().filter(m -> - m.getParameterCount() == 1 && Event.class.isAssignableFrom(m.getParameterTypes()[0])).toList(); - if (eventMethods.size() == 1) { - method = eventMethods.getFirst(); - } else { - final var emptyMethods = methods.stream().filter(m -> m.getParameterCount() == 0).toList(); - if (emptyMethods.size() == 1) { - method = emptyMethods.getFirst(); - } else { - throw new IllegalArgumentException("Multiple matching methods for " + methodName); - } - } - } else if (methods.size() == 1) { - method = methods.getFirst(); - } else { - throw new IllegalArgumentException("No matching method for " + methodName); - } - method.setAccessible(true); - if (method.getParameterCount() == 0) { - method.invoke(controller); - } else { - method.invoke(controller, event); - } - } catch (final IllegalAccessException | InvocationTargetException ex) { - throw new RuntimeException("Error using reflection on " + methodName, ex); - } - } - """); - } - if (injection.fieldInjectionType() == ControllerFieldInjectionTypes.REFLECTION) { - sb.append(""" - private void injectField(final String fieldName, final T object) { - try { - final var field = controller.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.set(controller, object); - } catch (final NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException("Error using reflection on " + fieldName, e); - } - } - """); - } - return sb.toString(); - } - - /** - * Gets imports for the given generation request - * - * @param request The generation request - * @return The imports - */ - private static String getImports(final GenerationRequest request) throws GenerationException { - final var injection = getControllerInjection(request); - final var fieldInjectionType = injection.fieldInjectionType(); - final var sb = new StringBuilder("import java.util.Map;\nimport java.util.ResourceBundle;\nimport java.util.HashMap;\n"); - if (fieldInjectionType == ControllerFieldInjectionTypes.FACTORY) { - sb.append("import com.github.gtache.fxml.compiler.ControllerFactory;\n"); - } - final var methodInjectionType = injection.methodInjectionType(); - if (methodInjectionType == ControllerMethodsInjectionType.REFLECTION) { - sb.append("import java.lang.reflect.InvocationTargetException;\n"); - sb.append("import java.util.Arrays;\n"); - sb.append("import javafx.event.Event;\n"); - sb.append("import java.lang.reflect.Method;\n"); - } - return sb.toString(); - } /** * Computes the load method * - * @param request The generation request + * @param progress The generation progress * @return The load method */ - private String getLoadMethod(final GenerationRequest request) throws GenerationException { + private static String getLoadMethod(final GenerationProgress progress) throws GenerationException { + final var request = progress.request(); final var rootObject = request.rootObject(); - final var controllerInjection = getControllerInjection(request); + final var controllerInjection = getControllerInjection(progress); final var controllerInjectionType = controllerInjection.fieldInjectionType(); final var controllerClass = controllerInjection.injectionClass(); - final var sb = new StringBuilder("public T load() {\n"); + final var sb = progress.stringBuilder(); + sb.append("public T load() {\n"); sb.append(" if (loaded) {\n"); sb.append(" throw new IllegalStateException(\"Already loaded\");\n"); sb.append(" }\n"); final var resourceBundleInjection = request.parameters().resourceBundleInjection(); if (resourceBundleInjection.injectionType() == ResourceBundleInjectionTypes.GET_BUNDLE) { - sb.append(" final var bundle = ResourceBundle.getBundle(\"").append(resourceBundleInjection.bundleName()).append("\");\n"); + sb.append(" final var bundle = java.util.ResourceBundle.getBundle(\"").append(resourceBundleInjection.bundleName()).append("\");\n"); } else if (resourceBundleInjection.injectionType() == ResourceBundleInjectionTypes.CONSTRUCTOR) { sb.append(" final var bundle = resourceBundlesMap.get(").append(controllerClass).append(".class);\n"); } @@ -244,19 +137,19 @@ public class GeneratorImpl implements Generator { } else { sb.append(" controller = (").append(controllerClass).append(") controllersMap.get(").append(controllerClass).append(".class);\n"); } - final var variableName = getNextVariableName("object"); - format(request, rootObject, variableName, sb); + final var variableName = progress.getNextVariableName(getVariablePrefix(rootObject)); + format(progress, rootObject, variableName); if (controllerInjectionType == ControllerFieldInjectionTypes.FACTORY) { sb.append(" final var controllerFactory = controllersMap.get(").append(controllerClass).append(".class);\n"); sb.append(" controller = (").append(controllerClass).append(") controllerFactory.create(fieldMap);\n"); - controllerFactoryPostAction.forEach(sb::append); + progress.controllerFactoryPostAction().forEach(sb::append); } if (controllerInjection.methodInjectionType() == ControllerMethodsInjectionType.REFLECTION) { sb.append(" try {\n"); sb.append(" final var initialize = controller.getClass().getDeclaredMethod(\"initialize\");\n"); sb.append(" initialize.setAccessible(true);\n"); sb.append(" initialize.invoke(controller);\n"); - sb.append(" } catch (final InvocationTargetException | IllegalAccessException e) {\n"); + sb.append(" } catch (final java.lang.reflect.InvocationTargetException | IllegalAccessException e) {\n"); sb.append(" throw new RuntimeException(\"Error using reflection\", e);\n"); sb.append(" } catch (final NoSuchMethodException ignored) {\n"); sb.append(" }\n"); @@ -264,584 +157,10 @@ public class GeneratorImpl implements Generator { sb.append(" controller.initialize();\n"); } sb.append(" loaded = true;\n"); - sb.append(" return ").append(variableName).append(";\n"); + sb.append(" return (T) ").append(variableName).append(";\n"); sb.append("}"); return sb.toString(); } - /** - * Formats an object - * - * @param request The generation request - * @param parsedObject The parsed object to format - * @param variableName The variable name for the object - * @param sb The string builder - */ - private void format(final GenerationRequest request, final ParsedObject parsedObject, final String variableName, final StringBuilder sb) throws GenerationException { - switch (parsedObject) { - case final ParsedInclude include -> formatInclude(request, include, variableName, sb); - case final ParsedConstant constant -> formatConstant(constant, variableName, sb); - default -> { - final var clazz = getClass(parsedObject.className()); - final var constructors = clazz.getConstructors(); - final var allPropertyNames = new HashSet<>(parsedObject.attributes().keySet()); - allPropertyNames.addAll(parsedObject.properties().keySet().stream().map(ParsedProperty::name).collect(Collectors.toSet())); - final var constructorArgs = getMatchingConstructorArgs(constructors, allPropertyNames); - if (constructorArgs == null) { - if (allPropertyNames.size() == 1 && allPropertyNames.iterator().next().equals("fx:constant")) { - final var property = parsedObject.attributes().get("fx:constant"); - sb.append(" final var ").append(variableName).append(" = ").append(clazz.getCanonicalName()).append(".").append(property.value()).append(";\n"); - } else { - throw new GenerationException("Cannot find constructor for " + clazz.getCanonicalName()); - } - } else { - final var args = getListConstructorArgs(constructorArgs, parsedObject); - final var genericTypes = getGenericTypes(request, parsedObject); - sb.append(" final var ").append(variableName).append(" = new ").append(clazz.getCanonicalName()) - .append(genericTypes).append("(").append(String.join(", ", args)).append(");\n"); - final var sortedAttributes = parsedObject.attributes().entrySet().stream().sorted(Map.Entry.comparingByKey()).toList(); - for (final var e : sortedAttributes) { - if (!constructorArgs.namedArgs().containsKey(e.getKey())) { - final var p = e.getValue(); - formatProperty(request, p, parsedObject, variableName, sb); - } - } - final var sortedProperties = parsedObject.properties().entrySet().stream().sorted(Comparator.comparing(e -> e.getKey().name())).toList(); - for (final var e : sortedProperties) { - if (!constructorArgs.namedArgs().containsKey(e.getKey().name())) { - final var p = e.getKey(); - final var o = e.getValue(); - formatChild(request, parsedObject, p, o, variableName, sb); - } - } - } - } - } - } - private static String getGenericTypes(final GenerationRequest request, final ParsedObject parsedObject) throws GenerationException { - final var clazz = getClass(parsedObject.className()); - if (isGeneric(clazz)) { - final var idProperty = parsedObject.attributes().get("fx:id"); - if (idProperty == null) { - return "<>"; - } else { - final var id = idProperty.value(); - final var genericTypes = request.controllerInfo().propertyGenericTypes(id); - if (genericTypes == null) { //Raw - return ""; - } else { - return "<" + String.join(", ", genericTypes) + ">"; - } - } - } - return ""; - } - - - /** - * Formats an include object - * - * @param request The generation request - * @param include The include object - * @param subNodeName The sub node name - * @param sb The string builder - */ - private void formatInclude(final GenerationRequest request, final ParsedInclude include, final String subNodeName, final StringBuilder sb) throws GenerationException { - final var subViewVariable = getNextVariableName("view"); - final var source = include.source(); - final var resources = include.resources(); - final var subControllerClass = request.parameters().sourceToControllerName().get(source); - final var subClassName = request.parameters().sourceToGeneratedClassName().get(source); - if (subClassName == null) { - throw new GenerationException("Unknown include source : " + source); - } - if (resources == null) { - sb.append(" final var ").append(subViewVariable).append(" = new ").append(subClassName).append("(controllersMap, resourceBundlesMap);\n"); - } else { - final var subResourceBundlesMapVariable = getNextVariableName("map"); - final var subBundleVariable = getNextVariableName("bundle"); - sb.append(" final var ").append(subResourceBundlesMapVariable).append(" = new HashMap<>(resourceBundlesMap);\n"); - sb.append(" final var ").append(subBundleVariable).append(" = ResourceBundle.getBundle(\"").append(resources).append("\");\n"); - sb.append(" ").append(subResourceBundlesMapVariable).append(".put(").append(subControllerClass).append(", ").append(subBundleVariable).append(");\n"); - sb.append(" final var ").append(subViewVariable).append(" = new ").append(subClassName).append("(controllersMap, ").append(subResourceBundlesMapVariable).append(");\n"); - } - sb.append(" final var ").append(subNodeName).append(" = ").append(subViewVariable).append(".load();\n"); - final var id = include.controllerId(); - if (id != null) { - final var subControllerVariable = getNextVariableName("controller"); - sb.append(" final var ").append(subControllerVariable).append(" = ").append(subViewVariable).append(".controller();\n"); - injectControllerField(request, id, subControllerVariable, sb); - } - } - - /** - * Formats a constant object - * - * @param constant The constant - * @param variableName The variable name - * @param sb The string builder - */ - private static void formatConstant(final ParsedConstant constant, final String variableName, final StringBuilder sb) { - sb.append(" final var ").append(variableName).append(" = ").append(constant.className()).append(".").append(constant.constant()).append(";\n"); - } - - /** - * Formats a property - * - * @param request The generation request - * @param property The property to format - * @param parent The property's parent object - * @param parentVariable The parent variable - * @param sb The string builder - */ - private void formatProperty(final GenerationRequest request, final ParsedProperty property, final ParsedObject parent, final String parentVariable, final StringBuilder sb) throws GenerationException { - final var propertyName = property.name(); - final var setMethod = getSetMethod(propertyName); - if (propertyName.equals("fx:id")) { - final var id = property.value(); - injectControllerField(request, id, parentVariable, sb); - } else if (propertyName.equals("fx:controller")) { - if (parent != request.rootObject()) { - throw new GenerationException("Invalid nested controller"); - } - } else if (Objects.equals(property.sourceType(), EventHandler.class.getName())) { - injectControllerMethod(request, property, parentVariable, sb); - } else if (property.sourceType() != null) { - final var propertySourceTypeClass = getClass(property.sourceType()); - if (hasStaticMethod(propertySourceTypeClass, setMethod)) { - final var method = getStaticMethod(propertySourceTypeClass, setMethod); - final var parameterType = method.getParameterTypes()[1]; - final var arg = getArg(request, property.value(), parameterType); - setLaterIfNeeded(request, property, parameterType, " " + property.sourceType() + "." + setMethod + "(" + parentVariable + ", " + arg + ");\n", sb); - } else { - throw new GenerationException("Cannot set " + propertyName + " on " + property.sourceType()); - } - } else { - final var getMethod = getGetMethod(propertyName); - final var parentClass = getClass(parent.className()); - if (hasMethod(parentClass, setMethod)) { - final var method = getMethod(parentClass, setMethod); - final var parameterType = method.getParameterTypes()[0]; - final var arg = getArg(request, property.value(), parameterType); - setLaterIfNeeded(request, property, parameterType, " " + parentVariable + "." + setMethod + "(" + arg + ");\n", sb); - } else if (hasMethod(parentClass, getMethod)) { - final var method = getMethod(parentClass, getMethod); - final var returnType = method.getReturnType(); - if (hasMethod(returnType, "addAll")) { - final var arg = getArg(request, property.value(), String.class); - setLaterIfNeeded(request, property, String.class, " " + parentVariable + "." + getMethod + "().addAll(" + arg + ");\n", sb); - } - } else { - throw new GenerationException("Cannot set " + propertyName + " on " + parent.className()); - } - } - } - - private void setLaterIfNeeded(final GenerationRequest request, final ParsedProperty property, final Class type, final String arg, final StringBuilder sb) throws GenerationException { - if (type == String.class && property.value().startsWith("%") && request.parameters().resourceBundleInjection().injectionType() == ResourceBundleInjectionTypes.GETTER - && getControllerInjection(request).fieldInjectionType() == ControllerFieldInjectionTypes.FACTORY) { - controllerFactoryPostAction.add(arg); - } else { - sb.append(arg); - } - } - - /** - * Injects a controller method - * - * @param request The generation request - * @param property The property to inject - * @param parentVariable The parent variable - * @param sb The string builder - */ - private void injectControllerMethod(final GenerationRequest request, final ParsedProperty property, final String parentVariable, final StringBuilder sb) throws GenerationException { - final var injection = getControllerInjection(request); - final var methodInjection = getMethodInjection(request, property, parentVariable, sb); - if (injection.fieldInjectionType() instanceof final ControllerFieldInjectionTypes fieldTypes) { - switch (fieldTypes) { - case FACTORY -> controllerFactoryPostAction.add(methodInjection); - case ASSIGN, SETTERS, REFLECTION -> sb.append(methodInjection); - } - } else { - throw new GenerationException("Unknown injection type : " + injection.fieldInjectionType()); - } - } - - /** - * Computes the method injection - * - * @param request The generation request - * @param property The property - * @param parentVariable The parent variable - * @param sb The string builder - * @return The method injection - */ - private static String getMethodInjection(final GenerationRequest request, final ParsedProperty property, final String parentVariable, final StringBuilder sb) throws GenerationException { - final var setMethod = getSetMethod(property.name()); - final var injection = getControllerInjection(request); - final var controllerMethod = property.value().replace("#", ""); - if (injection.methodInjectionType() instanceof final ControllerMethodsInjectionType methodTypes) { - return switch (methodTypes) { - case REFERENCE -> { - final var hasArgument = request.controllerInfo().handlerHasArgument(controllerMethod); - if (hasArgument) { - yield " " + parentVariable + "." + setMethod + "(controller::" + controllerMethod + ");\n"; - } else { - yield " " + parentVariable + "." + setMethod + "(e -> controller." + controllerMethod + "());\n"; - } - } - case REFLECTION -> - " " + parentVariable + "." + setMethod + "(e -> callMethod(\"" + controllerMethod + "\", e));\n"; - }; - } else { - throw new GenerationException("Unknown injection type : " + injection.methodInjectionType()); - } - } - - /** - * Formats an argument to a method - * - * @param request The generation request - * @param value The value - * @param parameterType The parameter type - * @return The formatted value - */ - private static String getArg(final GenerationRequest request, final String value, final Class parameterType) throws GenerationException { - if (parameterType == String.class && value.startsWith("%")) { - return getBundleValue(request, value.substring(1)); - } else { - return toString(value, parameterType); - } - } - - /** - * Injects the given variable into the controller - * - * @param request The generation request - * @param id The object id - * @param variable The object variable - * @param sb The string builder - */ - private static void injectControllerField(final GenerationRequest request, final String id, final String variable, final StringBuilder sb) throws GenerationException { - final var controllerInjection = getControllerInjection(request); - final var controllerInjectionType = controllerInjection.fieldInjectionType(); - if (controllerInjectionType instanceof final ControllerFieldInjectionTypes types) { - switch (types) { - case FACTORY -> - sb.append(" fieldMap.put(\"").append(id).append("\", ").append(variable).append(");\n"); - case ASSIGN -> sb.append(" controller.").append(id).append(" = ").append(variable).append(";\n"); - case SETTERS -> { - final var setMethod = getSetMethod(id); - sb.append(" controller.").append(setMethod).append("(").append(variable).append(");\n"); - } - case REFLECTION -> - sb.append(" injectField(\"").append(id).append("\", ").append(variable).append(");\n"); - } - } else { - throw new GenerationException("Unknown controller injection type : " + controllerInjectionType); - } - } - - /** - * Gets the controller injection object from the generation request - * - * @param request The generation request - * @return The controller injection - */ - private static ControllerInjection getControllerInjection(final GenerationRequest request) throws GenerationException { - final var property = request.rootObject().attributes().get("fx:controller"); - if (property == null) { - throw new GenerationException("Root object must have a controller property"); - } else { - final var id = property.value(); - return request.parameters().controllerInjections().get(id); - } - } - - /** - * Formats the children objects of a property - * - * @param request The generation request - * @param parent The parent object - * @param property The parent property - * @param objects The child objects - * @param parentVariable The parent object variable - * @param sb The string builder - */ - private void formatChild(final GenerationRequest request, final ParsedObject parent, final ParsedProperty property, - final Collection objects, final String parentVariable, final StringBuilder sb) throws GenerationException { - final var propertyName = property.name(); - final var variables = new ArrayList(); - for (final var object : objects) { - final var vn = getNextVariableName("object"); - format(request, object, vn, sb); - variables.add(vn); - } - if (variables.size() > 1) { - formatMultipleChildren(variables, propertyName, parent, parentVariable, sb); - } else if (variables.size() == 1) { - final var vn = variables.getFirst(); - formatSingleChild(vn, property, parent, parentVariable, sb); - } - } - - /** - * Formats children objects given that they are more than one - * - * @param variables The children variables - * @param propertyName The property name - * @param parent The parent object - * @param parentVariable The parent object variable - * @param sb The string builder - */ - private static void formatMultipleChildren(final Iterable variables, final String propertyName, final ParsedObject parent, - final String parentVariable, final StringBuilder sb) throws GenerationException { - final var getMethod = getGetMethod(propertyName); - if (hasMethod(getClass(parent.className()), getMethod)) { - sb.append(" ").append(parentVariable).append(".").append(getMethod).append("().addAll(").append(String.join(", ", variables)).append(");\n"); - } else { - throw new GenerationException("Cannot set " + propertyName + " on " + parent.className()); - } - } - - /** - * Formats a single child object - * - * @param variableName The child's variable name - * @param property The parent property - * @param parent The parent object - * @param parentVariable The parent object variable - * @param sb The string builder - */ - private static void formatSingleChild(final String variableName, final ParsedProperty property, final ParsedObject parent, - final String parentVariable, final StringBuilder sb) throws GenerationException { - if (property.sourceType() == null) { - formatSingleChildInstance(variableName, property, parent, parentVariable, sb); - } else { - formatSingleChildStatic(variableName, property, parentVariable, sb); - } - } - - /** - * Formats a single child object using an instance method on the parent object - * - * @param variableName The child's variable name - * @param property The parent property - * @param parent The parent object - * @param parentVariable The parent object variable - * @param sb The string builder - */ - private static void formatSingleChildInstance(final String variableName, final ParsedProperty property, final ParsedObject parent, - final String parentVariable, final StringBuilder sb) throws GenerationException { - final var setMethod = getSetMethod(property); - final var getMethod = getGetMethod(property); - final var parentClass = getClass(parent.className()); - if (hasMethod(parentClass, setMethod)) { - sb.append(" ").append(parentVariable).append(".").append(setMethod).append("(").append(variableName).append(");\n"); - } else if (hasMethod(parentClass, getMethod)) { - //Probably a list method that has only one element - sb.append(" ").append(parentVariable).append(".").append(getMethod).append("().addAll(").append(variableName).append(");\n"); - } else { - throw new GenerationException("Cannot set " + property.name() + " on " + parent.className()); - } - } - - /** - * Formats a child object using a static method - * - * @param variableName The child's variable name - * @param property The parent property - * @param parentVariable The parent variable - * @param sb The string builder - */ - private static void formatSingleChildStatic(final String variableName, final ParsedProperty property, final String parentVariable, final StringBuilder sb) throws GenerationException { - final var setMethod = getSetMethod(property); - if (hasStaticMethod(getClass(property.sourceType()), setMethod)) { - sb.append(" ").append(property.sourceType()).append(".").append(setMethod).append("(").append(parentVariable).append(", ").append(variableName).append(");\n"); - } else { - throw new GenerationException("Cannot set " + property.name() + " on " + property.sourceType()); - } - } - - /** - * Returns the getter method name for the given property - * - * @param property The property - * @return The getter method name - */ - private static String getGetMethod(final ParsedProperty property) { - return getGetMethod(property.name()); - } - - /** - * Returns the getter method name for the given property name - * - * @param propertyName The property name - * @return The getter method name - */ - private static String getGetMethod(final String propertyName) { - return "get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); - } - - /** - * Returns the setter method name for the given property - * - * @param property The property - * @return The setter method name - */ - private static String getSetMethod(final ParsedProperty property) { - return getSetMethod(property.name()); - } - - /** - * Returns the setter method name for the given property name - * - * @param propertyName The property name - * @return The setter method name - */ - private static String getSetMethod(final String propertyName) { - return "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); - } - - - private static String getBundleValue(final GenerationRequest request, final String value) throws GenerationException { - final var resourceBundleInjectionType = request.parameters().resourceBundleInjection().injectionType(); - if (resourceBundleInjectionType instanceof final ResourceBundleInjectionTypes types) { - return switch (types) { - case CONSTRUCTOR, GET_BUNDLE -> "bundle.getString(\"" + value + "\")"; - case GETTER -> "controller.resources().getString(\"" + value + "\")"; - }; - } else { - throw new GenerationException("Unknown resource bundle injection type : " + resourceBundleInjectionType); - } - } - - /** - * Gets the constructor arguments as a list of strings - * - * @param constructorArgs The constructor arguments - * @param parsedObject The parsed object - * @return The list of constructor arguments - */ - private static List getListConstructorArgs(final ConstructorArgs constructorArgs, final ParsedObject parsedObject) throws GenerationException { - final var args = new ArrayList(constructorArgs.namedArgs().size()); - for (final var entry : constructorArgs.namedArgs().entrySet()) { - final var type = entry.getValue().type(); - final var p = parsedObject.attributes().get(entry.getKey()); - if (p == null) { - final var c = parsedObject.properties().entrySet().stream().filter(e -> - e.getKey().name().equals(entry.getKey())).findFirst().orElse(null); - if (c == null) { - args.add(toString(entry.getValue().defaultValue(), type)); - } else { - throw new GenerationException("Constructor using complex property not supported yet"); - } - } else { - args.add(toString(p.value(), type)); - } - } - return args; - } - - /** - * Gets the constructor arguments that best match the given property names - * - * @param constructors The constructors - * @param allPropertyNames The property names - * @return The matching constructor arguments, or null if no constructor matches and no default constructor exists - */ - private static ConstructorArgs getMatchingConstructorArgs(final Constructor[] constructors, final Set allPropertyNames) { - ConstructorArgs matchingConstructorArgs = null; - for (final var constructor : constructors) { - final var constructorArgs = getConstructorArgs(constructor); - final var matchingArgsCount = getMatchingArgsCount(constructorArgs, allPropertyNames); - if (matchingConstructorArgs == null ? matchingArgsCount > 0 : matchingArgsCount > getMatchingArgsCount(matchingConstructorArgs, allPropertyNames)) { - matchingConstructorArgs = constructorArgs; - } - } - if (matchingConstructorArgs == null) { - return Arrays.stream(constructors).filter(c -> c.getParameterCount() == 0).findFirst().map(c -> new ConstructorArgs(c, new LinkedHashMap<>())).orElse(null); - } else { - return matchingConstructorArgs; - } - } - - /** - * Checks how many arguments of the given constructor match the given property names - * - * @param constructorArgs The constructor arguments - * @param allPropertyNames The property names - * @return The number of matching arguments - */ - private static long getMatchingArgsCount(final ConstructorArgs constructorArgs, final Set allPropertyNames) { - return constructorArgs.namedArgs().keySet().stream().filter(allPropertyNames::contains).count(); - } - - - /** - * Computes the string value to use in the generated code - * - * @param value The value - * @param clazz The value class - * @return The computed string value - */ - private static String toString(final String value, final Class clazz) { - if (clazz == String.class) { - return "\"" + value.replace("\"", "\\\"") + "\""; - } else if (clazz == char.class || clazz == Character.class) { - return "'" + value + "'"; - } else if (clazz == boolean.class || clazz == Boolean.class) { - return value; - } else if (clazz == byte.class || clazz == Byte.class || clazz == short.class || clazz == Short.class || - clazz == int.class || clazz == Integer.class || clazz == long.class || clazz == Long.class) { - if (INT_PATTERN.matcher(value).matches()) { - return value; - } else { - return getWrapperClass(clazz) + ".valueOf(\"" + value + "\")"; - } - } else if (clazz == float.class || clazz == Float.class || clazz == double.class || clazz == Double.class) { - if (DECIMAL_PATTERN.matcher(value).matches()) { - return value; - } else { - return getWrapperClass(clazz) + ".valueOf(\"" + value + "\")"; - } - } else if (hasValueOf(clazz)) { - if (clazz.isEnum()) { - return clazz.getCanonicalName() + "." + value; - } else { - return clazz.getCanonicalName() + ".valueOf(\"" + value + "\")"; - } - } else { - return value; - } - } - - private static String getWrapperClass(final Class clazz) { - final var name = clazz.getName(); - if (name.contains(".") || Character.isUpperCase(name.charAt(0))) { - return name; - } else { - return name.substring(0, 1).toUpperCase() + name.substring(1); - } - } - - /** - * Computes the next available variable name for the given prefix - * - * @param prefix The variable name prefix - * @return The next available variable name - */ - private String getNextVariableName(final String prefix) { - final var counter = variableNameCounters.computeIfAbsent(prefix, k -> new AtomicInteger(0)); - return prefix + counter.getAndIncrement(); - } - - private static Class getClass(final String className) throws GenerationException { - try { - return Class.forName(className, false, Thread.currentThread().getContextClassLoader()); - } catch (final ClassNotFoundException e) { - throw new GenerationException("Cannot find class " + className + " ; Is a dependency missing for the plugin?", e); - } - } } diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/ConstructorArgs.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ConstructorArgs.java similarity index 71% rename from core/src/main/java/com/github/gtache/fxml/compiler/impl/ConstructorArgs.java rename to core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ConstructorArgs.java index dc2bdb7..b1fe5f8 100644 --- a/core/src/main/java/com/github/gtache/fxml/compiler/impl/ConstructorArgs.java +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ConstructorArgs.java @@ -1,4 +1,4 @@ -package com.github.gtache.fxml.compiler.impl; +package com.github.gtache.fxml.compiler.impl.internal; import java.lang.reflect.Constructor; import java.util.Collections; @@ -15,6 +15,14 @@ import static java.util.Objects.requireNonNull; */ record ConstructorArgs(Constructor constructor, SequencedMap namedArgs) { + + /** + * Instantiates new args + * + * @param constructor The constructor + * @param namedArgs The named args + * @throws NullPointerException if any argument is null + */ ConstructorArgs { requireNonNull(constructor); namedArgs = Collections.unmodifiableSequencedMap(new LinkedHashMap<>(namedArgs)); diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ConstructorHelper.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ConstructorHelper.java new file mode 100644 index 0000000..e13e5e9 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ConstructorHelper.java @@ -0,0 +1,84 @@ +package com.github.gtache.fxml.compiler.impl.internal; + +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.impl.GeneratorImpl; +import com.github.gtache.fxml.compiler.parsing.ParsedObject; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; + +import static com.github.gtache.fxml.compiler.impl.internal.ReflectionHelper.getConstructorArgs; + +/** + * Helper methods for {@link GeneratorImpl} to handle constructors + */ +final class ConstructorHelper { + private ConstructorHelper() { + } + + /** + * Gets the constructor arguments as a list of strings + * + * @param constructorArgs The constructor arguments + * @param parsedObject The parsed object + * @return The list of constructor arguments + * @throws GenerationException if an error occurs + */ + static List getListConstructorArgs(final ConstructorArgs constructorArgs, final ParsedObject parsedObject) throws GenerationException { + final var args = new ArrayList(constructorArgs.namedArgs().size()); + for (final var entry : constructorArgs.namedArgs().entrySet()) { + final var type = entry.getValue().type(); + final var p = parsedObject.attributes().get(entry.getKey()); + if (p == null) { + final var c = parsedObject.properties().entrySet().stream().filter(e -> + e.getKey().name().equals(entry.getKey())).findFirst().orElse(null); + if (c == null) { + args.add(ValueFormatter.toString(entry.getValue().defaultValue(), type)); + } else { + throw new GenerationException("Constructor using complex property not supported yet"); + } + } else { + args.add(ValueFormatter.toString(p.value(), type)); + } + } + return args; + } + + /** + * Gets the constructor arguments that best match the given property names + * + * @param constructors The constructors + * @param allPropertyNames The property names + * @return The matching constructor arguments, or null if no constructor matches and no default constructor exists + */ + static ConstructorArgs getMatchingConstructorArgs(final Constructor[] constructors, final Set allPropertyNames) { + ConstructorArgs matchingConstructorArgs = null; + for (final var constructor : constructors) { + final var constructorArgs = getConstructorArgs(constructor); + final var matchingArgsCount = getMatchingArgsCount(constructorArgs, allPropertyNames); + if (matchingConstructorArgs == null ? matchingArgsCount > 0 : matchingArgsCount > getMatchingArgsCount(matchingConstructorArgs, allPropertyNames)) { + matchingConstructorArgs = constructorArgs; + } + } + if (matchingConstructorArgs == null) { + return Arrays.stream(constructors).filter(c -> c.getParameterCount() == 0).findFirst().map(c -> new ConstructorArgs(c, new LinkedHashMap<>())).orElse(null); + } else { + return matchingConstructorArgs; + } + } + + /** + * Checks how many arguments of the given constructor match the given property names + * + * @param constructorArgs The constructor arguments + * @param allPropertyNames The property names + * @return The number of matching arguments + */ + private static long getMatchingArgsCount(final ConstructorArgs constructorArgs, final Set allPropertyNames) { + return constructorArgs.namedArgs().keySet().stream().filter(allPropertyNames::contains).count(); + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ControllerInjector.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ControllerInjector.java new file mode 100644 index 0000000..cca0008 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ControllerInjector.java @@ -0,0 +1,154 @@ +package com.github.gtache.fxml.compiler.impl.internal; + +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.InjectionType; +import com.github.gtache.fxml.compiler.impl.ControllerFieldInjectionTypes; +import com.github.gtache.fxml.compiler.impl.ControllerMethodsInjectionType; +import com.github.gtache.fxml.compiler.impl.GeneratorImpl; +import com.github.gtache.fxml.compiler.parsing.ParsedProperty; + +import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.getControllerInjection; +import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.getSetMethod; + +/** + * Various methods to help {@link GeneratorImpl} for injecting controllers + */ +final class ControllerInjector { + private ControllerInjector() { + + } + + /** + * Injects the given variable into the controller + * + * @param progress The generation progress + * @param id The object id + * @param variable The object variable + * @throws GenerationException if an error occurs + */ + static void injectControllerField(final GenerationProgress progress, final String id, final String variable) throws GenerationException { + final var controllerInjection = getControllerInjection(progress); + final var controllerInjectionType = controllerInjection.fieldInjectionType(); + if (controllerInjectionType instanceof final ControllerFieldInjectionTypes types) { + final var sb = progress.stringBuilder(); + switch (types) { + case FACTORY -> + sb.append(" fieldMap.put(\"").append(id).append("\", ").append(variable).append(");\n"); + case ASSIGN -> sb.append(" controller.").append(id).append(" = ").append(variable).append(";\n"); + case SETTERS -> { + final var setMethod = getSetMethod(id); + sb.append(" controller.").append(setMethod).append("(").append(variable).append(");\n"); + } + case REFLECTION -> + sb.append(" injectField(\"").append(id).append("\", ").append(variable).append(");\n"); + } + } else { + throw new GenerationException("Unknown controller injection type : " + controllerInjectionType); + } + } + + /** + * Injects an event handler controller method + * + * @param progress The generation progress + * @param property The property to inject + * @param parentVariable The parent variable + * @throws GenerationException if an error occurs + */ + static void injectEventHandlerControllerMethod(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) throws GenerationException { + injectControllerMethod(progress, getEventHandlerMethodInjection(progress, property, parentVariable)); + } + + /** + * Injects a callback controller method + * + * @param progress The generation progress + * @param property The property to inject + * @param parentVariable The parent variable + * @param argumentClazz The argument class + * @throws GenerationException if an error occurs + */ + static void injectCallbackControllerMethod(final GenerationProgress progress, final ParsedProperty property, final String parentVariable, final String argumentClazz) throws GenerationException { + injectControllerMethod(progress, getCallbackMethodInjection(progress, property, parentVariable, argumentClazz)); + } + + /** + * Injects a controller method + * + * @param progress The generation progress + * @param methodInjection The method injection + * @throws GenerationException if an error occurs + */ + private static void injectControllerMethod(final GenerationProgress progress, final String methodInjection) throws GenerationException { + final var injection = getControllerInjection(progress); + if (injection.fieldInjectionType() instanceof final ControllerFieldInjectionTypes fieldTypes) { + switch (fieldTypes) { + case FACTORY -> progress.controllerFactoryPostAction().add(methodInjection); + case ASSIGN, SETTERS, REFLECTION -> progress.stringBuilder().append(methodInjection); + } + } else { + throw getUnknownInjectionException(injection.fieldInjectionType()); + } + } + + /** + * Computes the method injection for event handler + * + * @param progress The generation progress + * @param property The property + * @param parentVariable The parent variable + * @return The method injection + * @throws GenerationException if an error occurs + */ + private static String getEventHandlerMethodInjection(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) throws GenerationException { + final var setMethod = getSetMethod(property.name()); + final var injection = getControllerInjection(progress); + final var controllerMethod = property.value().replace("#", ""); + if (injection.methodInjectionType() instanceof final ControllerMethodsInjectionType methodTypes) { + return switch (methodTypes) { + case REFERENCE -> { + final var hasArgument = progress.request().controllerInfo().handlerHasArgument(controllerMethod); + if (hasArgument) { + yield " " + parentVariable + "." + setMethod + "(controller::" + controllerMethod + ");\n"; + } else { + yield " " + parentVariable + "." + setMethod + "(e -> controller." + controllerMethod + "());\n"; + } + } + case REFLECTION -> + " " + parentVariable + "." + setMethod + "(e -> callEventHandlerMethod(\"" + controllerMethod + "\", e));\n"; + }; + } else { + throw getUnknownInjectionException(injection.methodInjectionType()); + } + } + + /** + * Computes the method injection for callback + * + * @param progress The generation progress + * @param property The property + * @param parentVariable The parent variable + * @param argumentClazz The argument class + * @return The method injection + * @throws GenerationException if an error occurs + */ + private static String getCallbackMethodInjection(final GenerationProgress progress, final ParsedProperty property, final String parentVariable, final String argumentClazz) throws GenerationException { + final var setMethod = getSetMethod(property.name()); + final var injection = getControllerInjection(progress); + final var controllerMethod = property.value().replace("#", ""); + if (injection.methodInjectionType() instanceof final ControllerMethodsInjectionType methodTypes) { + return switch (methodTypes) { + case REFERENCE -> + " " + parentVariable + "." + setMethod + "(controller::" + controllerMethod + ");\n"; + case REFLECTION -> + " " + parentVariable + "." + setMethod + "(e -> callCallbackMethod(\"" + controllerMethod + "\", e, " + argumentClazz + "));\n"; + }; + } else { + throw getUnknownInjectionException(injection.methodInjectionType()); + } + } + + private static GenerationException getUnknownInjectionException(final InjectionType type) { + return new GenerationException("Unknown injection type : " + type); + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/FieldSetter.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/FieldSetter.java new file mode 100644 index 0000000..018d61c --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/FieldSetter.java @@ -0,0 +1,94 @@ +package com.github.gtache.fxml.compiler.impl.internal; + +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.impl.ControllerFieldInjectionTypes; +import com.github.gtache.fxml.compiler.impl.GeneratorImpl; +import com.github.gtache.fxml.compiler.parsing.ParsedProperty; + +import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*; + +/** + * Helper methods for {@link GeneratorImpl} to set fields + */ +final class FieldSetter { + + private FieldSetter() { + + } + + /** + * Sets an event handler field + * + * @param progress The generation progress + * @param property The property to inject + * @param parentVariable The parent variable + * @throws GenerationException if an error occurs@ + */ + static void setEventHandler(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) throws GenerationException { + setField(progress, property, parentVariable, "javafx.event.EventHandler"); + } + + + /** + * Sets a field + * + * @param progress The generation progress + * @param property The property to inject + * @param parentVariable The parent variable + * @param fieldType The field type + * @throws GenerationException if an error occurs + */ + static void setField(final GenerationProgress progress, final ParsedProperty property, final String parentVariable, final String fieldType) throws GenerationException { + final var injection = getControllerInjection(progress); + if (injection.fieldInjectionType() instanceof final ControllerFieldInjectionTypes fieldTypes) { + switch (fieldTypes) { + case ASSIGN -> setAssign(progress, property, parentVariable); + case FACTORY -> setFactory(progress, property, parentVariable); + case SETTERS -> setSetter(progress, property, parentVariable); + case REFLECTION -> setReflection(progress, property, parentVariable, fieldType); + } + } else { + throw new GenerationException("Unknown injection type : " + injection.fieldInjectionType()); + } + } + + private static void setAssign(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) { + final var methodName = getSetMethod(property); + final var value = property.value().replace("$", ""); + progress.stringBuilder().append(" ").append(parentVariable).append(".").append(methodName).append("(").append(value).append(");\n"); + } + + private static void setFactory(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) { + progress.controllerFactoryPostAction().add(getSetString(property, parentVariable)); + } + + private static void setSetter(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) { + progress.stringBuilder().append(getSetString(property, parentVariable)); + } + + private static String getSetString(final ParsedProperty property, final String parentVariable) { + final var methodName = getSetMethod(property); + final var value = property.value().replace("$", ""); + final var split = value.split("\\."); + final var getterName = getGetMethod(split[1]); + return " " + parentVariable + "." + methodName + "(" + split[0] + "." + getterName + ");\n"; + } + + private static void setReflection(final GenerationProgress progress, final ParsedProperty property, final String parentVariable, final String fieldType) { + final var methodName = getSetMethod(property); + final var value = property.value().replace("$", ""); + final var split = value.split("\\."); + final var fieldName = split[1]; + progress.stringBuilder().append(""" + try { + final var field = controller.getClass().getDeclaredField("%s"); + field.setAccessible(true); + final var value = (%s) field.get(controller); + %s.%s(value); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + """.formatted(fieldName, fieldType, parentVariable, methodName)); + } + +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/FontFormatter.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/FontFormatter.java new file mode 100644 index 0000000..7a617ae --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/FontFormatter.java @@ -0,0 +1,138 @@ +package com.github.gtache.fxml.compiler.impl.internal; + +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.impl.GeneratorImpl; +import com.github.gtache.fxml.compiler.parsing.ParsedObject; +import com.github.gtache.fxml.compiler.parsing.ParsedProperty; +import javafx.scene.text.FontPosture; +import javafx.scene.text.FontWeight; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*; + +/** + * Helper methods for {@link GeneratorImpl} to format fonts + */ +final class FontFormatter { + + private FontFormatter() { + + } + + static void formatFont(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException { + if (parsedObject.children().isEmpty() && parsedObject.properties().isEmpty()) { + final var value = parseFontValue(parsedObject); + final var url = value.url(); + final var fw = value.fontWeight(); + final var fp = value.fontPosture(); + final var size = value.size(); + final var name = value.name(); + if (url != null) { + formatURL(progress, url, size, variableName); + } else if (fw == null && fp == null) { + formatNoStyle(progress, name, size, variableName); + } else { + formatStyle(progress, fw, fp, size, name, variableName); + } + handleId(progress, parsedObject, variableName); + } else { + throw new GenerationException("Font cannot have children or properties : " + parsedObject); + } + } + + private static void formatURL(final GenerationProgress progress, final URL url, final double size, final String variableName) { + final var urlVariableName = URLBuilder.formatURL(progress, url.toString()); + progress.stringBuilder().append(""" + Font %1$s; + try (final var in = %2$s.openStream()) { + %1$s = Font.loadFont(in, %3$s); + } catch (final IOException e) { + throw new RuntimeException(e); + } + """.formatted(variableName, urlVariableName, size)); + } + + private static void formatNoStyle(final GenerationProgress progress, final String name, final double size, final String variableName) { + progress.stringBuilder().append(START_VAR).append(variableName).append(" = new javafx.scene.text.Font(\"").append(name).append("\", ").append(size).append(");\n"); + } + + private static void formatStyle(final GenerationProgress progress, final FontWeight fw, final FontPosture fp, final double size, final String name, final String variableName) { + final var finalFW = fw == null ? FontWeight.NORMAL : fw; + final var finalFP = fp == null ? FontPosture.REGULAR : fp; + progress.stringBuilder().append(START_VAR).append(variableName).append(" = new javafx.scene.text.Font(\"").append(name) + .append("\", javafx.scene.text.FontWeight.").append(finalFW.name()).append(", javafx.scene.text.FontPosture.") + .append(finalFP.name()).append(", ").append(size).append(");\n"); + } + + private static FontValue parseFontValue(final ParsedObject parsedObject) throws GenerationException { + URL url = null; + String name = null; + double size = 12; + FontWeight fw = null; + FontPosture fp = null; + final var sortedAttributes = getSortedAttributes(parsedObject); + for (final var property : sortedAttributes) { + switch (property.name()) { + case FX_ID -> { + //Do nothing + } + case "name" -> { + try { + url = new URI(property.value()).toURL(); + } catch (final MalformedURLException | URISyntaxException | IllegalArgumentException ignored) { + name = property.value(); + } + } + case "size" -> size = Double.parseDouble(property.value()); + case "style" -> { + final var style = getFontStyle(property); + if (style.fontWeight() != null) { + fw = style.fontWeight(); + } + if (style.fontPosture() != null) { + fp = style.fontPosture(); + } + } + case "url" -> url = getURL(property); + default -> throw new GenerationException("Unknown font attribute : " + property.name()); + } + } + return new FontValue(url, name, size, fw, fp); + } + + private static URL getURL(final ParsedProperty property) throws GenerationException { + try { + return new URI(property.value()).toURL(); + } catch (final MalformedURLException | URISyntaxException e) { + throw new GenerationException("Couldn't parse url : " + property.value(), e); + } + } + + private static FontStyle getFontStyle(final ParsedProperty property) { + final var split = property.value().split(" "); + FontWeight fw = null; + FontPosture fp = null; + for (final var s : split) { + final var fontWeight = FontWeight.findByName(s); + final var fontPosture = FontPosture.findByName(s); + if (fontWeight != null) { + fw = fontWeight; + } else if (fontPosture != null) { + fp = fontPosture; + } + } + return new FontStyle(fw, fp); + } + + private record FontValue(URL url, String name, double size, FontWeight fontWeight, + FontPosture fontPosture) { + } + + private record FontStyle(FontWeight fontWeight, FontPosture fontPosture) { + } + +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/GenerationHelper.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/GenerationHelper.java new file mode 100644 index 0000000..7648c8b --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/GenerationHelper.java @@ -0,0 +1,124 @@ +package com.github.gtache.fxml.compiler.impl.internal; + +import com.github.gtache.fxml.compiler.ControllerInjection; +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.impl.GeneratorImpl; +import com.github.gtache.fxml.compiler.parsing.ParsedObject; +import com.github.gtache.fxml.compiler.parsing.ParsedProperty; + +import java.util.List; +import java.util.Map; + +import static com.github.gtache.fxml.compiler.impl.internal.ControllerInjector.injectControllerField; + +/** + * Various helper methods for {@link GeneratorImpl} + */ +public final class GenerationHelper { + + static final String FX_ID = "fx:id"; + static final String FX_VALUE = "fx:value"; + static final String VALUE = "value"; + static final String START_VAR = " final var "; + + private GenerationHelper() { + + } + + /** + * Returns the variable prefix for the given object + * + * @param object The object + * @return The variable prefix + */ + public static String getVariablePrefix(final ParsedObject object) { + final var className = object.className(); + return className.substring(className.lastIndexOf('.') + 1).toLowerCase(); + } + + /** + * Gets the controller injection object from the generation request + * + * @param progress The generation progress + * @return The controller injection + * @throws GenerationException If the controller is not found + */ + public static ControllerInjection getControllerInjection(final GenerationProgress progress) throws GenerationException { + final var request = progress.request(); + final var property = request.rootObject().attributes().get("fx:controller"); + if (property == null) { + throw new GenerationException("Root object must have a controller property"); + } else { + final var id = property.value(); + return request.parameters().controllerInjections().get(id); + } + } + + /** + * Returns the getter method name for the given property + * + * @param property The property + * @return The getter method name + */ + static String getGetMethod(final ParsedProperty property) { + return getGetMethod(property.name()); + } + + /** + * Returns the getter method name for the given property name + * + * @param propertyName The property name + * @return The getter method name + */ + static String getGetMethod(final String propertyName) { + return "get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + } + + /** + * Returns the setter method name for the given property + * + * @param property The property + * @return The setter method name + */ + static String getSetMethod(final ParsedProperty property) { + return getSetMethod(property.name()); + } + + /** + * Returns the setter method name for the given property name + * + * @param propertyName The property name + * @return The setter method name + */ + static String getSetMethod(final String propertyName) { + return "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + } + + /** + * Handles the fx:id attribute of an object + * + * @param progress The generation progress + * @param parsedObject The parsed object + * @param variableName The variable name + * @throws GenerationException if an error occurs + */ + static void handleId(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException { + final var id = parsedObject.attributes().get(FX_ID); + if (id != null) { + progress.idToVariableName().put(id.value(), variableName); + progress.idToObject().put(id.value(), parsedObject); + //TODO Don't inject if variable doesn't exist + injectControllerField(progress, id.value(), variableName); + } + } + + /** + * Returns the sorted attributes of the given object + * + * @param parsedObject The parsed object + * @return The sorted attributes + */ + static List getSortedAttributes(final ParsedObject parsedObject) { + return parsedObject.attributes().entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue).toList(); + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/GenerationProgress.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/GenerationProgress.java new file mode 100644 index 0000000..8fea449 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/GenerationProgress.java @@ -0,0 +1,70 @@ +package com.github.gtache.fxml.compiler.impl.internal; + +import com.github.gtache.fxml.compiler.GenerationRequest; +import com.github.gtache.fxml.compiler.impl.GeneratorImpl; +import com.github.gtache.fxml.compiler.parsing.ParsedObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.SequencedCollection; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Used by {@link GeneratorImpl} to track the generation progress + * + * @param request The generation request + * @param idToVariableName The id to variable name mapping + * @param idToObject The id to parsed object mapping + * @param variableNameCounters The variable name counters for variable name generation + * @param controllerFactoryPostAction The controller factory post action for factory injection + * @param stringBuilder The string builder + */ +public record GenerationProgress(GenerationRequest request, Map idToVariableName, + Map idToObject, + Map variableNameCounters, + SequencedCollection controllerFactoryPostAction, + StringBuilder stringBuilder) { + + /** + * Instantiates a new GenerationProgress + * + * @param request The generation request + * @param idToVariableName The id to variable name mapping + * @param idToObject The id to parsed object mapping + * @param variableNameCounters The variable name counters + * @param controllerFactoryPostAction The controller factory post action + * @param stringBuilder The string builder + * @throws NullPointerException if any parameter is null + */ + public GenerationProgress { + Objects.requireNonNull(request); + Objects.requireNonNull(idToVariableName); + Objects.requireNonNull(idToObject); + Objects.requireNonNull(variableNameCounters); + Objects.requireNonNull(controllerFactoryPostAction); + Objects.requireNonNull(stringBuilder); + } + + /** + * Instantiates a new GenerationProgress + * + * @param request The generation request + * @throws NullPointerException if request is null + */ + public GenerationProgress(final GenerationRequest request) { + this(request, new HashMap<>(), new HashMap<>(), new HashMap<>(), new ArrayList<>(), new StringBuilder()); + } + + /** + * Gets the next available variable name for the given prefix + * + * @param prefix The variable name prefix + * @return The next available variable name + */ + public String getNextVariableName(final String prefix) { + final var counter = variableNameCounters.computeIfAbsent(prefix, k -> new AtomicInteger(0)); + return prefix + counter.getAndIncrement(); + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/HelperMethodsProvider.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/HelperMethodsProvider.java new file mode 100644 index 0000000..4175e47 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/HelperMethodsProvider.java @@ -0,0 +1,106 @@ +package com.github.gtache.fxml.compiler.impl.internal; + +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.impl.ControllerFieldInjectionTypes; +import com.github.gtache.fxml.compiler.impl.ControllerMethodsInjectionType; + +import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.getControllerInjection; + +/** + * Provides the helper methods for the generated code + */ +public final class HelperMethodsProvider { + + private HelperMethodsProvider() { + + } + + /** + * Gets helper methods string for the given generation progress + * + * @param progress The generation progress + * @return The helper methods + * @throws GenerationException if an error occurs + */ + public static String getHelperMethods(final GenerationProgress progress) throws GenerationException { + final var injection = getControllerInjection(progress); + final var methodInjectionType = injection.methodInjectionType(); + final var sb = new StringBuilder(); + if (methodInjectionType == ControllerMethodsInjectionType.REFLECTION) { + sb.append(""" + private void callEventHandlerMethod(final String methodName, final T event) { + try { + final java.lang.reflect.Method method; + final var methods = java.util.Arrays.stream(controller.getClass().getDeclaredMethods()) + .filter(m -> m.getName().equals(methodName)).toList(); + if (methods.size() > 1) { + final var eventMethods = methods.stream().filter(m -> + m.getParameterCount() == 1 && javafx.event.Event.class.isAssignableFrom(m.getParameterTypes()[0])).toList(); + if (eventMethods.size() == 1) { + method = eventMethods.getFirst(); + } else { + final var emptyMethods = methods.stream().filter(m -> m.getParameterCount() == 0).toList(); + if (emptyMethods.size() == 1) { + method = emptyMethods.getFirst(); + } else { + throw new IllegalArgumentException("Multiple matching methods for " + methodName); + } + } + } else if (methods.size() == 1) { + method = methods.getFirst(); + } else { + throw new IllegalArgumentException("No matching method for " + methodName); + } + method.setAccessible(true); + if (method.getParameterCount() == 0) { + method.invoke(controller); + } else { + method.invoke(controller, event); + } + } catch (final IllegalAccessException | java.lang.reflect.InvocationTargetException ex) { + throw new RuntimeException("Error using reflection on " + methodName, ex); + } + } + + private U callCallbackMethod(final String methodName, final T value, final Class clazz) { + try { + final java.lang.reflect.Method method; + final var methods = java.util.Arrays.stream(controller.getClass().getDeclaredMethods()) + .filter(m -> m.getName().equals(methodName)).toList(); + if (methods.size() > 1) { + final var eventMethods = methods.stream().filter(m -> + m.getParameterCount() == 1 && clazz.isAssignableFrom(m.getParameterTypes()[0])).toList(); + if (eventMethods.size() == 1) { + method = eventMethods.getFirst(); + } else { + throw new IllegalArgumentException("Multiple matching methods for " + methodName); + } + } else if (methods.size() == 1) { + method = methods.getFirst(); + } else { + throw new IllegalArgumentException("No matching method for " + methodName); + } + method.setAccessible(true); + return (U) method.invoke(controller, value); + } catch (final IllegalAccessException | java.lang.reflect.InvocationTargetException ex) { + throw new RuntimeException("Error using reflection on " + methodName, ex); + } + } + """); + } + if (injection.fieldInjectionType() == ControllerFieldInjectionTypes.REFLECTION) { + sb.append(""" + private void injectField(final String fieldName, final T object) { + try { + final var field = controller.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(controller, object); + } catch (final NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException("Error using reflection on " + fieldName, e); + } + } + """); + } + return sb.toString(); + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ImageBuilder.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ImageBuilder.java new file mode 100644 index 0000000..4984251 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ImageBuilder.java @@ -0,0 +1,52 @@ +package com.github.gtache.fxml.compiler.impl.internal; + +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.impl.GeneratorImpl; +import com.github.gtache.fxml.compiler.parsing.ParsedObject; + +import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*; +import static com.github.gtache.fxml.compiler.impl.internal.URLBuilder.formatURL; + +/** + * Helper methods for {@link GeneratorImpl} to format Images + */ +final class ImageBuilder { + + private ImageBuilder() { + + } + + static void formatImage(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException { + if (parsedObject.children().isEmpty() && parsedObject.properties().isEmpty()) { + final var sortedAttributes = getSortedAttributes(parsedObject); + String url = null; + var requestedWidth = 0.0; + var requestedHeight = 0.0; + var preserveRatio = false; + var smooth = false; + var backgroundLoading = false; + for (final var property : sortedAttributes) { + switch (property.name()) { + case FX_ID -> { + //Do nothing + } + case "url" -> url = formatURL(progress, property.value()); + case "requestedWidth" -> requestedWidth = Double.parseDouble(property.value()); + case "requestedHeight" -> requestedHeight = Double.parseDouble(property.value()); + case "preserveRatio" -> preserveRatio = Boolean.parseBoolean(property.value()); + case "smooth" -> smooth = Boolean.parseBoolean(property.value()); + case "backgroundLoading" -> backgroundLoading = Boolean.parseBoolean(property.value()); + default -> throw new GenerationException("Unknown image attribute : " + property.name()); + } + } + final var urlString = progress.getNextVariableName("urlStr"); + progress.stringBuilder().append(START_VAR).append(urlString).append(" = ").append(url).append(".toString();\n"); + progress.stringBuilder().append(START_VAR).append(variableName).append(" = new javafx.scene.image.Image(").append(urlString) + .append(", ").append(requestedWidth).append(", ").append(requestedHeight).append(", ") + .append(preserveRatio).append(", ").append(smooth).append(", ").append(backgroundLoading).append(");\n"); + handleId(progress, parsedObject, variableName); + } else { + throw new GenerationException("Image cannot have children or properties : " + parsedObject); + } + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ObjectFormatter.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ObjectFormatter.java new file mode 100644 index 0000000..6f5ec7b --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ObjectFormatter.java @@ -0,0 +1,511 @@ +package com.github.gtache.fxml.compiler.impl.internal; + +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.impl.GeneratorImpl; +import com.github.gtache.fxml.compiler.parsing.*; +import com.github.gtache.fxml.compiler.parsing.impl.ParsedPropertyImpl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.SequencedCollection; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.github.gtache.fxml.compiler.impl.internal.ConstructorHelper.getListConstructorArgs; +import static com.github.gtache.fxml.compiler.impl.internal.ConstructorHelper.getMatchingConstructorArgs; +import static com.github.gtache.fxml.compiler.impl.internal.ControllerInjector.injectControllerField; +import static com.github.gtache.fxml.compiler.impl.internal.FontFormatter.formatFont; +import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*; +import static com.github.gtache.fxml.compiler.impl.internal.ImageBuilder.formatImage; +import static com.github.gtache.fxml.compiler.impl.internal.PropertyFormatter.formatProperty; +import static com.github.gtache.fxml.compiler.impl.internal.ReflectionHelper.*; +import static com.github.gtache.fxml.compiler.impl.internal.SceneBuilder.formatScene; +import static com.github.gtache.fxml.compiler.impl.internal.TriangleMeshBuilder.formatTriangleMesh; +import static com.github.gtache.fxml.compiler.impl.internal.URLBuilder.formatURL; +import static com.github.gtache.fxml.compiler.impl.internal.WebViewBuilder.formatWebView; + +/** + * Helper methods for {@link GeneratorImpl} to format properties + */ +public final class ObjectFormatter { + + private static final String NEW_ASSIGN = " = new "; + + private static final Set BUILDER_CLASSES = Set.of( + "javafx.scene.Scene", + "javafx.scene.text.Font", + "javafx.scene.image.Image", + "java.net.URL", + "javafx.scene.shape.TriangleMesh", + "javafx.scene.web.WebView" + ); + + private static final Set SIMPLE_CLASSES = Set.of( + "java.lang.String", + "java.lang.Integer", + "java.lang.Byte", + "java.lang.Short", + "java.lang.Long", + "java.lang.Float", + "java.lang.Double" + ); + + private ObjectFormatter() { + + } + + /** + * Formats an object + * + * @param progress The generation progress + * @param parsedObject The parsed object to format + * @param variableName The variable name for the object + * @throws GenerationException if an error occurs + */ + public static void format(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException { + switch (parsedObject) { + case final ParsedConstant constant -> formatConstant(progress, constant, variableName); + case final ParsedCopy copy -> formatCopy(progress, copy, variableName); + case final ParsedDefine define -> formatDefine(progress, define, variableName); + case final ParsedFactory factory -> formatFactory(progress, factory, variableName); + case final ParsedInclude include -> formatInclude(progress, include, variableName); + case final ParsedReference reference -> formatReference(progress, reference, variableName); + case final ParsedValue value -> formatValue(progress, value, variableName); + case final ParsedText text -> formatText(progress, text, variableName); + default -> formatObject(progress, parsedObject, variableName); + } + } + + /** + * Formats a simple text + * + * @param progress The generation progress + * @param text The parsed text + * @param variableName The variable name + */ + private static void formatText(final GenerationProgress progress, final ParsedText text, final String variableName) { + progress.stringBuilder().append(START_VAR).append(variableName).append(" = \"").append(text.text()).append("\";\n"); + } + + /** + * Formats a basic object + * + * @param progress The generation progress + * @param parsedObject The parsed object to format + * @param variableName The variable name for the object + */ + private static void formatObject(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException { + if (BUILDER_CLASSES.contains(parsedObject.className())) { + formatBuilderObject(progress, parsedObject, variableName); + } else { + formatNotBuilder(progress, parsedObject, variableName); + } + } + + /** + * Formats a builder object + * + * @param progress The generation progress + * @param parsedObject The parsed object + * @param variableName The variable name + */ + private static void formatBuilderObject(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException { + final var className = parsedObject.className(); + switch (className) { + case "javafx.scene.Scene" -> formatScene(progress, parsedObject, variableName); + case "javafx.scene.text.Font" -> formatFont(progress, parsedObject, variableName); + case "javafx.scene.image.Image" -> formatImage(progress, parsedObject, variableName); + case "java.net.URL" -> formatURL(progress, parsedObject, variableName); + case "javafx.scene.shape.TriangleMesh" -> formatTriangleMesh(progress, parsedObject, variableName); + case "javafx.scene.web.WebView" -> formatWebView(progress, parsedObject, variableName); + default -> throw new IllegalArgumentException("Unknown builder class : " + className); + } + } + + private static void formatNotBuilder(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException { + if (isSimpleClass(parsedObject)) { + formatSimpleClass(progress, parsedObject, variableName); + } else { + formatComplexClass(progress, parsedObject, variableName); + } + } + + private static void formatSimpleClass(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException { + if (!parsedObject.properties().isEmpty()) { + throw new GenerationException("Simple class cannot have properties : " + parsedObject); + } + if (parsedObject.attributes().keySet().stream().anyMatch(k -> !k.equals(FX_ID) && !k.equals(VALUE) && !k.equals(FX_VALUE))) { + throw new GenerationException("Invalid attributes for simple class : " + parsedObject); + } + final var value = getSimpleValue(progress, parsedObject); + final var valueStr = ValueFormatter.toString(value, ReflectionHelper.getClass(parsedObject.className())); + progress.stringBuilder().append(START_VAR).append(variableName).append(" = ").append(valueStr).append(";\n"); + handleId(progress, parsedObject, variableName); + } + + private static String getSimpleValue(final GenerationProgress progress, final ParsedObject parsedObject) throws GenerationException { + final var definedChildren = parsedObject.children().stream().filter(ParsedDefine.class::isInstance).toList(); + for (final var definedChild : definedChildren) { + formatObject(progress, definedChild, progress.getNextVariableName("define")); + } + final var notDefinedChildren = parsedObject.children().stream().filter(c -> !(c instanceof ParsedDefine)).toList(); + if (parsedObject.attributes().containsKey(FX_VALUE)) { + return getSimpleFXValue(parsedObject, notDefinedChildren); + } else if (parsedObject.attributes().containsKey(VALUE)) { + return getSimpleValue(parsedObject, notDefinedChildren); + } else { + return getSimpleChild(parsedObject, notDefinedChildren); + } + } + + private static String getSimpleFXValue(final ParsedObject parsedObject, final Collection notDefinedChildren) throws GenerationException { + if (notDefinedChildren.isEmpty() && !parsedObject.attributes().containsKey(VALUE)) { + return parsedObject.attributes().get(FX_VALUE).value(); + } else { + throw new GenerationException("Malformed simple class : " + parsedObject); + } + } + + private static String getSimpleValue(final ParsedObject parsedObject, final Collection notDefinedChildren) throws GenerationException { + if (notDefinedChildren.isEmpty()) { + return parsedObject.attributes().get(VALUE).value(); + } else { + throw new GenerationException("Malformed simple class : " + parsedObject); + } + } + + private static String getSimpleChild(final ParsedObject parsedObject, final SequencedCollection notDefinedChildren) throws GenerationException { + if (notDefinedChildren.size() == 1) { + final var child = notDefinedChildren.getFirst(); + if (child instanceof final ParsedText text) { + return text.text(); + } else { + throw new GenerationException("Invalid value for : " + parsedObject); + } + } else { + throw new GenerationException("Value not found for : " + parsedObject); + } + } + + private static boolean isSimpleClass(final ParsedObject object) throws GenerationException { + final var className = object.className(); + if (SIMPLE_CLASSES.contains(className)) { + return true; + } else { + final var clazz = ReflectionHelper.getClass(className); + return clazz.isEnum(); + } + } + + private static void formatComplexClass(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException { + final var clazz = ReflectionHelper.getClass(parsedObject.className()); + final var children = parsedObject.children(); + final var definedChildren = children.stream().filter(ParsedDefine.class::isInstance).toList(); + final var notDefinedChildren = children.stream().filter(c -> !(c instanceof ParsedDefine)).toList(); + final var constructors = clazz.getConstructors(); + final var allPropertyNames = new HashSet<>(parsedObject.attributes().keySet()); + allPropertyNames.addAll(parsedObject.properties().keySet().stream().map(ParsedProperty::name).collect(Collectors.toSet())); + if (!definedChildren.isEmpty()) { + for (final var definedChild : definedChildren) { + format(progress, definedChild, progress.getNextVariableName("define")); + } + } + if (!notDefinedChildren.isEmpty()) { + final var defaultProperty = getDefaultProperty(parsedObject.className()); + if (defaultProperty != null) { + allPropertyNames.add(defaultProperty); + } + } + final var constructorArgs = getMatchingConstructorArgs(constructors, allPropertyNames); + if (constructorArgs == null) { + formatNoConstructor(progress, parsedObject, variableName, allPropertyNames); + } else { + formatConstructor(progress, parsedObject, variableName, constructorArgs); + } + } + + private static void formatNoConstructor(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName, final Collection allPropertyNames) throws GenerationException { + final var clazz = ReflectionHelper.getClass(parsedObject.className()); + if (allPropertyNames.size() == 1 && allPropertyNames.iterator().next().equals("fx:constant")) { + final var property = parsedObject.attributes().get("fx:constant"); + progress.stringBuilder().append(START_VAR).append(variableName).append(" = ").append(clazz.getCanonicalName()).append(".").append(property.value()).append(";\n"); + } else { + throw new GenerationException("Cannot find constructor for " + clazz.getCanonicalName()); + } + } + + private static void formatConstructor(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName, final ConstructorArgs constructorArgs) throws GenerationException { + final var args = getListConstructorArgs(constructorArgs, parsedObject); + final var genericTypes = getGenericTypes(progress, parsedObject); + progress.stringBuilder().append(START_VAR).append(variableName).append(NEW_ASSIGN).append(parsedObject.className()) + .append(genericTypes).append("(").append(String.join(", ", args)).append(");\n"); + final var sortedAttributes = getSortedAttributes(parsedObject); + for (final var value : sortedAttributes) { + if (!constructorArgs.namedArgs().containsKey(value.name())) { + formatProperty(progress, value, parsedObject, variableName); + } + } + final var sortedProperties = parsedObject.properties().entrySet().stream().sorted(Comparator.comparing(e -> e.getKey().name())).toList(); + for (final var e : sortedProperties) { + if (!constructorArgs.namedArgs().containsKey(e.getKey().name())) { + final var p = e.getKey(); + final var o = e.getValue(); + formatChild(progress, parsedObject, p, o, variableName); + } + } + final var notDefinedChildren = parsedObject.children().stream().filter(c -> !(c instanceof ParsedDefine)).toList(); + if (!notDefinedChildren.isEmpty()) { + final var defaultProperty = getDefaultProperty(parsedObject.className()); + if (!constructorArgs.namedArgs().containsKey(defaultProperty)) { + final var property = new ParsedPropertyImpl(defaultProperty, null, null); + formatChild(progress, parsedObject, property, notDefinedChildren, variableName); + } + } + } + + /** + * Formats an include object + * + * @param progress The generation progress + * @param include The include object + * @param subNodeName The sub node name + */ + private static void formatInclude(final GenerationProgress progress, final ParsedInclude include, final String subNodeName) throws GenerationException { + final var subViewVariable = progress.getNextVariableName("view"); + final var source = include.source(); + final var resources = include.resources(); + final var request = progress.request(); + final var subControllerClass = request.parameters().sourceToControllerName().get(source); + final var subClassName = request.parameters().sourceToGeneratedClassName().get(source); + if (subClassName == null) { + throw new GenerationException("Unknown include source : " + source); + } + final var sb = progress.stringBuilder(); + if (resources == null) { + sb.append(START_VAR).append(subViewVariable).append(NEW_ASSIGN).append(subClassName).append("(controllersMap, resourceBundlesMap);\n"); + } else { + final var subResourceBundlesMapVariable = progress.getNextVariableName("map"); + final var subBundleVariable = progress.getNextVariableName("bundle"); + sb.append(START_VAR).append(subResourceBundlesMapVariable).append(" = new HashMap<>(resourceBundlesMap);\n"); + sb.append(START_VAR).append(subBundleVariable).append(" = java.util.ResourceBundle.getBundle(\"").append(resources).append("\");\n"); + sb.append(" ").append(subResourceBundlesMapVariable).append(".put(").append(subControllerClass).append(", ").append(subBundleVariable).append(");\n"); + sb.append(START_VAR).append(subViewVariable).append(NEW_ASSIGN).append(subClassName).append("(controllersMap, ").append(subResourceBundlesMapVariable).append(");\n"); + } + sb.append(" final javafx.scene.Parent ").append(subNodeName).append(" = ").append(subViewVariable).append(".load();\n"); + injectSubController(progress, include, subViewVariable); + } + + private static void injectSubController(final GenerationProgress progress, final ParsedInclude include, final String subViewVariable) throws GenerationException { + final var id = include.controllerId(); + if (id != null) { + final var subControllerVariable = progress.getNextVariableName("controller"); + progress.stringBuilder().append(START_VAR).append(subControllerVariable).append(" = ").append(subViewVariable).append(".controller();\n"); + progress.idToVariableName().put(id, subControllerVariable); + progress.idToObject().put(id, include); + //TODO Don't inject if variable doesn't exist + injectControllerField(progress, id, subControllerVariable); + } + } + + /** + * Formats a fx:define + * + * @param progress The generation progress + * @param define The parsed define + * @param variableName The variable name + * @throws GenerationException if an error occurs + */ + private static void formatDefine(final GenerationProgress progress, final ParsedDefine define, final String variableName) throws GenerationException { + formatObject(progress, define.object(), variableName); + } + + /** + * Formats a fx:reference + * + * @param progress The generation progress + * @param reference The parsed reference + * @throws GenerationException if an error occurs + */ + private static void formatReference(final GenerationProgress progress, final ParsedReference reference, final String variableName) throws GenerationException { + final var id = reference.source(); + final var variable = progress.idToVariableName().get(id); + if (variable == null) { + throw new GenerationException("Unknown id : " + id); + } + progress.stringBuilder().append(START_VAR).append(variableName).append(" = ").append(variable).append(";\n"); + } + + /** + * Formats a fx:copy + * + * @param progress The generation progress + * @param copy The parsed copy + * @param variableName The variable name + * @throws GenerationException if an error occurs + */ + private static void formatCopy(final GenerationProgress progress, final ParsedCopy copy, final String variableName) throws GenerationException { + final var id = copy.source(); + final var variable = progress.idToVariableName().get(id); + final var object = progress.idToObject().get(id); + if (variable == null || object == null) { + throw new GenerationException("Unknown id : " + id); + } + progress.stringBuilder().append(START_VAR).append(variableName).append(NEW_ASSIGN).append(object.className()).append("(").append(variable).append(");\n"); + } + + /** + * Formats a constant object + * + * @param progress The generation progress + * @param constant The constant + * @param variableName The variable name + */ + private static void formatConstant(final GenerationProgress progress, final ParsedConstant constant, final String variableName) { + progress.stringBuilder().append(START_VAR).append(variableName).append(" = ").append(constant.className()).append(".").append(constant.constant()).append(";\n"); + } + + /** + * Formats a value object + * + * @param progress The generation progress + * @param value The value + * @param variableName The variable name + */ + private static void formatValue(final GenerationProgress progress, final ParsedValue value, final String variableName) { + progress.stringBuilder().append(START_VAR).append(variableName).append(" = ").append(value.className()).append(".valueOf(\"").append(value.value()).append("\");\n"); + } + + /** + * Formats a factory object + * + * @param progress The generation progress + * @param factory The factory + * @param variableName The variable name + */ + private static void formatFactory(final GenerationProgress progress, final ParsedFactory factory, final String variableName) throws GenerationException { + final var variables = new ArrayList(); + for (final var argument : factory.arguments()) { + final var argumentVariable = progress.getNextVariableName("arg"); + variables.add(argumentVariable); + format(progress, argument, argumentVariable); + } + progress.stringBuilder().append(START_VAR).append(variableName).append(" = ").append(factory.className()) + .append(".").append(factory.factory()).append("(").append(String.join(", ", variables)).append(");\n"); + } + + /** + * Formats the children objects of a property + * + * @param progress The generation progress + * @param parent The parent object + * @param property The parent property + * @param objects The child objects + * @param parentVariable The parent object variable + */ + private static void formatChild(final GenerationProgress progress, final ParsedObject parent, final ParsedProperty property, + final Iterable objects, final String parentVariable) throws GenerationException { + final var propertyName = property.name(); + final var variables = new ArrayList(); + for (final var object : objects) { + final var vn = progress.getNextVariableName(getVariablePrefix(object)); + format(progress, object, vn); + if (!(object instanceof ParsedDefine)) { + variables.add(vn); + } + } + if (variables.size() > 1) { + formatMultipleChildren(progress, variables, propertyName, parent, parentVariable); + } else if (variables.size() == 1) { + final var vn = variables.getFirst(); + formatSingleChild(progress, vn, property, parent, parentVariable); + } + } + + /** + * Formats children objects given that they are more than one + * + * @param progress The generation progress + * @param variables The children variables + * @param propertyName The property name + * @param parent The parent object + * @param parentVariable The parent object variable + */ + private static void formatMultipleChildren(final GenerationProgress progress, final Iterable variables, final String propertyName, final ParsedObject parent, + final String parentVariable) throws GenerationException { + final var getMethod = getGetMethod(propertyName); + if (hasMethod(ReflectionHelper.getClass(parent.className()), getMethod)) { + progress.stringBuilder().append(" ").append(parentVariable).append(".").append(getMethod).append("().addAll(java.util.List.of(").append(String.join(", ", variables)).append("));\n"); + } else { + throw getCannotSetException(propertyName, parent.className()); + } + } + + /** + * Formats a single child object + * + * @param progress The generation progress + * @param variableName The child's variable name + * @param property The parent property + * @param parent The parent object + * @param parentVariable The parent object variable + */ + private static void formatSingleChild(final GenerationProgress progress, final String variableName, final ParsedProperty property, final ParsedObject parent, + final String parentVariable) throws GenerationException { + if (property.sourceType() == null) { + formatSingleChildInstance(progress, variableName, property, parent, parentVariable); + } else { + formatSingleChildStatic(progress, variableName, property, parentVariable); + } + } + + /** + * Formats a single child object using an instance method on the parent object + * + * @param progress The generation progress + * @param variableName The child's variable name + * @param property The parent property + * @param parent The parent object + * @param parentVariable The parent object variable + */ + private static void formatSingleChildInstance(final GenerationProgress progress, final String variableName, + final ParsedProperty property, final ParsedObject parent, + final String parentVariable) throws GenerationException { + final var setMethod = getSetMethod(property); + final var getMethod = getGetMethod(property); + final var parentClass = ReflectionHelper.getClass(parent.className()); + final var sb = progress.stringBuilder(); + if (hasMethod(parentClass, setMethod)) { + sb.append(" ").append(parentVariable).append(".").append(setMethod).append("(").append(variableName).append(");\n"); + } else if (hasMethod(parentClass, getMethod)) { + //Probably a list method that has only one element + sb.append(" ").append(parentVariable).append(".").append(getMethod).append("().addAll(java.util.List.of(").append(variableName).append("));\n"); + } else { + throw getCannotSetException(property.name(), parent.className()); + } + } + + /** + * Formats a child object using a static method + * + * @param progress The generation progress + * @param variableName The child's variable name + * @param property The parent property + * @param parentVariable The parent variable + */ + private static void formatSingleChildStatic(final GenerationProgress progress, final String variableName, + final ParsedProperty property, final String parentVariable) throws GenerationException { + final var setMethod = getSetMethod(property); + if (hasStaticMethod(ReflectionHelper.getClass(property.sourceType()), setMethod)) { + progress.stringBuilder().append(" ").append(property.sourceType()).append(".").append(setMethod) + .append("(").append(parentVariable).append(", ").append(variableName).append(");\n"); + } else { + throw getCannotSetException(property.name(), property.sourceType()); + } + } + + private static GenerationException getCannotSetException(final String propertyName, final String className) { + return new GenerationException("Cannot set " + propertyName + " on " + className); + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/Parameter.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/Parameter.java similarity index 56% rename from core/src/main/java/com/github/gtache/fxml/compiler/impl/Parameter.java rename to core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/Parameter.java index b22effe..eca8634 100644 --- a/core/src/main/java/com/github/gtache/fxml/compiler/impl/Parameter.java +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/Parameter.java @@ -1,4 +1,4 @@ -package com.github.gtache.fxml.compiler.impl; +package com.github.gtache.fxml.compiler.impl.internal; import static java.util.Objects.requireNonNull; @@ -11,7 +11,15 @@ import static java.util.Objects.requireNonNull; */ record Parameter(String name, Class type, String defaultValue) { - Parameter { + /** + * Instantiates a new Parameter + * + * @param name The parameter name + * @param type The parameter type + * @param defaultValue The parameter default value + * @throws NullPointerException if any parameter is null + */ + public Parameter { requireNonNull(name); requireNonNull(type); requireNonNull(defaultValue); diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/PropertyFormatter.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/PropertyFormatter.java new file mode 100644 index 0000000..3dd6d63 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/PropertyFormatter.java @@ -0,0 +1,127 @@ +package com.github.gtache.fxml.compiler.impl.internal; + +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.impl.ControllerFieldInjectionTypes; +import com.github.gtache.fxml.compiler.impl.GeneratorImpl; +import com.github.gtache.fxml.compiler.impl.ResourceBundleInjectionTypes; +import com.github.gtache.fxml.compiler.parsing.ParsedObject; +import com.github.gtache.fxml.compiler.parsing.ParsedProperty; +import javafx.event.EventHandler; + +import java.util.Objects; + +import static com.github.gtache.fxml.compiler.impl.internal.ControllerInjector.injectEventHandlerControllerMethod; +import static com.github.gtache.fxml.compiler.impl.internal.FieldSetter.setEventHandler; +import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*; +import static com.github.gtache.fxml.compiler.impl.internal.ReflectionHelper.*; +import static com.github.gtache.fxml.compiler.impl.internal.ValueFormatter.getArg; + +/** + * Helper methods for {@link GeneratorImpl} to format properties + */ +final class PropertyFormatter { + private PropertyFormatter() { + + } + + /** + * Formats a property + * + * @param progress The generation progress + * @param property The property to format + * @param parent The property's parent object + * @param parentVariable The parent variable + * @throws GenerationException if an error occurs + */ + static void formatProperty(final GenerationProgress progress, final ParsedProperty property, final ParsedObject parent, final String parentVariable) throws GenerationException { + final var propertyName = property.name(); + if (propertyName.equals(FX_ID)) { + handleId(progress, parent, parentVariable); + } else if (propertyName.equals("fx:controller")) { + checkDuplicateController(progress, parent); + } else if (Objects.equals(property.sourceType(), EventHandler.class.getName())) { + handleEventHandler(progress, property, parentVariable); + } else if (property.sourceType() != null) { + handleStaticProperty(progress, property, parentVariable, propertyName); + } else { + handleProperty(progress, property, parent, parentVariable); + } + } + + private static void checkDuplicateController(final GenerationProgress progress, final ParsedObject parent) throws GenerationException { + if (parent != progress.request().rootObject()) { + throw new GenerationException("Invalid nested controller"); + } + } + + private static void handleEventHandler(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) throws GenerationException { + if (property.value().startsWith("#")) { + injectEventHandlerControllerMethod(progress, property, parentVariable); + } else { + setEventHandler(progress, property, parentVariable); + } + } + + private static void handleStaticProperty(final GenerationProgress progress, final ParsedProperty property, final String parentVariable, final String propertyName) throws GenerationException { + final var setMethod = getSetMethod(propertyName); + final var propertySourceTypeClass = ReflectionHelper.getClass(property.sourceType()); + if (hasStaticMethod(propertySourceTypeClass, setMethod)) { + final var method = getStaticMethod(propertySourceTypeClass, setMethod); + final var parameterType = method.getParameterTypes()[1]; + final var arg = getArg(progress, property.value(), parameterType); + setLaterIfNeeded(progress, property, parameterType, " " + property.sourceType() + "." + setMethod + "(" + parentVariable + ", " + arg + ");\n"); + } else { + throw new GenerationException("Cannot set " + propertyName + " on " + property.sourceType()); + } + } + + private static void handleProperty(final GenerationProgress progress, final ParsedProperty property, final ParsedObject parent, final String parentVariable) throws GenerationException { + final var propertyName = property.name(); + final var setMethod = getSetMethod(propertyName); + final var getMethod = getGetMethod(propertyName); + final var parentClass = ReflectionHelper.getClass(parent.className()); + if (hasMethod(parentClass, setMethod)) { + handleSetProperty(progress, property, parentClass, parentVariable); + } else if (hasMethod(parentClass, getMethod)) { + handleGetProperty(progress, property, parentClass, parentVariable); + } else { + throw new GenerationException("Cannot set " + propertyName + " on " + parent.className()); + } + } + + private static void handleSetProperty(final GenerationProgress progress, final ParsedProperty property, final Class parentClass, final String parentVariable) throws GenerationException { + final var setMethod = getSetMethod(property.name()); + final var method = getMethod(parentClass, setMethod); + final var parameterType = method.getParameterTypes()[0]; + final var arg = getArg(progress, property.value(), parameterType); + setLaterIfNeeded(progress, property, parameterType, " " + parentVariable + "." + setMethod + "(" + arg + ");\n"); + } + + private static void handleGetProperty(final GenerationProgress progress, final ParsedProperty property, final Class parentClass, final String parentVariable) throws GenerationException { + final var getMethod = getGetMethod(property.name()); + final var method = getMethod(parentClass, getMethod); + final var returnType = method.getReturnType(); + if (hasMethod(returnType, "addAll")) { + final var arg = getArg(progress, property.value(), String.class); + setLaterIfNeeded(progress, property, String.class, " " + parentVariable + "." + getMethod + "().addAll(java.util.List.of(" + arg + "));\n"); + } + } + + /** + * Saves the text to set after constructor creation if factory injection is used + * + * @param progress The generation progress + * @param property The property + * @param type The type + * @param arg The argument + * @throws GenerationException if an error occurs + */ + private static void setLaterIfNeeded(final GenerationProgress progress, final ParsedProperty property, final Class type, final String arg) throws GenerationException { + if (type == String.class && property.value().startsWith("%") && progress.request().parameters().resourceBundleInjection().injectionType() == ResourceBundleInjectionTypes.GETTER + && getControllerInjection(progress).fieldInjectionType() == ControllerFieldInjectionTypes.FACTORY) { + progress.controllerFactoryPostAction().add(arg); + } else { + progress.stringBuilder().append(arg); + } + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/ReflectionHelper.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ReflectionHelper.java similarity index 68% rename from core/src/main/java/com/github/gtache/fxml/compiler/impl/ReflectionHelper.java rename to core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ReflectionHelper.java index e9545bd..b483d15 100644 --- a/core/src/main/java/com/github/gtache/fxml/compiler/impl/ReflectionHelper.java +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ReflectionHelper.java @@ -1,5 +1,8 @@ -package com.github.gtache.fxml.compiler.impl; +package com.github.gtache.fxml.compiler.impl.internal; +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.parsing.ParsedObject; +import javafx.beans.DefaultProperty; import javafx.beans.NamedArg; import javafx.scene.Node; @@ -12,12 +15,15 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.FX_ID; + /** * Helper methods for reflection */ final class ReflectionHelper { private static final Map, Boolean> HAS_VALUE_OF = new ConcurrentHashMap<>(); private static final Map, Boolean> IS_GENERIC = new ConcurrentHashMap<>(); + private static final Map DEFAULT_PROPERTY = new ConcurrentHashMap<>(); private static final Map, Map> METHODS = new ConcurrentHashMap<>(); private static final Map, Map> STATIC_METHODS = new ConcurrentHashMap<>(); @@ -199,6 +205,69 @@ final class ReflectionHelper { } } + /** + * Computes the default property for the given class + * + * @param className The class name + * @return The default property + * @throws GenerationException If the class is not found or no default property is found + */ + static String getDefaultProperty(final String className) throws GenerationException { + if (DEFAULT_PROPERTY.containsKey(className)) { + return DEFAULT_PROPERTY.get(className); + } else { + final var defaultProperty = computeDefaultProperty(className); + if (defaultProperty != null) { + DEFAULT_PROPERTY.put(className, defaultProperty); + } + return defaultProperty; + } + } + + /** + * Gets the wrapper class for the given class + * + * @param clazz The class + * @return The wrapper class (e.g. int.class -> Integer.class) or the original class if it is not a primitive + */ + static String getWrapperClass(final Class clazz) { + final var name = clazz.getName(); + if (name.contains(".") || Character.isUpperCase(name.charAt(0))) { + return name; + } else { + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + } + + /** + * Gets the class for the given class name + * + * @param className The class name + * @return The class + * @throws GenerationException If the class is not found + */ + public static Class getClass(final String className) throws GenerationException { + try { + return Class.forName(className, false, Thread.currentThread().getContextClassLoader()); + } catch (final ClassNotFoundException e) { + throw new GenerationException("Cannot find class " + className + " ; Is a dependency missing for the plugin?", e); + } + } + + private static String computeDefaultProperty(final String className) throws GenerationException { + try { + final var clazz = Class.forName(className, false, Thread.currentThread().getContextClassLoader()); + final var annotation = clazz.getAnnotation(DefaultProperty.class); + if (annotation == null) { + return null; + } else { + return annotation.value(); + } + } catch (final ClassNotFoundException e) { + throw new GenerationException("Class " + className + " not found ; Either specify the property explicitly or put the class in a dependency", e); + } + } + /** * Computes the default value for the given class * @@ -217,4 +286,31 @@ final class ReflectionHelper { return "null"; } } + + /** + * Gets the generic types for the given object + * + * @param progress The generation progress + * @param parsedObject The parsed object + * @return The generic types + * @throws GenerationException if an error occurs + */ + static String getGenericTypes(final GenerationProgress progress, final ParsedObject parsedObject) throws GenerationException { + final var clazz = getClass(parsedObject.className()); + if (isGeneric(clazz)) { + final var idProperty = parsedObject.attributes().get(FX_ID); + if (idProperty == null) { + return "<>"; + } else { + final var id = idProperty.value(); + final var genericTypes = progress.request().controllerInfo().propertyGenericTypes(id); + if (genericTypes == null) { //Raw + return ""; + } else { + return "<" + String.join(", ", genericTypes) + ">"; + } + } + } + return ""; + } } diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/SceneBuilder.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/SceneBuilder.java new file mode 100644 index 0000000..76fc74f --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/SceneBuilder.java @@ -0,0 +1,77 @@ +package com.github.gtache.fxml.compiler.impl.internal; + +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.impl.GeneratorImpl; +import com.github.gtache.fxml.compiler.parsing.ParsedObject; +import javafx.scene.paint.Color; + +import java.util.ArrayList; +import java.util.Collection; + +import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*; +import static com.github.gtache.fxml.compiler.impl.internal.ObjectFormatter.format; +import static com.github.gtache.fxml.compiler.impl.internal.URLBuilder.formatURL; + +/** + * Helper methods for {@link GeneratorImpl} to format Scenes + */ +final class SceneBuilder { + + private SceneBuilder() { + + } + + static void formatScene(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException { + final var root = findRoot(parsedObject); + final var rootVariableName = progress.getNextVariableName("root"); + format(progress, root, rootVariableName); + final var sortedAttributes = getSortedAttributes(parsedObject); + double width = -1; + double height = -1; + var paint = Color.WHITE.toString(); + final var stylesheets = new ArrayList(); + for (final var property : sortedAttributes) { + switch (property.name()) { + case FX_ID -> { + //Do nothing + } + case "width" -> width = Double.parseDouble(property.value()); + case "height" -> height = Double.parseDouble(property.value()); + case "fill" -> paint = property.value(); + case "stylesheets" -> stylesheets.add(property.value()); + default -> throw new GenerationException("Unknown font attribute : " + property.name()); + } + } + final var sb = progress.stringBuilder(); + sb.append(START_VAR).append(variableName).append(" = new javafx.scene.Scene(").append(rootVariableName).append(", ") + .append(width).append(", ").append(height).append(", javafx.scene.paint.Color.valueOf(\"").append(paint).append("\"));\n"); + addStylesheets(progress, variableName, stylesheets); + handleId(progress, parsedObject, variableName); + } + + private static ParsedObject findRoot(final ParsedObject parsedObject) throws GenerationException { + final var rootProperty = parsedObject.properties().entrySet().stream().filter(e -> e.getKey().name().equals("root")) + .filter(e -> e.getValue().size() == 1) + .map(e -> e.getValue().getFirst()).findFirst().orElse(null); + if (rootProperty != null) { + return rootProperty; + } else if (parsedObject.children().size() == 1) { + return parsedObject.children().getFirst(); + } else { + throw new GenerationException("Scene must have a root"); + } + } + + private static void addStylesheets(final GenerationProgress progress, final String variableName, final Collection stylesheets) { + if (!stylesheets.isEmpty()) { + final var urlVariables = formatURL(progress, stylesheets); + final var tmpVariable = progress.getNextVariableName("stylesheets"); + final var sb = progress.stringBuilder(); + sb.append(""" + final var %1$s = %2$s.getStylesheets(); + %1$s.addAll(java.util.List.of(%3$s)); + """.formatted(tmpVariable, variableName, String.join(", ", urlVariables))); + stylesheets.forEach(s -> sb.append(" ").append(variableName).append(".getStyleSheets().add(\"").append(s).append("\");\n")); + } + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/TriangleMeshBuilder.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/TriangleMeshBuilder.java new file mode 100644 index 0000000..cedd1e2 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/TriangleMeshBuilder.java @@ -0,0 +1,136 @@ +package com.github.gtache.fxml.compiler.impl.internal; + +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.impl.GeneratorImpl; +import com.github.gtache.fxml.compiler.parsing.ParsedObject; +import com.github.gtache.fxml.compiler.parsing.ParsedProperty; +import javafx.scene.shape.VertexFormat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*; + +/** + * Helper methods for {@link GeneratorImpl} to format TriangleMeshes + */ +final class TriangleMeshBuilder { + + private TriangleMeshBuilder() { + + } + + static void formatTriangleMesh(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException { + if (parsedObject.children().isEmpty() && parsedObject.properties().isEmpty()) { + final var sortedAttributes = getSortedAttributes(parsedObject); + final var points = new ArrayList(); + final var texCoords = new ArrayList(); + final var normals = new ArrayList(); + final var faces = new ArrayList(); + final var faceSmoothingGroups = new ArrayList(); + VertexFormat vertexFormat = null; + for (final var property : sortedAttributes) { + switch (property.name().toLowerCase()) { + case FX_ID -> { + //Do nothing + } + case "points" -> { + points.clear(); + points.addAll(parseList(property.value(), Float::parseFloat)); + } + case "texcoords" -> { + texCoords.clear(); + texCoords.addAll(parseList(property.value(), Float::parseFloat)); + } + case "normals" -> { + normals.clear(); + normals.addAll(parseList(property.value(), Float::parseFloat)); + } + case "faces" -> { + faces.clear(); + faces.addAll(parseList(property.value(), Integer::parseInt)); + } + case "facesmoothinggroups" -> { + faceSmoothingGroups.clear(); + faceSmoothingGroups.addAll(parseList(property.value(), Integer::parseInt)); + } + case "vertexformat" -> vertexFormat = parseVertexFormat(property); + default -> throw new GenerationException("Unknown TriangleMesh attribute : " + property.name()); + } + } + final var sb = progress.stringBuilder(); + sb.append(START_VAR).append(variableName).append(" = new javafx.scene.shape.TriangleMesh();\n"); + setPoints(progress, variableName, points); + setTexCoords(progress, variableName, texCoords); + setNormals(progress, variableName, normals); + setFaces(progress, variableName, faces); + setFaceSmoothingGroups(progress, variableName, faceSmoothingGroups); + setVertexFormat(progress, variableName, vertexFormat); + handleId(progress, parsedObject, variableName); + } else { + throw new GenerationException("Image cannot have children or properties : " + parsedObject); + } + } + + private static VertexFormat parseVertexFormat(final ParsedProperty property) throws GenerationException { + if (property.value().equalsIgnoreCase("point_texcoord")) { + return VertexFormat.POINT_TEXCOORD; + } else if (property.value().equalsIgnoreCase("point_normal_texcoord")) { + return VertexFormat.POINT_NORMAL_TEXCOORD; + } else { + throw new GenerationException("Unknown vertex format : " + property.value()); + } + } + + private static void setPoints(final GenerationProgress progress, final String variableName, final Collection points) { + if (!points.isEmpty()) { + progress.stringBuilder().append(" ").append(variableName).append(".getPoints().setAll(new float[]{").append(formatList(points)).append("});\n"); + } + } + + private static void setTexCoords(final GenerationProgress progress, final String variableName, final Collection texCoords) { + if (!texCoords.isEmpty()) { + progress.stringBuilder().append(" ").append(variableName).append(".getTexCoords().setAll(new float[]{").append(formatList(texCoords)).append("});\n"); + } + } + + private static void setNormals(final GenerationProgress progress, final String variableName, final Collection normals) { + if (!normals.isEmpty()) { + progress.stringBuilder().append(" ").append(variableName).append(".getNormals().setAll(new float[]{").append(formatList(normals)).append("});\n"); + } + } + + private static void setFaces(final GenerationProgress progress, final String variableName, final Collection faces) { + if (!faces.isEmpty()) { + progress.stringBuilder().append(" ").append(variableName).append(".getFaces().setAll(new int[]{").append(formatList(faces)).append("});\n"); + } + } + + private static void setFaceSmoothingGroups(final GenerationProgress progress, final String variableName, final Collection faceSmoothingGroups) { + if (!faceSmoothingGroups.isEmpty()) { + progress.stringBuilder().append(" ").append(variableName).append(".getFaceSmoothingGroups().setAll(new int[]{").append(formatList(faceSmoothingGroups)).append("});\n"); + } + } + + private static void setVertexFormat(final GenerationProgress progress, final String variableName, final VertexFormat vertexFormat) { + if (vertexFormat != null) { + progress.stringBuilder().append(" ").append(variableName).append(".setVertexFormat(javafx.scene.shape.VertexFormat.").append(vertexFormat).append(");\n"); + } + } + + private static String formatList(final Collection list) { + return list.stream().map(String::valueOf).collect(Collectors.joining(", ")); + } + + private static List parseList(final CharSequence value, final Function parser) { + final var splitPattern = Pattern.compile("[\\s+,]"); + final var split = splitPattern.split(value); + return Arrays.stream(split).map(parser).collect(Collectors.toList()); + } + +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/URLBuilder.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/URLBuilder.java new file mode 100644 index 0000000..e59b9a9 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/URLBuilder.java @@ -0,0 +1,62 @@ +package com.github.gtache.fxml.compiler.impl.internal; + +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.impl.GeneratorImpl; +import com.github.gtache.fxml.compiler.parsing.ParsedObject; + +import java.util.Collection; +import java.util.List; + +import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*; + +/** + * Helper methods for {@link GeneratorImpl} to format URLs + */ +final class URLBuilder { + + private URLBuilder() { + + } + + static List formatURL(final GenerationProgress progress, final Collection stylesheets) { + return stylesheets.stream().map(s -> formatURL(progress, s)).toList(); + } + + static String formatURL(final GenerationProgress progress, final String url) { + final var variableName = progress.getNextVariableName("url"); + final var sb = progress.stringBuilder(); + if (url.startsWith("@")) { + sb.append(START_VAR).append(variableName).append(" = getClass().getResource(\"").append(url.substring(1)).append("\");\n"); + } else { + sb.append(""" + final java.net.URL %1$s; + try { + %1$s = new java.net.URI("%2$s").toURL(); + } catch (final java.net.MalformedURLException | java.net.URISyntaxException e) { + throw new RuntimeException("Couldn't parse url : %2$s", e); + } + """.formatted(variableName, url)); + } + return variableName; + } + + static void formatURL(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException { + if (parsedObject.children().isEmpty() && parsedObject.properties().isEmpty()) { + final var sortedAttributes = getSortedAttributes(parsedObject); + String value = null; + for (final var property : sortedAttributes) { + switch (property.name()) { + case FX_ID -> { + //Do nothing + } + case "value" -> value = property.value(); + default -> throw new GenerationException("Unknown URL attribute : " + property.name()); + } + } + progress.stringBuilder().append(START_VAR).append(variableName).append(" = getClass().getResource(\"").append(value).append("\");\n"); + handleId(progress, parsedObject, variableName); + } else { + throw new GenerationException("URL cannot have children or properties : " + parsedObject); + } + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ValueFormatter.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ValueFormatter.java new file mode 100644 index 0000000..1f1c714 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/ValueFormatter.java @@ -0,0 +1,118 @@ +package com.github.gtache.fxml.compiler.impl.internal; + +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.impl.GeneratorImpl; +import com.github.gtache.fxml.compiler.impl.ResourceBundleInjectionTypes; + +import java.util.regex.Pattern; + +import static com.github.gtache.fxml.compiler.impl.internal.ReflectionHelper.getWrapperClass; +import static com.github.gtache.fxml.compiler.impl.internal.ReflectionHelper.hasValueOf; + +/** + * Helper methods for {@link GeneratorImpl} to format values + */ +final class ValueFormatter { + + private static final Pattern INT_PATTERN = Pattern.compile("\\d+"); + private static final Pattern DECIMAL_PATTERN = Pattern.compile("\\d+(?:\\.\\d+)?"); + private static final Pattern START_BACKSLASH_PATTERN = Pattern.compile("^\\\\"); + + private ValueFormatter() { + } + + /** + * Formats an argument to a method + * + * @param progress The generation progress + * @param value The value + * @param parameterType The parameter type + * @return The formatted value + * @throws GenerationException if an error occurs + */ + static String getArg(final GenerationProgress progress, final String value, final Class parameterType) throws GenerationException { + if (parameterType == String.class && value.startsWith("%")) { + return getBundleValue(progress, value.substring(1)); + } else if (value.startsWith("@")) { + final var subpath = value.substring(1); + return getResourceValue(subpath); + } else if (value.startsWith("${")) { + throw new UnsupportedOperationException("Not implemented yet"); + } else if (value.startsWith("$")) { + final var variable = progress.idToVariableName().get(value.substring(1)); + if (variable == null) { + throw new GenerationException("Unknown variable : " + value.substring(1)); + } + return variable; + } else { + return toString(value, parameterType); + } + } + + private static String getResourceValue(final String subpath) { + return "getClass().getResource(\"" + subpath + "\").toString()"; + } + + /** + * Gets the resource bundle value for the given value + * + * @param progress The generation progress + * @param value The value + * @return The resource bundle value + * @throws GenerationException if an error occurs + */ + private static String getBundleValue(final GenerationProgress progress, final String value) throws GenerationException { + final var resourceBundleInjectionType = progress.request().parameters().resourceBundleInjection().injectionType(); + if (resourceBundleInjectionType instanceof final ResourceBundleInjectionTypes types) { + return switch (types) { + case CONSTRUCTOR, GET_BUNDLE -> "bundle.getString(\"" + value + "\")"; + case GETTER -> "controller.resources().getString(\"" + value + "\")"; + }; + } else { + throw new GenerationException("Unknown resource bundle injection type : " + resourceBundleInjectionType); + } + } + + + /** + * Computes the string value to use in the generated code + * + * @param value The value + * @param clazz The value class + * @return The computed string value + */ + static String toString(final String value, final Class clazz) { + if (clazz == String.class) { + return "\"" + START_BACKSLASH_PATTERN.matcher(value).replaceAll("").replace("\"", "\\\"") + "\""; + } else if (clazz == char.class || clazz == Character.class) { + return "'" + value + "'"; + } else if (clazz == boolean.class || clazz == Boolean.class) { + return value; + } else if (clazz == byte.class || clazz == Byte.class || clazz == short.class || clazz == Short.class || + clazz == int.class || clazz == Integer.class || clazz == long.class || clazz == Long.class) { + if (INT_PATTERN.matcher(value).matches()) { + return value; + } else { + return getValueOf(getWrapperClass(clazz), value); + } + } else if (clazz == float.class || clazz == Float.class || clazz == double.class || clazz == Double.class) { + if (DECIMAL_PATTERN.matcher(value).matches()) { + return value; + } else { + return getValueOf(getWrapperClass(clazz), value); + } + } else if (hasValueOf(clazz)) { + if (clazz.isEnum()) { + return clazz.getCanonicalName() + "." + value; + } else { + return getValueOf(clazz.getCanonicalName(), value); + } + } else { + return value; + } + } + + private static String getValueOf(final String clazz, final String value) { + return clazz + ".valueOf(\"" + value + "\")"; + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/WebViewBuilder.java b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/WebViewBuilder.java new file mode 100644 index 0000000..cdf8216 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/impl/internal/WebViewBuilder.java @@ -0,0 +1,111 @@ +package com.github.gtache.fxml.compiler.impl.internal; + +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.impl.GeneratorImpl; +import com.github.gtache.fxml.compiler.parsing.ParsedObject; +import com.github.gtache.fxml.compiler.parsing.ParsedProperty; + +import static com.github.gtache.fxml.compiler.impl.internal.ControllerInjector.injectCallbackControllerMethod; +import static com.github.gtache.fxml.compiler.impl.internal.ControllerInjector.injectEventHandlerControllerMethod; +import static com.github.gtache.fxml.compiler.impl.internal.FieldSetter.setEventHandler; +import static com.github.gtache.fxml.compiler.impl.internal.FieldSetter.setField; +import static com.github.gtache.fxml.compiler.impl.internal.GenerationHelper.*; +import static com.github.gtache.fxml.compiler.impl.internal.PropertyFormatter.formatProperty; + +/** + * Helper methods for {@link GeneratorImpl} to format WebViews + */ +final class WebViewBuilder { + + private WebViewBuilder() { + + } + + /** + * Formats a WebView object + * + * @param progress The generation progress + * @param parsedObject The parsed object + * @param variableName The variable name + * @throws GenerationException if an error occurs + */ + static void formatWebView(final GenerationProgress progress, final ParsedObject parsedObject, final String variableName) throws GenerationException { + if (parsedObject.children().isEmpty() && parsedObject.properties().isEmpty()) { + final var sortedAttributes = getSortedAttributes(parsedObject); + final var sb = progress.stringBuilder(); + sb.append(START_VAR).append(variableName).append(" = new javafx.scene.web.WebView();\n"); + final var engineVariable = progress.getNextVariableName("engine"); + sb.append(START_VAR).append(engineVariable).append(" = ").append(variableName).append(".getEngine();\n"); + for (final var value : sortedAttributes) { + formatAttribute(progress, value, parsedObject, variableName, engineVariable); + } + handleId(progress, parsedObject, variableName); + } else { + throw new GenerationException("WebView cannot have children or properties : " + parsedObject); + } + } + + private static void formatAttribute(final GenerationProgress progress, final ParsedProperty value, final ParsedObject parsedObject, + final String variableName, final String engineVariable) throws GenerationException { + switch (value.name()) { + case FX_ID -> { + //Do nothing + } + case "confirmHandler" -> injectConfirmHandler(progress, value, engineVariable); + case "createPopupHandler" -> injectCreatePopupHandler(progress, value, engineVariable); + case "onAlert", "onResized", "onStatusChanged", "onVisibilityChanged" -> + injectEventHandler(progress, value, engineVariable); + case "promptHandler" -> injectPromptHandler(progress, value, engineVariable); + case "location" -> injectLocation(progress, value, engineVariable); + default -> formatProperty(progress, value, parsedObject, variableName); + } + } + + private static void injectConfirmHandler(final GenerationProgress progress, final ParsedProperty value, final String engineVariable) throws GenerationException { + if (value.value().startsWith("#")) { + injectCallbackControllerMethod(progress, value, engineVariable, "String.class"); + } else { + setCallback(progress, value, engineVariable); + } + } + + private static void injectCreatePopupHandler(final GenerationProgress progress, final ParsedProperty value, final String engineVariable) throws GenerationException { + if (value.value().startsWith("#")) { + injectCallbackControllerMethod(progress, value, engineVariable, "javafx.scene.web.PopupFeatures.class"); + } else { + setCallback(progress, value, engineVariable); + } + } + + private static void injectEventHandler(final GenerationProgress progress, final ParsedProperty value, final String engineVariable) throws GenerationException { + if (value.value().startsWith("#")) { + injectEventHandlerControllerMethod(progress, value, engineVariable); + } else { + setEventHandler(progress, value, engineVariable); + } + } + + private static void injectPromptHandler(final GenerationProgress progress, final ParsedProperty value, final String engineVariable) throws GenerationException { + if (value.value().startsWith("#")) { + injectCallbackControllerMethod(progress, value, engineVariable, "javafx.scene.web.PromptData.class"); + } else { + setCallback(progress, value, engineVariable); + } + } + + private static void injectLocation(final GenerationProgress progress, final ParsedProperty value, final String engineVariable) { + progress.stringBuilder().append(" ").append(engineVariable).append(".load(\"").append(value.value()).append("\");\n"); + + } + + /** + * Sets a callback field + * + * @param progress The generation progress + * @param property The property to inject + * @param parentVariable The parent variable + */ + private static void setCallback(final GenerationProgress progress, final ParsedProperty property, final String parentVariable) throws GenerationException { + setField(progress, property, parentVariable, "javafx.util.Callback"); + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedCopyImpl.java b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedCopyImpl.java new file mode 100644 index 0000000..4507ce5 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedCopyImpl.java @@ -0,0 +1,18 @@ +package com.github.gtache.fxml.compiler.parsing.impl; + +import com.github.gtache.fxml.compiler.parsing.ParsedCopy; +import com.github.gtache.fxml.compiler.parsing.ParsedProperty; + +import java.util.Map; + +/** + * Implementation of {@link ParsedCopy} + * + * @param attributes The reference properties + */ +public record ParsedCopyImpl(Map attributes) implements ParsedCopy { + + public ParsedCopyImpl { + attributes = Map.copyOf(attributes); + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedDefineImpl.java b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedDefineImpl.java new file mode 100644 index 0000000..2ccccab --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedDefineImpl.java @@ -0,0 +1,20 @@ +package com.github.gtache.fxml.compiler.parsing.impl; + +import com.github.gtache.fxml.compiler.parsing.ParsedDefine; +import com.github.gtache.fxml.compiler.parsing.ParsedObject; + +import java.util.Objects; + +/** + * Implementation of {@link ParsedObject} + * + * @param className The object class + * @param attributes The object properties + * @param properties The object children (complex properties) + */ +public record ParsedDefineImpl(ParsedObject object) implements ParsedDefine { + + public ParsedDefineImpl { + Objects.requireNonNull(object); + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedFactoryImpl.java b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedFactoryImpl.java new file mode 100644 index 0000000..5a026c7 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedFactoryImpl.java @@ -0,0 +1,29 @@ +package com.github.gtache.fxml.compiler.parsing.impl; + +import com.github.gtache.fxml.compiler.parsing.ParsedFactory; +import com.github.gtache.fxml.compiler.parsing.ParsedObject; +import com.github.gtache.fxml.compiler.parsing.ParsedProperty; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.SequencedCollection; + +/** + * Implementation of {@link ParsedFactory} + * + * @param className The factory class + * @param attributes The factory properties + * @param arguments The factory arguments + */ +public record ParsedFactoryImpl(String className, Map attributes, + SequencedCollection arguments, + SequencedCollection children) implements ParsedFactory { + + public ParsedFactoryImpl { + Objects.requireNonNull(className); + attributes = Map.copyOf(attributes); + arguments = List.copyOf(arguments); + children = List.copyOf(children); + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedObjectImpl.java b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedObjectImpl.java index e4925be..42e1039 100644 --- a/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedObjectImpl.java +++ b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedObjectImpl.java @@ -3,10 +3,9 @@ package com.github.gtache.fxml.compiler.parsing.impl; import com.github.gtache.fxml.compiler.parsing.ParsedObject; import com.github.gtache.fxml.compiler.parsing.ParsedProperty; -import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.SequencedCollection; @@ -20,74 +19,13 @@ import java.util.SequencedMap; * @param properties The object children (complex properties) */ public record ParsedObjectImpl(String className, Map attributes, - SequencedMap> properties) implements ParsedObject { + SequencedMap> properties, + SequencedCollection children) implements ParsedObject { public ParsedObjectImpl { Objects.requireNonNull(className); attributes = Map.copyOf(attributes); properties = Collections.unmodifiableSequencedMap(new LinkedHashMap<>(properties)); - } - - /** - * Builder for {@link ParsedObjectImpl} - */ - public static class Builder { - - private String className; - private final Map attributes; - private final SequencedMap> properties; - - /** - * Creates a new builder - */ - public Builder() { - this.attributes = new HashMap<>(); - this.properties = new LinkedHashMap<>(); - } - - /** - * Sets the object class - * - * @param className The object class - * @return The builder - */ - public Builder className(final String className) { - this.className = className; - return this; - } - - /** - * Adds an attribute - * - * @param attribute The attribute - * @return The builder - */ - public Builder addAttribute(final ParsedProperty attribute) { - attributes.put(attribute.name(), attribute); - return this; - } - - /** - * Adds a property - * - * @param property The property - * @param child The property element - * @return The builder - */ - public Builder addProperty(final ParsedProperty property, final ParsedObject child) { - final var sequence = properties.computeIfAbsent(property, k -> new ArrayList<>()); - sequence.add(child); - properties.put(property, sequence); - return this; - } - - /** - * Builds the object - * - * @return The object - */ - public ParsedObjectImpl build() { - return new ParsedObjectImpl(className, attributes, properties); - } + children = List.copyOf(children); } } diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedPropertyImpl.java b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedPropertyImpl.java index eb5dbd5..a7d0887 100644 --- a/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedPropertyImpl.java +++ b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedPropertyImpl.java @@ -10,6 +10,7 @@ import java.util.Objects; * @param name The property name * @param sourceType The property source type * @param value The property value + * @param defines The property defines */ public record ParsedPropertyImpl(String name, String sourceType, String value) implements ParsedProperty { diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedReferenceImpl.java b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedReferenceImpl.java new file mode 100644 index 0000000..a3a55d5 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedReferenceImpl.java @@ -0,0 +1,18 @@ +package com.github.gtache.fxml.compiler.parsing.impl; + +import com.github.gtache.fxml.compiler.parsing.ParsedProperty; +import com.github.gtache.fxml.compiler.parsing.ParsedReference; + +import java.util.Map; + +/** + * Implementation of {@link ParsedReference} + * + * @param attributes The reference properties + */ +public record ParsedReferenceImpl(Map attributes) implements ParsedReference { + + public ParsedReferenceImpl { + attributes = Map.copyOf(attributes); + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedTextImpl.java b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedTextImpl.java new file mode 100644 index 0000000..9249d68 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedTextImpl.java @@ -0,0 +1,23 @@ +package com.github.gtache.fxml.compiler.parsing.impl; + +import com.github.gtache.fxml.compiler.parsing.ParsedText; + +import java.util.Objects; + +/** + * Implementation of {@link ParsedText} + * + * @param text The text + */ +public record ParsedTextImpl(String text) implements ParsedText { + + /** + * Instantiates a new parsed text + * + * @param text The text + * @throws NullPointerException if the text is null + */ + public ParsedTextImpl { + Objects.requireNonNull(text); + } +} diff --git a/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedValueImpl.java b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedValueImpl.java new file mode 100644 index 0000000..bc01b04 --- /dev/null +++ b/core/src/main/java/com/github/gtache/fxml/compiler/parsing/impl/ParsedValueImpl.java @@ -0,0 +1,21 @@ +package com.github.gtache.fxml.compiler.parsing.impl; + +import com.github.gtache.fxml.compiler.parsing.ParsedProperty; +import com.github.gtache.fxml.compiler.parsing.ParsedValue; + +import java.util.Map; +import java.util.Objects; + +/** + * Implementation of {@link ParsedValue} + * + * @param className The value class + * @param attributes The value properties + */ +public record ParsedValueImpl(String className, Map attributes) implements ParsedValue { + + public ParsedValueImpl { + Objects.requireNonNull(className); + attributes = Map.copyOf(attributes); + } +} diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/impl/TestClassesFinder.java b/core/src/test/java/com/github/gtache/fxml/compiler/impl/TestClassesFinder.java index d60ee46..039c1fd 100644 --- a/core/src/test/java/com/github/gtache/fxml/compiler/impl/TestClassesFinder.java +++ b/core/src/test/java/com/github/gtache/fxml/compiler/impl/TestClassesFinder.java @@ -13,10 +13,14 @@ class TestClassesFinder { void testGetClassesCurrent() throws IOException { final var expected = Set.of( "com.github.gtache.fxml.compiler.parsing.impl.TestParsedConstantImpl", + "com.github.gtache.fxml.compiler.parsing.impl.TestParsedCopyImpl", + "com.github.gtache.fxml.compiler.parsing.impl.TestParsedFactoryImpl", "com.github.gtache.fxml.compiler.parsing.impl.TestParsedIncludeImpl", "com.github.gtache.fxml.compiler.parsing.impl.TestParsedObjectImpl", - "com.github.gtache.fxml.compiler.parsing.impl.TestParsedObjectImplBuilder", - "com.github.gtache.fxml.compiler.parsing.impl.TestParsedPropertyImpl"); + "com.github.gtache.fxml.compiler.parsing.impl.TestParsedPropertyImpl", + "com.github.gtache.fxml.compiler.parsing.impl.TestParsedReferenceImpl", + "com.github.gtache.fxml.compiler.parsing.impl.TestParsedTextImpl", + "com.github.gtache.fxml.compiler.parsing.impl.TestParsedValueImpl"); final var actual = ClassesFinder.getClasses("com.github.gtache.fxml.compiler.parsing.impl"); assertEquals(expected, actual); } diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/impl/TestGenerationProgress.java b/core/src/test/java/com/github/gtache/fxml/compiler/impl/TestGenerationProgress.java new file mode 100644 index 0000000..66b6073 --- /dev/null +++ b/core/src/test/java/com/github/gtache/fxml/compiler/impl/TestGenerationProgress.java @@ -0,0 +1,121 @@ +package com.github.gtache.fxml.compiler.impl; + +import com.github.gtache.fxml.compiler.GenerationRequest; +import com.github.gtache.fxml.compiler.impl.internal.GenerationProgress; +import com.github.gtache.fxml.compiler.parsing.ParsedObject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.SequencedCollection; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +class TestGenerationProgress { + + private final GenerationRequest request; + private final Map idToVariableName; + private final Map idToObject; + private final Map variableNameCounters; + private final SequencedCollection controllerFactoryPostAction; + private final StringBuilder sb; + private final GenerationProgress progress; + + TestGenerationProgress(@Mock final GenerationRequest request, @Mock final ParsedObject object) { + this.request = Objects.requireNonNull(request); + this.idToVariableName = new HashMap<>(); + idToVariableName.put("var1", "var2"); + this.idToObject = new HashMap<>(); + idToObject.put("var1", object); + this.variableNameCounters = new HashMap<>(); + variableNameCounters.put("var", new AtomicInteger(0)); + this.controllerFactoryPostAction = new ArrayList<>(); + controllerFactoryPostAction.add("bla"); + this.sb = new StringBuilder("test"); + this.progress = new GenerationProgress(request, idToVariableName, idToObject, variableNameCounters, controllerFactoryPostAction, sb); + } + + @Test + void testGetters() { + assertEquals(request, progress.request()); + assertEquals(idToVariableName, progress.idToVariableName()); + assertEquals(variableNameCounters, progress.variableNameCounters()); + assertEquals(controllerFactoryPostAction, progress.controllerFactoryPostAction()); + assertEquals(sb, progress.stringBuilder()); + } + + @Test + void testConstructorDoesntCopy() { + idToVariableName.clear(); + assertEquals(idToVariableName, progress.idToVariableName()); + + idToObject.clear(); + assertEquals(idToObject, progress.idToObject()); + + variableNameCounters.clear(); + assertEquals(variableNameCounters, progress.variableNameCounters()); + + controllerFactoryPostAction.clear(); + assertEquals(controllerFactoryPostAction, progress.controllerFactoryPostAction()); + + sb.setLength(0); + assertEquals(sb, progress.stringBuilder()); + } + + @Test + void testCanModify() { + progress.idToVariableName().put("var3", "var4"); + assertEquals(idToVariableName, progress.idToVariableName()); + + progress.idToObject().put("var3", mock(ParsedObject.class)); + assertEquals(idToObject, progress.idToObject()); + + progress.variableNameCounters().put("var5", new AtomicInteger(0)); + assertEquals(variableNameCounters, progress.variableNameCounters()); + + progress.controllerFactoryPostAction().add("bla2"); + assertEquals(controllerFactoryPostAction, progress.controllerFactoryPostAction()); + + progress.stringBuilder().append("test2"); + assertEquals(sb, progress.stringBuilder()); + } + + @Test + void testOtherConstructor() { + final var progress2 = new GenerationProgress(request); + assertEquals(request, progress2.request()); + assertEquals(Map.of(), progress2.idToVariableName()); + assertEquals(Map.of(), progress2.variableNameCounters()); + assertEquals(List.of(), progress2.controllerFactoryPostAction()); + assertEquals("", progress2.stringBuilder().toString()); + } + + @Test + void testGetNextVariableName() { + assertEquals("var0", progress.getNextVariableName("var")); + assertEquals("var1", progress.getNextVariableName("var")); + assertEquals("var2", progress.getNextVariableName("var")); + assertEquals("bla0", progress.getNextVariableName("bla")); + } + + @Test + void testIllegal() { + assertThrows(NullPointerException.class, () -> new GenerationProgress(null, idToVariableName, idToObject, variableNameCounters, controllerFactoryPostAction, sb)); + assertThrows(NullPointerException.class, () -> new GenerationProgress(request, null, idToObject, variableNameCounters, controllerFactoryPostAction, sb)); + assertThrows(NullPointerException.class, () -> new GenerationProgress(request, idToVariableName, null, variableNameCounters, controllerFactoryPostAction, sb)); + assertThrows(NullPointerException.class, () -> new GenerationProgress(request, idToVariableName, idToObject, null, controllerFactoryPostAction, sb)); + assertThrows(NullPointerException.class, () -> new GenerationProgress(request, idToVariableName, idToObject, variableNameCounters, null, sb)); + assertThrows(NullPointerException.class, () -> new GenerationProgress(request, idToVariableName, idToObject, variableNameCounters, controllerFactoryPostAction, null)); + } + +} diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/impl/TestConstructorArgs.java b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestConstructorArgs.java similarity index 97% rename from core/src/test/java/com/github/gtache/fxml/compiler/impl/TestConstructorArgs.java rename to core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestConstructorArgs.java index 779ded1..c14c01e 100644 --- a/core/src/test/java/com/github/gtache/fxml/compiler/impl/TestConstructorArgs.java +++ b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestConstructorArgs.java @@ -1,4 +1,4 @@ -package com.github.gtache.fxml.compiler.impl; +package com.github.gtache.fxml.compiler.impl.internal; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/impl/TestParameter.java b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestParameter.java similarity index 94% rename from core/src/test/java/com/github/gtache/fxml/compiler/impl/TestParameter.java rename to core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestParameter.java index 1659a89..20bfd69 100644 --- a/core/src/test/java/com/github/gtache/fxml/compiler/impl/TestParameter.java +++ b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestParameter.java @@ -1,4 +1,4 @@ -package com.github.gtache.fxml.compiler.impl; +package com.github.gtache.fxml.compiler.impl.internal; import org.junit.jupiter.api.Test; diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/impl/TestReflectionHelper.java b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestReflectionHelper.java similarity index 88% rename from core/src/test/java/com/github/gtache/fxml/compiler/impl/TestReflectionHelper.java rename to core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestReflectionHelper.java index de4b5b1..f780f51 100644 --- a/core/src/test/java/com/github/gtache/fxml/compiler/impl/TestReflectionHelper.java +++ b/core/src/test/java/com/github/gtache/fxml/compiler/impl/internal/TestReflectionHelper.java @@ -1,5 +1,7 @@ -package com.github.gtache.fxml.compiler.impl; +package com.github.gtache.fxml.compiler.impl.internal; +import com.github.gtache.fxml.compiler.GenerationException; +import com.github.gtache.fxml.compiler.impl.WholeConstructorArgs; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.ComboBox; @@ -110,4 +112,19 @@ class TestReflectionHelper { final var constructor = Arrays.stream(WholeConstructorArgs.class.getConstructors()).filter(c -> c.getParameterCount() == 3).findFirst().orElseThrow(); assertThrows(IllegalStateException.class, () -> ReflectionHelper.getConstructorArgs(constructor)); } + + @Test + void testGetDefaultPropertyClassNotFound() { + assertThrows(GenerationException.class, () -> ReflectionHelper.getDefaultProperty("bla.bla")); + } + + @Test + void testGetDefaultPropertyNoDefaultProperty() throws GenerationException { + assertNull(ReflectionHelper.getDefaultProperty("java.lang.String")); + } + + @Test + void testGetDefaultProperty() throws GenerationException { + assertEquals("items", ReflectionHelper.getDefaultProperty("javafx.scene.control.ListView")); + } } diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedCopyImpl.java b/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedCopyImpl.java new file mode 100644 index 0000000..12f5881 --- /dev/null +++ b/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedCopyImpl.java @@ -0,0 +1,52 @@ +package com.github.gtache.fxml.compiler.parsing.impl; + +import com.github.gtache.fxml.compiler.parsing.ParsedCopy; +import com.github.gtache.fxml.compiler.parsing.ParsedProperty; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.LinkedHashMap; +import java.util.SequencedMap; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class TestParsedCopyImpl { + + private final SequencedMap properties; + private final ParsedCopy copy; + + TestParsedCopyImpl(@Mock final ParsedProperty property) { + this.properties = new LinkedHashMap<>(); + this.properties.put("name", property); + this.copy = new ParsedCopyImpl(properties); + } + + @Test + void testGetters() { + assertEquals(properties, copy.attributes()); + assertEquals(ParsedCopy.class.getName(), copy.className()); + assertEquals(new LinkedHashMap<>(), copy.properties()); + } + + @Test + void testCopyMap() { + final var originalProperties = copy.attributes(); + properties.clear(); + assertEquals(originalProperties, copy.attributes()); + assertNotEquals(properties, copy.attributes()); + } + + @Test + void testUnmodifiable() { + final var objectProperties = copy.attributes(); + assertThrows(UnsupportedOperationException.class, objectProperties::clear); + } + + @Test + void testIllegal() { + assertThrows(NullPointerException.class, () -> new TestParsedCopyImpl(null)); + } +} diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedFactoryImpl.java b/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedFactoryImpl.java new file mode 100644 index 0000000..ce829db --- /dev/null +++ b/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedFactoryImpl.java @@ -0,0 +1,90 @@ +package com.github.gtache.fxml.compiler.parsing.impl; + +import com.github.gtache.fxml.compiler.parsing.ParsedDefine; +import com.github.gtache.fxml.compiler.parsing.ParsedFactory; +import com.github.gtache.fxml.compiler.parsing.ParsedObject; +import com.github.gtache.fxml.compiler.parsing.ParsedProperty; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SequencedCollection; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class TestParsedFactoryImpl { + + private final String className; + private final Map attributes; + private final SequencedCollection arguments; + private final SequencedCollection children; + private final ParsedFactory factory; + + TestParsedFactoryImpl(@Mock final ParsedObject object1, @Mock final ParsedObject object2, @Mock final ParsedDefine define) { + this.className = "test"; + this.attributes = new HashMap<>(Map.of("fx:factory", new ParsedPropertyImpl("fx:factory", String.class.getName(), "value"))); + this.arguments = new ArrayList<>(List.of(object1, object2)); + this.children = new ArrayList<>(List.of(define)); + this.factory = new ParsedFactoryImpl(className, attributes, arguments, children); + } + + @Test + void testGetters() { + assertEquals(className, factory.className()); + assertEquals(attributes, factory.attributes()); + assertEquals(attributes.get("fx:factory").value(), factory.factory()); + assertEquals(arguments, factory.arguments()); + assertEquals(children, factory.children()); + assertEquals(Map.of(), factory.properties()); + } + + @Test + void testCopyMap() { + final var originalAttributes = factory.attributes(); + attributes.clear(); + assertEquals(originalAttributes, factory.attributes()); + assertNotEquals(attributes, factory.attributes()); + } + + @Test + void testCopyArguments() { + final var originalArguments = factory.arguments(); + arguments.clear(); + assertEquals(originalArguments, factory.arguments()); + assertNotEquals(arguments, factory.arguments()); + } + + @Test + void testCopyDefines() { + final var originalDefines = factory.children(); + children.clear(); + assertEquals(originalDefines, factory.children()); + assertNotEquals(children, factory.children()); + } + + @Test + void testUnmodifiable() { + final var objectProperties = factory.attributes(); + assertThrows(UnsupportedOperationException.class, objectProperties::clear); + + final var objectArguments = factory.arguments(); + assertThrows(UnsupportedOperationException.class, objectArguments::clear); + + final var objectDefines = factory.children(); + assertThrows(UnsupportedOperationException.class, objectDefines::clear); + } + + @Test + void testIllegal() { + assertThrows(NullPointerException.class, () -> new ParsedFactoryImpl(null, attributes, arguments, children)); + assertThrows(NullPointerException.class, () -> new ParsedFactoryImpl(className, null, arguments, children)); + assertThrows(NullPointerException.class, () -> new ParsedFactoryImpl(className, attributes, null, children)); + assertThrows(NullPointerException.class, () -> new ParsedFactoryImpl(className, attributes, arguments, null)); + } +} diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedObjectImpl.java b/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedObjectImpl.java index 856cbfd..c92a503 100644 --- a/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedObjectImpl.java +++ b/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedObjectImpl.java @@ -1,5 +1,6 @@ package com.github.gtache.fxml.compiler.parsing.impl; +import com.github.gtache.fxml.compiler.parsing.ParsedDefine; import com.github.gtache.fxml.compiler.parsing.ParsedObject; import com.github.gtache.fxml.compiler.parsing.ParsedProperty; import org.junit.jupiter.api.Test; @@ -7,6 +8,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.SequencedCollection; @@ -19,48 +21,57 @@ import static org.junit.jupiter.api.Assertions.assertThrows; class TestParsedObjectImpl { private final String clazz; - private final SequencedMap properties; - private final SequencedMap> children; + private final SequencedMap attributes; + private final SequencedMap> properties; + private final SequencedCollection objects; private final ParsedObject parsedObject; - TestParsedObjectImpl(@Mock final ParsedProperty property, @Mock final ParsedObject object) { + TestParsedObjectImpl(@Mock final ParsedProperty property, @Mock final ParsedObject object, @Mock final ParsedDefine define) { this.clazz = Object.class.getName(); + this.attributes = new LinkedHashMap<>(); + this.attributes.put("name", property); this.properties = new LinkedHashMap<>(); - this.properties.put("name", property); - this.children = new LinkedHashMap<>(); - this.children.put(property, List.of(object)); - this.parsedObject = new ParsedObjectImpl(clazz, properties, children); + this.properties.put(property, List.of(object)); + this.objects = new ArrayList<>(List.of(define)); + this.parsedObject = new ParsedObjectImpl(clazz, attributes, properties, objects); } @Test void testGetters() { assertEquals(clazz, parsedObject.className()); - assertEquals(properties, parsedObject.attributes()); - assertEquals(children, parsedObject.properties()); + assertEquals(attributes, parsedObject.attributes()); + assertEquals(properties, parsedObject.properties()); + assertEquals(objects, parsedObject.children()); } @Test void testCopyMap() { - final var originalProperties = parsedObject.attributes(); - final var originalChildren = parsedObject.properties(); + final var originalAttributes = parsedObject.attributes(); + final var originalProperties = parsedObject.properties(); + final var originalChildren = parsedObject.children(); + attributes.clear(); properties.clear(); - children.clear(); - assertEquals(originalProperties, parsedObject.attributes()); - assertEquals(originalChildren, parsedObject.properties()); + objects.clear(); + assertEquals(originalAttributes, parsedObject.attributes()); + assertEquals(originalProperties, parsedObject.properties()); + assertEquals(originalChildren, parsedObject.children()); } @Test void testUnmodifiable() { - final var objectProperties = parsedObject.attributes(); - final var objectChildren = parsedObject.properties(); + final var objectAttributes = parsedObject.attributes(); + final var objectProperties = parsedObject.properties(); + final var objectChildren = parsedObject.children(); + assertThrows(UnsupportedOperationException.class, objectAttributes::clear); assertThrows(UnsupportedOperationException.class, objectProperties::clear); assertThrows(UnsupportedOperationException.class, objectChildren::clear); } @Test void testIllegal() { - assertThrows(NullPointerException.class, () -> new ParsedObjectImpl(null, properties, children)); - assertThrows(NullPointerException.class, () -> new ParsedObjectImpl(clazz, null, children)); - assertThrows(NullPointerException.class, () -> new ParsedObjectImpl(clazz, properties, null)); + assertThrows(NullPointerException.class, () -> new ParsedObjectImpl(null, attributes, properties, objects)); + assertThrows(NullPointerException.class, () -> new ParsedObjectImpl(clazz, null, properties, objects)); + assertThrows(NullPointerException.class, () -> new ParsedObjectImpl(clazz, attributes, null, objects)); + assertThrows(NullPointerException.class, () -> new ParsedObjectImpl(clazz, attributes, properties, null)); } } diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedObjectImplBuilder.java b/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedObjectImplBuilder.java deleted file mode 100644 index 06e09d9..0000000 --- a/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedObjectImplBuilder.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.github.gtache.fxml.compiler.parsing.impl; - -import com.github.gtache.fxml.compiler.parsing.ParsedObject; -import com.github.gtache.fxml.compiler.parsing.ParsedProperty; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class TestParsedObjectImplBuilder { - - private final String clazz1; - private final String clazz2; - private final ParsedProperty property1; - private final ParsedProperty property2; - private final ParsedObject object1; - private final ParsedObject object2; - private final ParsedObjectImpl.Builder builder; - - TestParsedObjectImplBuilder(@Mock final ParsedProperty property1, @Mock final ParsedProperty property2, - @Mock final ParsedObject object1, @Mock final ParsedObject object2) { - this.clazz1 = Object.class.getName(); - this.clazz2 = String.class.getName(); - this.property1 = Objects.requireNonNull(property1); - this.property2 = Objects.requireNonNull(property2); - this.object1 = Objects.requireNonNull(object1); - this.object2 = Objects.requireNonNull(object2); - this.builder = new ParsedObjectImpl.Builder(); - } - - @BeforeEach - void beforeEach() { - when(property1.name()).thenReturn("property1"); - when(property2.name()).thenReturn("property2"); - } - - @Test - void testBuildNullClass() { - assertThrows(NullPointerException.class, builder::build); - } - - @Test - void testClassName() { - builder.className(clazz1); - final var built = builder.build(); - assertEquals(clazz1, built.className()); - assertEquals(Map.of(), built.attributes()); - assertEquals(Map.of(), built.properties()); - } - - @Test - void testOverwriteClassName() { - builder.className(clazz1); - builder.className(clazz2); - final var built = builder.build(); - assertEquals(clazz2, built.className()); - assertEquals(Map.of(), built.attributes()); - assertEquals(Map.of(), built.properties()); - } - - @Test - void testAddAttribute() { - builder.className(clazz1); - builder.addAttribute(property1); - final var built = builder.build(); - assertEquals(Map.of(property1.name(), property1), built.attributes()); - assertEquals(Map.of(), built.properties()); - } - - @Test - void testAddMultipleAttributes() { - builder.className(clazz1); - builder.addAttribute(property1); - builder.addAttribute(property2); - final var built = builder.build(); - assertEquals(Map.of(property1.name(), property1, property2.name(), property2), built.attributes()); - assertEquals(Map.of(), built.properties()); - } - - @Test - void testAddProperty() { - builder.className(clazz1); - builder.addProperty(property1, object1); - final var built = builder.build(); - assertEquals(Map.of(), built.attributes()); - assertEquals(Map.of(property1, List.of(object1)), built.properties()); - } - - @Test - void testAddMultipleProperties() { - builder.className(clazz1); - builder.addProperty(property1, object1); - builder.addProperty(property2, object2); - final var built = builder.build(); - assertEquals(Map.of(), built.attributes()); - assertEquals(Map.of(property1, List.of(object1), property2, List.of(object2)), built.properties()); - } -} \ No newline at end of file diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedReferenceImpl.java b/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedReferenceImpl.java new file mode 100644 index 0000000..412aed5 --- /dev/null +++ b/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedReferenceImpl.java @@ -0,0 +1,52 @@ +package com.github.gtache.fxml.compiler.parsing.impl; + +import com.github.gtache.fxml.compiler.parsing.ParsedProperty; +import com.github.gtache.fxml.compiler.parsing.ParsedReference; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.LinkedHashMap; +import java.util.SequencedMap; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class TestParsedReferenceImpl { + + private final SequencedMap properties; + private final ParsedReference reference; + + TestParsedReferenceImpl(@Mock final ParsedProperty property) { + this.properties = new LinkedHashMap<>(); + this.properties.put("name", property); + this.reference = new ParsedReferenceImpl(properties); + } + + @Test + void testGetters() { + assertEquals(properties, reference.attributes()); + assertEquals(ParsedReference.class.getName(), reference.className()); + assertEquals(new LinkedHashMap<>(), reference.properties()); + } + + @Test + void testCopyMap() { + final var originalProperties = reference.attributes(); + properties.clear(); + assertEquals(originalProperties, reference.attributes()); + assertNotEquals(properties, reference.attributes()); + } + + @Test + void testUnmodifiable() { + final var objectProperties = reference.attributes(); + assertThrows(UnsupportedOperationException.class, objectProperties::clear); + } + + @Test + void testIllegal() { + assertThrows(NullPointerException.class, () -> new ParsedReferenceImpl(null)); + } +} diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedTextImpl.java b/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedTextImpl.java new file mode 100644 index 0000000..1ecc7cd --- /dev/null +++ b/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedTextImpl.java @@ -0,0 +1,28 @@ +package com.github.gtache.fxml.compiler.parsing.impl; + +import com.github.gtache.fxml.compiler.parsing.ParsedText; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TestParsedTextImpl { + + private final String text; + private final ParsedText parsedText; + + TestParsedTextImpl() { + this.text = "text"; + this.parsedText = new ParsedTextImpl(text); + } + + @Test + void testText() { + assertEquals(text, parsedText.text()); + } + + @Test + void testIllegal() { + assertThrows(NullPointerException.class, () -> new ParsedTextImpl(null)); + } +} diff --git a/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedValueImpl.java b/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedValueImpl.java new file mode 100644 index 0000000..68657b6 --- /dev/null +++ b/core/src/test/java/com/github/gtache/fxml/compiler/parsing/impl/TestParsedValueImpl.java @@ -0,0 +1,51 @@ +package com.github.gtache.fxml.compiler.parsing.impl; + +import com.github.gtache.fxml.compiler.parsing.ParsedProperty; +import com.github.gtache.fxml.compiler.parsing.ParsedValue; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class TestParsedValueImpl { + + private final String className; + private final Map attributes; + private final ParsedValue value; + + TestParsedValueImpl() { + this.className = "test"; + this.attributes = new HashMap<>(Map.of("fx:value", new ParsedPropertyImpl("fx:value", String.class.getName(), "value"))); + this.value = new ParsedValueImpl(className, attributes); + } + + @Test + void testGetters() { + assertEquals(className, value.className()); + assertEquals(attributes, value.attributes()); + assertEquals(attributes.get("fx:value").value(), value.value()); + assertEquals(Map.of(), value.properties()); + } + + @Test + void testCopyMap() { + final var originalAttributes = value.attributes(); + attributes.clear(); + assertEquals(originalAttributes, value.attributes()); + assertNotEquals(attributes, value.attributes()); + } + + @Test + void testUnmodifiable() { + final var objectProperties = value.attributes(); + assertThrows(UnsupportedOperationException.class, objectProperties::clear); + } + + @Test + void testIllegal() { + assertThrows(NullPointerException.class, () -> new ParsedValueImpl(null, attributes)); + assertThrows(NullPointerException.class, () -> new ParsedValueImpl(className, null)); + } +} diff --git a/loader/pom.xml b/loader/pom.xml deleted file mode 100644 index b7a855d..0000000 --- a/loader/pom.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - 4.0.0 - - com.github.gtache - fxml-compiler - 1.0-SNAPSHOT - - - fxml-compiler-loader - - - - com.github.gtache - fxml-compiler-core - - - org.openjfx - javafx-fxml - ${javafx.version} - - - - org.openjfx - javafx-media - ${javafx.version} - test - - - org.openjfx - javafx-web - ${javafx.version} - test - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - false - - - - - \ No newline at end of file diff --git a/loader/src/main/java/com/github/gtache/fxml/compiler/parsing/listener/LoadListenerParser.java b/loader/src/main/java/com/github/gtache/fxml/compiler/parsing/listener/LoadListenerParser.java deleted file mode 100644 index 7ffdb29..0000000 --- a/loader/src/main/java/com/github/gtache/fxml/compiler/parsing/listener/LoadListenerParser.java +++ /dev/null @@ -1,306 +0,0 @@ -package com.github.gtache.fxml.compiler.parsing.listener; - -import com.github.gtache.fxml.compiler.parsing.FXMLParser; -import com.github.gtache.fxml.compiler.parsing.ParseException; -import com.github.gtache.fxml.compiler.parsing.ParsedObject; -import com.github.gtache.fxml.compiler.parsing.ParsedProperty; -import com.github.gtache.fxml.compiler.parsing.impl.ParsedIncludeImpl; -import com.github.gtache.fxml.compiler.parsing.impl.ParsedObjectImpl; -import com.github.gtache.fxml.compiler.parsing.impl.ParsedPropertyImpl; -import javafx.application.Platform; -import javafx.collections.ObservableList; -import javafx.event.EventHandler; -import javafx.fxml.FXMLLoader; -import javafx.fxml.LoadListener; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.net.MalformedURLException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.SequencedMap; -import java.util.concurrent.CompletableFuture; - -/** - * {@link LoadListener} implementation parsing the FXML file to {@link ParsedObject} - */ -public class LoadListenerParser implements LoadListener, FXMLParser { - - private static final Logger logger = LogManager.getLogger(LoadListenerParser.class); - - private final Deque stack; - private final Deque propertyStack; - private final Deque> currentObjectStack; - private final SequencedMap currentIncludeProperties; - private ParsedObjectImpl.Builder current; - private ParsedProperty currentProperty; - private List currentObjects; - private Object previousEnd; - private boolean isInclude; - - /** - * Instantiates the listener - */ - public LoadListenerParser() { - this.stack = new ArrayDeque<>(); - this.propertyStack = new ArrayDeque<>(); - this.currentObjectStack = new ArrayDeque<>(); - this.currentObjects = new ArrayList<>(); - this.currentIncludeProperties = new LinkedHashMap<>(); - } - - /** - * @return The parsed root - */ - ParsedObject root() { - if (currentObjects != null && currentObjects.size() == 1) { - return currentObjects.getFirst(); - } else { - throw new IllegalStateException("Expected 1 root object, found " + currentObjects); - } - } - - - @Override - public void readImportProcessingInstruction(final String target) { - logger.debug("Import processing instruction : {}", target); - previousEnd = null; - //Do nothing - } - - @Override - public void readLanguageProcessingInstruction(final String language) { - logger.debug("Language processing instruction : {}", language); - previousEnd = null; - //Do nothing - } - - @Override - public void readComment(final String comment) { - logger.debug("Comment : {}", comment); - //Do nothing - } - - @Override - public void beginInstanceDeclarationElement(final Class type) { - logger.debug("Instance declaration : {}", type); - previousEnd = null; - if (current != null) { - stack.push(current); - } - current = new ParsedObjectImpl.Builder(); - current.className(type.getName()); - } - - @Override - public void beginUnknownTypeElement(final String name) { - logger.debug("Unknown type : {}", name); - throw new IllegalArgumentException("Unknown type : " + name); - } - - @Override - public void beginIncludeElement() { - logger.debug("Include"); - previousEnd = null; - if (isInclude) { - throw new IllegalStateException("Nested include"); - } else if (!currentIncludeProperties.isEmpty()) { - throw new IllegalStateException("Include properties not empty"); - } - isInclude = true; - } - - @Override - public void beginReferenceElement() { - logger.debug("Reference"); - throw new UnsupportedOperationException("Reference not supported yet"); - } - - @Override - public void beginCopyElement() { - logger.debug("Copy"); - throw new UnsupportedOperationException("Copy not supported yet"); - } - - @Override - public void beginRootElement() { - logger.debug("Root element"); - throw new UnsupportedOperationException("Root element not supported yet"); - } - - @Override - public void beginPropertyElement(final String name, final Class sourceType) { - logger.debug("Property ({}): {}", sourceType, name); - previousEnd = null; - if (isInclude) { - throw new IllegalStateException("Reading complex property for include"); - } else { - if (currentProperty != null) { - propertyStack.push(currentProperty); - } - currentProperty = new ParsedPropertyImpl(name, sourceType == null ? null : sourceType.getName(), null); - currentObjectStack.push(currentObjects); - currentObjects = new ArrayList<>(); - } - } - - @Override - public void beginUnknownStaticPropertyElement(final String name) { - logger.debug("Unknown static property : {}", name); - throw new IllegalArgumentException("Unknown static property : " + name); - } - - @Override - public void beginScriptElement() { - logger.debug("Script"); - throw new UnsupportedOperationException("Script not supported yet"); - } - - @Override - public void beginDefineElement() { - logger.debug("Define"); - throw new UnsupportedOperationException("Define not supported yet"); - } - - @Override - public void readInternalAttribute(final String name, final String value) { - logger.debug("Internal attribute : {} = {}", name, value); - previousEnd = null; - final var property = new ParsedPropertyImpl(name, null, value); - if (isInclude) { - currentIncludeProperties.put(name, property); - } else if (current == null) { - throw new IllegalStateException("Current object is null (trying to add attribute " + name + " = " + value + ")"); - } else { - current.addAttribute(property); - } - } - - @Override - public void readPropertyAttribute(final String name, final Class sourceType, final String value) { - logger.debug("Property ({}): {} = {}", sourceType, name, value); - if (isInclude) { - throw new IllegalStateException("Reading complex property for include"); - } else if (current == null) { - throw new IllegalStateException("Current object is null (trying to add property " + name + "/" + sourceType + " = " + value + ")"); - } else { - previousEnd = null; - current.addAttribute(new ParsedPropertyImpl(name, sourceType == null ? null : sourceType.getName(), value)); - } - } - - @Override - public void readUnknownStaticPropertyAttribute(final String name, final String value) { - logger.debug("Unknown static property attribute : {} = {}", name, value); - throw new IllegalArgumentException("Unknown static property : " + name); - } - - @Override - public void readEventHandlerAttribute(final String name, final String value) { - logger.debug("Event handler attribute : {} = {}", name, value); - if (isInclude) { - throw new IllegalStateException("Reading event handler for include"); - } else if (current == null) { - throw new IllegalStateException("Current object is null (trying to add event handler" + name + " = " + value + ")"); - } else { - current.addAttribute(new ParsedPropertyImpl(name, EventHandler.class.getName(), value)); - } - } - - @Override - public void endElement(final Object value) { - logger.debug("End element : {}", value); - if (isInclude) { - endInclude(); - } else if (previousEnd == value || value instanceof ObservableList) { - endProperty(value); - } else if (current != null) { - endObject(value); - } else { - throw new IllegalStateException("Unexpected end element (current is null) : " + value); - } - } - - private void endInclude() { - currentObjects.add(new ParsedIncludeImpl(currentIncludeProperties)); - currentIncludeProperties.clear(); - isInclude = false; - } - - private void endProperty(final Object value) { - //End of property - if (currentProperty == null) { - throw new IllegalStateException("Unexpected end element (property is null) : " + value); - } else { - currentObjects.forEach(go -> current.addProperty(currentProperty, go)); - currentObjects = currentObjectStack.isEmpty() ? new ArrayList<>() : currentObjectStack.pop(); - currentProperty = propertyStack.isEmpty() ? null : propertyStack.pop(); - } - } - - private void endObject(final Object value) { - final var built = current.build(); - currentObjects.add(built); - current = stack.isEmpty() ? null : stack.pop(); - previousEnd = value; - } - - @Override - public ParsedObject parse(final String content) throws ParseException { - Path path = null; - try { - path = Files.createTempFile("temp", ".fxml"); - Files.writeString(path, content); - return parse(path); - } catch (final IOException e) { - throw new ParseException("Error creating temp file", e); - } finally { - if (path != null) { - try { - Files.deleteIfExists(path); - } catch (final IOException ignored) { - } - } - } - } - - private void reset() { - current = null; - stack.clear(); - propertyStack.clear(); - currentObjects.clear(); - currentObjectStack.clear(); - currentIncludeProperties.clear(); - currentProperty = null; - isInclude = false; - previousEnd = null; - } - - @Override - public ParsedObject parse(final Path path) throws ParseException { - reset(); - try { - final var url = path.toUri().toURL(); - logger.info("Parsing {}", url); - return CompletableFuture.supplyAsync(() -> { - try { - final var loader = new FXMLLoader(url); - loader.setLoadListener(this); - loader.load(); - return root(); - } catch (final IOException e) { - throw new UncheckedIOException(e); - } - }, Platform::runLater).join(); - } catch (final MalformedURLException | RuntimeException e) { - throw new ParseException("Error parsing " + path, e); - } - } -} diff --git a/loader/src/main/java/module-info.java b/loader/src/main/java/module-info.java deleted file mode 100644 index e53e0c6..0000000 --- a/loader/src/main/java/module-info.java +++ /dev/null @@ -1,10 +0,0 @@ -/** - * FXML LoadListener module for FXML compiler - */ -module com.github.gtache.fxml.compiler.loader { - requires transitive com.github.gtache.fxml.compiler.core; - requires transitive javafx.fxml; - requires org.apache.logging.log4j; - - exports com.github.gtache.fxml.compiler.parsing.listener; -} \ No newline at end of file diff --git a/loader/src/test/java/com/github/gtache/fxml/compiler/parsing/listener/ControlsController.java b/loader/src/test/java/com/github/gtache/fxml/compiler/parsing/listener/ControlsController.java deleted file mode 100644 index 4230608..0000000 --- a/loader/src/test/java/com/github/gtache/fxml/compiler/parsing/listener/ControlsController.java +++ /dev/null @@ -1,489 +0,0 @@ -package com.github.gtache.fxml.compiler.parsing.listener; - -import javafx.fxml.FXML; -import javafx.scene.control.*; -import javafx.scene.image.ImageView; -import javafx.scene.layout.GridPane; -import javafx.scene.media.MediaView; -import javafx.scene.paint.Color; -import javafx.scene.web.HTMLEditor; -import javafx.scene.web.WebView; - -public class ControlsController { - @FXML - GridPane gridPane; - @FXML - Button button; - @FXML - CheckBox checkBox; - @FXML - ChoiceBox choiceBox; - @FXML - ColorPicker colorPicker; - @FXML - ComboBox comboBox; - @FXML - DatePicker datePicker; - @FXML - HTMLEditor htmlEditor; - @FXML - Hyperlink hyperlink; - @FXML - ImageView imageView; - @FXML - Label label; - @FXML - ListView