diff --git a/README.md b/README.md index b6a9cf912..91203998a 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ ![image](http://www.buildpath.de/mvvm/mvvmfx.png) -__mvvm(fx)__ is an application framework which provides you necessary components to implement the [MVVM](../../wiki/MVVM "MVVM") pattern with JavaFX. +**mvvmFX** is an application framework which provides you necessary components to implement the [MVVM](../../wiki/MVVM "MVVM") pattern with JavaFX. -__MVVM__ is the enhanced version of the [Presentation Model](http://martinfowler.com/eaaDev/PresentationModel.html "Presentation Model") pattern and was created by Microsoft engineers for [WPF](http://msdn.microsoft.com/en-us/library/ms754130.aspx "WPF") . JavaFX and WPF does have similarities like Databinding and descriptive UI declaration (FXML/XAML). Because of this fact we adopt best practices of the development with the Microsoft technology. +**MVVM** is the enhanced version of the [Presentation Model](http://martinfowler.com/eaaDev/PresentationModel.html "Presentation Model") pattern and was created by Microsoft engineers for [WPF](http://msdn.microsoft.com/en-us/library/ms754130.aspx "WPF"). JavaFX and WPF does have similarities like data binding and descriptive UI declaration (FXML/XAML). Because of this fact we adopted best practices of the development with the Microsoft technology and introduced new helpers to support the development of applications with JavaFX and MVVM. [![Commercial Support](https://img.shields.io/badge/Commercial%20Support%20-by%20Saxonia%20Systems-brightgreen.svg)](http://goo.gl/forms/WVBG3SWHuL) [![Build Status](https://api.travis-ci.org/sialcasa/mvvmFX.svg?branch=develop)](https://travis-ci.org/sialcasa/mvvmFX) -###[Howto](../../wiki "Howto")### +### [Howto](../../wiki "Howto") -### Maven dependency### +### Maven dependency #### Stable Release @@ -20,7 +20,7 @@ This is the stable release that can be used in production. de.saxsys mvvmfx - 1.6.0 + 1.7.0 ``` @@ -32,7 +32,7 @@ Here we make bug fixes for the current stable release. de.saxsys mvvmfx - 1.6.1-SNAPSHOT + 1.7.1-SNAPSHOT ``` @@ -44,21 +44,23 @@ Here we develop new features. This release is unstable and shouldn't be used in de.saxsys mvvmfx - 1.7.0-SNAPSHOT + 1.8.0-SNAPSHOT ``` ### Get Help -If you need help you can use the forums on [Google Groups](https://groups.google.com/forum/#!forum/mvvmfx-dev) for asking questions and interacting with the mvvmFX developers. Additionally you can create issues, report bugs and add feature requests on the issue tracker at [github](https://github.com/sialcasa/mvvmFX/issues). +The best way to get help with mvvmFX is to either ask questions on StackOverflow using the [tag "mvvmfx"](https://stackoverflow.com/questions/tagged/mvvmfx) or to use our [Google Groups](https://groups.google.com/forum/#!forum/mvvmfx-dev) mailing list. Additionally you can create issues, report bugs and add feature requests on the issue tracker at [github](https://github.com/sialcasa/mvvmFX/issues). ### Links - [Project Page](http://sialcasa.github.io/mvvmFX/) -- [javadoc mvvmfx core](http://sialcasa.github.io/mvvmFX/javadoc/1.5.0/mvvmfx/) -- [javadoc mvvmfx-cdi](http://sialcasa.github.io/mvvmFX/javadoc/1.5.0/mvvmfx-cdi/) -- [javadoc mvvmfx-guice](http://sialcasa.github.io/mvvmFX/javadoc/1.5.0/mvvmfx-guice/) -- [javadoc mvvmfx-utils](http://sialcasa.github.io/mvvmFX/javadoc/1.5.0/mvvmfx-utils/) -- [javadoc mvvmfx-testing-utils](http://sialcasa.github.io/mvvmFX/javadoc/1.5.0/mvvmfx-testing-utils/) +- [javadoc mvvmfx core](http://sialcasa.github.io/mvvmFX/javadoc/1.7.0/mvvmfx/) +- [javadoc mvvmfx-cdi](http://sialcasa.github.io/mvvmFX/javadoc/1.7.0/mvvmfx-cdi/) +- [javadoc mvvmfx-guice](http://sialcasa.github.io/mvvmFX/javadoc/1.7.0/mvvmfx-guice/) +- [javadoc mvvmfx-easydi](http://sialcasa.github.io/mvvmFX/javadoc/1.7.0/mvvmfx-easydi/) +- [javadoc mvvmfx-validation](http://sialcasa.github.io/mvvmFX/javadoc/1.7.0/mvvmfx-validation/) +- [javadoc mvvmfx-utils](http://sialcasa.github.io/mvvmFX/javadoc/1.7.0/mvvmfx-utils/) +- [javadoc mvvmfx-testing-utils](http://sialcasa.github.io/mvvmFX/javadoc/1.7.0/mvvmfx-testing-utils/) diff --git a/examples/books-example/pom.xml b/examples/books-example/pom.xml index d6c97a89e..cf8880c8d 100644 --- a/examples/books-example/pom.xml +++ b/examples/books-example/pom.xml @@ -5,7 +5,7 @@ de.saxsys.mvvmfx examples - 1.6.0 + 1.7.0 4.0.0 @@ -88,7 +88,11 @@ junit test - + + org.junit.jupiter + junit-jupiter-api + test + org.assertj assertj-core diff --git a/examples/books-example/src/test/java/de/saxsys/mvvmfx/examples/books/MainViewModelTest.java b/examples/books-example/src/test/java/de/saxsys/mvvmfx/examples/books/MainViewModelTest.java index 267f0f0a0..48ccf479c 100644 --- a/examples/books-example/src/test/java/de/saxsys/mvvmfx/examples/books/MainViewModelTest.java +++ b/examples/books-example/src/test/java/de/saxsys/mvvmfx/examples/books/MainViewModelTest.java @@ -3,8 +3,8 @@ import de.saxsys.mvvmfx.examples.books.backend.Error; import de.saxsys.mvvmfx.examples.books.backend.Book; import de.saxsys.mvvmfx.examples.books.backend.LibraryService; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.Collections; @@ -19,7 +19,7 @@ public class MainViewModelTest { private MainViewModel viewModel; private LibraryService libraryService; - @Before + @BeforeEach public void setup() { libraryService = mock(LibraryService.class); diff --git a/examples/books-example/src/test/java/de/saxsys/mvvmfx/examples/books/backend/LibraryServiceMockTest.java b/examples/books-example/src/test/java/de/saxsys/mvvmfx/examples/books/backend/LibraryServiceMockTest.java index 77d59fd05..4b926f4a3 100644 --- a/examples/books-example/src/test/java/de/saxsys/mvvmfx/examples/books/backend/LibraryServiceMockTest.java +++ b/examples/books-example/src/test/java/de/saxsys/mvvmfx/examples/books/backend/LibraryServiceMockTest.java @@ -1,7 +1,7 @@ package de.saxsys.mvvmfx.examples.books.backend; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.List; @@ -18,7 +18,7 @@ public class LibraryServiceMockTest { private Book theMetamorphosis; - @Before + @BeforeEach public void setup() { libraryService = new LibraryServiceMockImpl(); diff --git a/examples/contacts-example/pom.xml b/examples/contacts-example/pom.xml index 9542d4362..3a16299e8 100644 --- a/examples/contacts-example/pom.xml +++ b/examples/contacts-example/pom.xml @@ -6,7 +6,7 @@ de.saxsys.mvvmfx examples - 1.6.0 + 1.7.0 contacts-example @@ -48,12 +48,16 @@ de.saxsys mvvmfx-cdi - + + org.jboss.weld.se + weld-se-core + org.jboss jandex - 1.2.4.Final + 2.0.3.Final + ch.qos.logback logback-classic @@ -89,8 +93,8 @@ - junit - junit + org.junit.jupiter + junit-jupiter-api test diff --git a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/Countries.java b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/Countries.java new file mode 100644 index 000000000..09f6ed731 --- /dev/null +++ b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/Countries.java @@ -0,0 +1,20 @@ +package de.saxsys.mvvmfx.examples.contacts.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; + +@XmlRootElement(name = "iso_3166_entries") +@XmlAccessorType(XmlAccessType.FIELD) +public class Countries { + + @XmlElement(name = "iso_3166_entry") + private ArrayList countries; + + public ArrayList getCountries() { + return countries; + } + +} diff --git a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/CountrySelector.java b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/CountrySelector.java new file mode 100644 index 000000000..f572ea867 --- /dev/null +++ b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/CountrySelector.java @@ -0,0 +1,34 @@ +package de.saxsys.mvvmfx.examples.contacts.model.countries; + +import de.saxsys.mvvmfx.examples.contacts.model.Country; +import de.saxsys.mvvmfx.examples.contacts.model.Subdivision; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.collections.ObservableList; + +/** + * Implementations of this interface are used to encapsulate the process of loading available countries + * and their subdivisions (if available). + * + * Instances are meant to be a stateful wrapper around the existing countries. + * You should create an instance of this class, call the {@link #init()} method + * and then bind the UI to the provided observable lists ( + * {@link #availableCountries()} and {@link #subdivisions()}). + * + * To choose a country use the {@link #setCountry(Country)} method. This + * will lead to a change of the {@link #subdivisions()} list. + */ +public interface CountrySelector { + + void init(); + + void setCountry(Country country); + + ObservableList availableCountries(); + + ReadOnlyStringProperty subdivisionLabel(); + + ObservableList subdivisions(); + + ReadOnlyBooleanProperty inProgressProperty(); +} diff --git a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/CountrySelector.java b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/DataFxCountrySelector.java similarity index 78% rename from examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/CountrySelector.java rename to examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/DataFxCountrySelector.java index 751522c79..6011d7816 100644 --- a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/CountrySelector.java +++ b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/DataFxCountrySelector.java @@ -1,5 +1,7 @@ -package de.saxsys.mvvmfx.examples.contacts.model; +package de.saxsys.mvvmfx.examples.contacts.model.countries; +import de.saxsys.mvvmfx.examples.contacts.model.Country; +import de.saxsys.mvvmfx.examples.contacts.model.Subdivision; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyStringProperty; @@ -15,36 +17,41 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.xml.bind.annotation.*; +import javax.annotation.PostConstruct; +import javax.enterprise.inject.Alternative; +import javax.inject.Singleton; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * This class is used to encapsulate the process of loading available countries - * and there subdivisions (if available). + * and their subdivisions (if available). * * This class is meant to be a stateful wrapper around the existing countries. * You should create an instance of this class, call the {@link #init()} method * and then bind the UI to the provided observable lists ( * {@link #availableCountries()} and {@link #subdivisions()}). * - * To choose a country have to use the {@link #setCountry(Country)} method. This + * To choose a country use the {@link #setCountry(Country)} method. This * will lead to a change of the {@link #subdivisions()} list. * * - * At the moment this class used two XML files ({@link #ISO_3166_LOCATION} and + * At the moment this class uses two XML files ({@link #ISO_3166_LOCATION} and * {@link #ISO_3166_2_LOCATION}) that contain information about countries, * country-codes and subdivisions according to ISO 3166 and ISO 3166-2. * * The loading process is implemented with the DataFX framework. */ -public class CountrySelector { +@Singleton +@Alternative +public class DataFxCountrySelector implements CountrySelector { - private static final Logger LOG = LoggerFactory.getLogger(CountrySelector.class); + private static final Logger LOG = LoggerFactory.getLogger(DataFxCountrySelector.class); public static final String ISO_3166_LOCATION = "/countries/iso_3166.xml"; public static final String ISO_3166_2_LOCATION = "/countries/iso_3166_2.xml"; @@ -62,6 +69,8 @@ public class CountrySelector { * This method triggers the loading of the available countries and * subdivisions. */ + @PostConstruct + @Override public void init() { inProgress.set(true); loadCountries(); @@ -75,6 +84,7 @@ public void init() { * @param country the country that will be selected or null if * no country is selected. */ + @Override public void setCountry(Country country) { if (country == null) { subdivisionLabel.set(null); @@ -169,11 +179,17 @@ void loadSubdivisions() { List subdivisionList = countryCodeSubdivisionMap.get(country); - entity.subsets.get(0).entryList.forEach(entry -> { - subdivisionList.add(new Subdivision(entry.name, entry.code, country)); + entity.subsets.forEach(subset -> { + subset.entryList.forEach(entry -> { + subdivisionList.add(new Subdivision(entry.name, entry.code, country)); + }); }); - countryCodeSubdivisionNameMap.put(country, entity.subsets.get(0).subdivisionType); + String subdivisionName = entity.subsets.stream() + .map(subset -> subset.subdivisionType) + .collect(Collectors.joining("/")); + + countryCodeSubdivisionNameMap.put(country, subdivisionName); } }); @@ -190,67 +206,22 @@ private Country findCountryByCode(String code) { return countries.stream().filter(country -> country.getCountryCode().equals(code)).findFirst().orElse(null); } - /** - * XML entity class. These classes represent the structure of the XML files - * to be loaded. - */ - @XmlRootElement(name = "iso_3166_subset") - @XmlAccessorType(XmlAccessType.FIELD) - static class ISO3166_2_EntryEntity { - - @XmlAttribute(name = "code") - public String code; - @XmlAttribute(name = "name") - public String name; - } - - /** - * XML entity class. These classes represent the structure of the XML files - * to be loaded. - */ - @XmlRootElement(name = "iso_3166_subset") - @XmlAccessorType(XmlAccessType.FIELD) - static class ISO3166_2_SubsetEntity { - - @XmlElement(name = "iso_3166_2_entry") - public List entryList; - - @XmlAttribute(name = "type") - public String subdivisionType; - } - - /** - * XML entity class. These classes represent the structure of the XML files - * to be loaded. - */ - @XmlRootElement(name = "iso_3166_country") - @XmlAccessorType(XmlAccessType.FIELD) - static class ISO3166_2_CountryEntity { - - @XmlAttribute(name = "code") - public String code; - - @XmlElement(name = "iso_3166_subset") - public List subsets; - - @Override - public String toString() { - return "CountryEntity " + code; - } - } - + @Override public ObservableList availableCountries() { return countries; } + @Override public ReadOnlyStringProperty subdivisionLabel() { return subdivisionLabel.getReadOnlyProperty(); } + @Override public ObservableList subdivisions() { return FXCollections.unmodifiableObservableList(subdivisions); } + @Override public ReadOnlyBooleanProperty inProgressProperty() { return inProgress.getReadOnlyProperty(); } diff --git a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/DomCountrySelector.java b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/DomCountrySelector.java new file mode 100644 index 000000000..1ffc1aa02 --- /dev/null +++ b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/DomCountrySelector.java @@ -0,0 +1,175 @@ +package de.saxsys.mvvmfx.examples.contacts.model.countries; + +import de.saxsys.mvvmfx.examples.contacts.model.Country; +import de.saxsys.mvvmfx.examples.contacts.model.Subdivision; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.annotation.PostConstruct; +import javax.enterprise.inject.Alternative; +import javax.inject.Singleton; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Singleton +@Alternative +public class DomCountrySelector implements CountrySelector { + private static final Logger LOG = LoggerFactory.getLogger(DomCountrySelector.class); + + public static final String ISO_3166_LOCATION = "/countries/iso_3166.xml"; + public static final String ISO_3166_2_LOCATION = "/countries/iso_3166_2.xml"; + + private ObservableList countries = FXCollections.observableArrayList(); + private ObservableList subdivisions = FXCollections.observableArrayList(); + + private ReadOnlyStringWrapper subdivisionLabel = new ReadOnlyStringWrapper(); + + private ReadOnlyBooleanWrapper inProgress = new ReadOnlyBooleanWrapper(false); + + private Map> countryCodeSubdivisionMap = new HashMap<>(); + private Map countryCodeSubdivisionNameMap = new HashMap<>(); + + @Override + @PostConstruct + public void init() { + inProgress.setValue(true); + loadCountries(); + } + + private void loadCountries() { + try { + + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + + loadCountriesFromXml(documentBuilder); + loadSubdivisionsFromXml(documentBuilder); + + } catch (Exception e) { + LOG.error("Cannot load Countries from XML file", e); + } + inProgress.setValue(false); + } + + private void loadSubdivisionsFromXml(DocumentBuilder documentBuilder) throws SAXException, IOException { + Document subdivisionsDocument = documentBuilder.parse(this.getClass().getResourceAsStream(ISO_3166_2_LOCATION)); + subdivisionsDocument.getDocumentElement().normalize(); + + NodeList countryNodes = subdivisionsDocument.getElementsByTagName("iso_3166_country"); + for (int countryIndex = 0; countryIndex < countryNodes.getLength(); countryIndex++) { + Node countryNode = countryNodes.item(countryIndex); + String countryCode = countryNode.getAttributes().getNamedItem("code").getNodeValue(); + + Country country = findCountryByCode(countryCode); + if (!countryCodeSubdivisionMap.containsKey(country)) { + countryCodeSubdivisionMap.put(country, new ArrayList<>()); + } + + List subdivisionList = countryCodeSubdivisionMap.get(country); + + if (country != null) { + NodeList subsetNodes = ((Element) countryNode).getElementsByTagName("iso_3166_subset"); + + List subdivisionNames = new ArrayList<>(); + + for (int subsetIndex = 0; subsetIndex < subsetNodes.getLength(); subsetIndex++) { + Node subsetNode = subsetNodes.item(subsetIndex); + + + String subsetType = subsetNode.getAttributes().getNamedItem("type").getNodeValue(); + subdivisionNames.add(subsetType); + + NodeList entryNodes = ((Element) subsetNode).getElementsByTagName("iso_3166_2_entry"); + + for (int entryIndex = 0; entryIndex < entryNodes.getLength(); entryIndex++) { + Node entryNode = entryNodes.item(entryIndex); + + String entryName = entryNode.getAttributes().getNamedItem("name").getNodeValue(); + String entryCode = entryNode.getAttributes().getNamedItem("code").getNodeValue(); + + subdivisionList.add(new Subdivision(entryName, entryCode, country)); + } + } + + String subdivisionName = subdivisionNames.stream() + .collect(Collectors.joining("/")); + + countryCodeSubdivisionNameMap.put(country, subdivisionName); + } + } + } + + private void loadCountriesFromXml(DocumentBuilder documentBuilder) throws SAXException, IOException { + Document countriesDocument = documentBuilder.parse(this.getClass().getResourceAsStream(ISO_3166_LOCATION)); + + countriesDocument.getDocumentElement().normalize(); + NodeList entries = countriesDocument.getElementsByTagName("iso_3166_entry"); + for (int i = 0; i < entries.getLength(); i++) { + Node item = entries.item(i); + String name = item.getAttributes().getNamedItem("name").getNodeValue(); + String alpha2Code = item.getAttributes().getNamedItem("alpha_2_code").getNodeValue(); + + Country country = new Country(name, alpha2Code); + + countries.add(country); + } + } + + private Country findCountryByCode(String code) { + return countries.stream().filter(country -> country.getCountryCode().equals(code)).findFirst().orElse(null); + } + + + @Override + public void setCountry(Country country) { + if (country == null) { + subdivisionLabel.set(null); + subdivisions.clear(); + return; + } + + subdivisionLabel.set(countryCodeSubdivisionNameMap.get(country)); + + subdivisions.clear(); + if (countryCodeSubdivisionMap.containsKey(country)) { + subdivisions.addAll(countryCodeSubdivisionMap.get(country)); + } + } + + @Override + public ObservableList availableCountries() { + return countries; + } + + @Override + public ReadOnlyStringProperty subdivisionLabel() { + return subdivisionLabel.getReadOnlyProperty(); + } + + @Override + public ObservableList subdivisions() { + return FXCollections.unmodifiableObservableList(subdivisions); + } + + @Override + public ReadOnlyBooleanProperty inProgressProperty() { + return inProgress.getReadOnlyProperty(); + } +} diff --git a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/ISO3166_2_CountryEntity.java b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/ISO3166_2_CountryEntity.java new file mode 100644 index 000000000..6f7ab6696 --- /dev/null +++ b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/ISO3166_2_CountryEntity.java @@ -0,0 +1,27 @@ +package de.saxsys.mvvmfx.examples.contacts.model.countries; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; + +/** + * XML entity class. These classes represent the structure of the XML files + * to be loaded. + */ +@XmlRootElement(name = "iso_3166_country") +@XmlAccessorType(XmlAccessType.FIELD) class ISO3166_2_CountryEntity { + + @XmlAttribute(name = "code") + public String code; + + @XmlElement(name = "iso_3166_subset") + public List subsets; + + @Override + public String toString() { + return "CountryEntity " + code; + } +} diff --git a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/ISO3166_2_Entries.java b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/ISO3166_2_Entries.java new file mode 100644 index 000000000..f1da7d865 --- /dev/null +++ b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/ISO3166_2_Entries.java @@ -0,0 +1,16 @@ +package de.saxsys.mvvmfx.examples.contacts.model.countries; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; + +@XmlRootElement(name = "iso_3166_2_entries") +@XmlAccessorType(XmlAccessType.FIELD) +public class ISO3166_2_Entries { + + @XmlElement(name = "iso_3166_country") + public ArrayList countryEntities; + +} diff --git a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/ISO3166_2_EntryEntity.java b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/ISO3166_2_EntryEntity.java new file mode 100644 index 000000000..66b93b7f2 --- /dev/null +++ b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/ISO3166_2_EntryEntity.java @@ -0,0 +1,19 @@ +package de.saxsys.mvvmfx.examples.contacts.model.countries; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * XML entity class. These classes represent the structure of the XML files + * to be loaded. + */ +@XmlRootElement(name = "iso_3166_subset") +@XmlAccessorType(XmlAccessType.FIELD) class ISO3166_2_EntryEntity { + + @XmlAttribute(name = "code") + public String code; + @XmlAttribute(name = "name") + public String name; +} diff --git a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/ISO3166_2_SubsetEntity.java b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/ISO3166_2_SubsetEntity.java new file mode 100644 index 000000000..553746597 --- /dev/null +++ b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/ISO3166_2_SubsetEntity.java @@ -0,0 +1,22 @@ +package de.saxsys.mvvmfx.examples.contacts.model.countries; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; + +/** + * XML entity class. These classes represent the structure of the XML files + * to be loaded. + */ +@XmlRootElement(name = "iso_3166_subset") +@XmlAccessorType(XmlAccessType.FIELD) class ISO3166_2_SubsetEntity { + + @XmlElement(name = "iso_3166_2_entry") + public List entryList; + + @XmlAttribute(name = "type") + public String subdivisionType; +} diff --git a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/JAXBCountrySelector.java b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/JAXBCountrySelector.java new file mode 100644 index 000000000..e2f1e2693 --- /dev/null +++ b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/model/countries/JAXBCountrySelector.java @@ -0,0 +1,151 @@ +package de.saxsys.mvvmfx.examples.contacts.model.countries; + +import de.saxsys.mvvmfx.examples.contacts.model.Countries; +import de.saxsys.mvvmfx.examples.contacts.model.Country; +import de.saxsys.mvvmfx.examples.contacts.model.Subdivision; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.PostConstruct; +import javax.inject.Singleton; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Singleton +public class JAXBCountrySelector implements CountrySelector { + private static final Logger LOG = LoggerFactory.getLogger(JAXBCountrySelector.class); + + public static final String ISO_3166_LOCATION = "/countries/iso_3166.xml"; + public static final String ISO_3166_2_LOCATION = "/countries/iso_3166_2.xml"; + + private ObservableList countries = FXCollections.observableArrayList(); + private ObservableList subdivisions = FXCollections.observableArrayList(); + + private ReadOnlyStringWrapper subdivisionLabel = new ReadOnlyStringWrapper(); + + private ReadOnlyBooleanWrapper inProgress = new ReadOnlyBooleanWrapper(false); + + private Map> countryCodeSubdivisionMap = new HashMap<>(); + private Map countryCodeSubdivisionNameMap = new HashMap<>(); + + public static void main(String[] args) { + JAXBCountrySelector jaxbCountrySelector = new JAXBCountrySelector(); + jaxbCountrySelector.init(); + } + + @Override + @PostConstruct + public void init() { + loadCountries(); + } + + private void loadCountries() { + inProgress.setValue(true); + + try { + loadCountriesFromXml(); + loadSubdivisionsFromXml(); + + } catch (JAXBException e) { + LOG.error("Cannot load Countries from XML file", e); + } + + inProgress.set(false); + } + + private void loadCountriesFromXml() throws JAXBException { + InputStream iso_3166 = this.getClass().getResourceAsStream(ISO_3166_LOCATION); + + JAXBContext context = JAXBContext.newInstance(Countries.class); + Countries countries = (Countries) context.createUnmarshaller().unmarshal(iso_3166); + this.countries.addAll(countries.getCountries()); + + } + + private void loadSubdivisionsFromXml() throws JAXBException { + JAXBContext jaxbContext = JAXBContext.newInstance(ISO3166_2_Entries.class); + InputStream iso_3166_2 = this.getClass().getResourceAsStream(ISO_3166_2_LOCATION); + ISO3166_2_Entries iso3166_2_entries = (ISO3166_2_Entries) jaxbContext.createUnmarshaller() + .unmarshal(iso_3166_2); + + iso3166_2_entries.countryEntities.stream() + .filter(entity -> entity.subsets != null && !entity.subsets.isEmpty()) + .forEach(entity -> { + Country country = findCountryByCode(entity.code); + if (!countryCodeSubdivisionMap.containsKey(country)) { + countryCodeSubdivisionMap.put(country, new ArrayList<>()); + } + + List subdivisionList = countryCodeSubdivisionMap.get(country); + + entity.subsets.stream() + .flatMap(subset -> subset.entryList.stream()) + .map(entry -> new Subdivision(entry.name, entry.code, country)) + .forEach(subdivisionList::add); + + + String subdivisionName = entity.subsets.stream() + .map(subset -> subset.subdivisionType) + .collect(Collectors.joining("/")); + + + countryCodeSubdivisionNameMap.put(country, subdivisionName); + }); + + } + + private Country findCountryByCode(String code) { + return countries.stream() + .filter(country -> country.getCountryCode().equals(code)) + .findFirst() + .orElse(null); + } + + @Override + public void setCountry(Country country) { + if (country == null) { + subdivisionLabel.set(null); + subdivisions.clear(); + return; + } + + subdivisionLabel.set(countryCodeSubdivisionNameMap.get(country)); + + subdivisions.clear(); + if (countryCodeSubdivisionMap.containsKey(country)) { + subdivisions.addAll(countryCodeSubdivisionMap.get(country)); + } + } + + @Override + public ObservableList availableCountries() { + return countries; + } + + @Override + public ReadOnlyStringProperty subdivisionLabel() { + return subdivisionLabel.getReadOnlyProperty(); + } + + @Override + public ObservableList subdivisions() { + return FXCollections.unmodifiableObservableList(subdivisions); + } + + @Override + public ReadOnlyBooleanProperty inProgressProperty() { + return inProgress.getReadOnlyProperty(); + } +} diff --git a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/ui/addcontact/AddContactDialogView.java b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/ui/addcontact/AddContactDialogView.java index 3246d7f5e..3bd3021ef 100644 --- a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/ui/addcontact/AddContactDialogView.java +++ b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/ui/addcontact/AddContactDialogView.java @@ -1,12 +1,9 @@ package de.saxsys.mvvmfx.examples.contacts.ui.addcontact; -import javax.inject.Singleton; - import de.saxsys.mvvmfx.FxmlView; import de.saxsys.mvvmfx.InjectViewModel; import javafx.stage.Stage; -@Singleton public class AddContactDialogView implements FxmlView { @InjectViewModel diff --git a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/ui/addressform/AddressFormViewModel.java b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/ui/addressform/AddressFormViewModel.java index 52a54151d..72d2f3464 100644 --- a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/ui/addressform/AddressFormViewModel.java +++ b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/ui/addressform/AddressFormViewModel.java @@ -10,7 +10,7 @@ import de.saxsys.mvvmfx.examples.contacts.model.Address; import de.saxsys.mvvmfx.examples.contacts.model.Contact; import de.saxsys.mvvmfx.examples.contacts.model.Country; -import de.saxsys.mvvmfx.examples.contacts.model.CountrySelector; +import de.saxsys.mvvmfx.examples.contacts.model.countries.CountrySelector; import de.saxsys.mvvmfx.examples.contacts.model.Subdivision; import de.saxsys.mvvmfx.examples.contacts.ui.scopes.ContactDialogScope; import de.saxsys.mvvmfx.utils.itemlist.ItemList; @@ -90,7 +90,6 @@ public void initialize() { }); loadingInProgress.bind(countrySelector.inProgressProperty()); - countrySelector.init(); initSubdivisionLabel(); initCountryList(); diff --git a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/ui/toolbar/ToolbarViewModel.java b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/ui/toolbar/ToolbarViewModel.java index fbba55514..f22d99d98 100644 --- a/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/ui/toolbar/ToolbarViewModel.java +++ b/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/ui/toolbar/ToolbarViewModel.java @@ -4,12 +4,17 @@ import de.saxsys.mvvmfx.ViewModel; import de.saxsys.mvvmfx.examples.contacts.ui.scopes.ContactDialogScope; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; import java.util.Collections; import java.util.List; public class ToolbarViewModel implements ViewModel { + @Inject + private Instance scopeInstance; + public List getScopesForAddDialog() { - return Collections.singletonList(new ContactDialogScope()); + return Collections.singletonList(scopeInstance.get()); } } diff --git a/examples/contacts-example/src/main/resources/countries/iso_3166_2.xml b/examples/contacts-example/src/main/resources/countries/iso_3166_2.xml index 4d598ca62..048a4f717 100644 --- a/examples/contacts-example/src/main/resources/countries/iso_3166_2.xml +++ b/examples/contacts-example/src/main/resources/countries/iso_3166_2.xml @@ -3731,8 +3731,9 @@ Source: - - + + + diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/AppTestFxIT.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/AppTestFxIT.java index f48d27b03..d1e8ef65f 100644 --- a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/AppTestFxIT.java +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/AppTestFxIT.java @@ -1,19 +1,19 @@ package de.saxsys.mvvmfx.examples.contacts; import de.saxsys.mvvmfx.examples.contacts.App; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; import org.testfx.api.FxRobot; import org.testfx.api.FxToolkit; import static org.testfx.api.FxAssert.verifyThat; import static org.testfx.matcher.control.TableViewMatchers.hasTableCell; -@Ignore +@Disabled public class AppTestFxIT extends FxRobot { - @Before + @BeforeEach public void setupApp() throws Exception { FxToolkit.registerPrimaryStage(); diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/CountrySelectorIntegrationTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/CountrySelectorIntegrationTest.java deleted file mode 100644 index 69d283b36..000000000 --- a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/CountrySelectorIntegrationTest.java +++ /dev/null @@ -1,162 +0,0 @@ -package de.saxsys.mvvmfx.examples.contacts.model; - -import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; -import javafx.application.Platform; -import org.datafx.reader.converter.XmlConverter; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.stream.Collectors; - -import static org.assertj.core.api.Assertions.assertThat; -import static eu.lestard.assertj.javafx.api.Assertions.assertThat; - -@RunWith(JfxRunner.class) -public class CountrySelectorIntegrationTest { - - private CountrySelector countrySelector; - - @Before - public void setup() { - countrySelector = new CountrySelector(); - } - - @Test - public void testXmlConverterForCountry() throws FileNotFoundException { - XmlConverter converter = new XmlConverter<>("iso_3166_entry", Country.class); - - InputStream iso_3166_xml = this.getClass().getResourceAsStream("/countries/iso_3166.xml"); - - assertThat(iso_3166_xml).isNotNull(); - - converter.initialize(iso_3166_xml); - - Country country = converter.get(); - assertThat(country).isNotNull(); - } - - @Test - public void testXmlConverterForSubdivision() throws Exception { - XmlConverter converter = new XmlConverter<>("iso_3166_country", - CountrySelector.ISO3166_2_CountryEntity.class); - - InputStream iso_3166_2_xml = this.getClass().getResourceAsStream("/countries/iso_3166_2.xml"); - - assertThat(iso_3166_2_xml).isNotNull(); - - converter.initialize(iso_3166_2_xml); - - CountrySelector.ISO3166_2_CountryEntity entity = converter.get(); - - assertThat(entity).isNotNull(); - assertThat(entity.code).isNotNull().isEqualTo("DE"); - - assertThat(entity.subsets).isNotNull().hasSize(1); - assertThat(entity.subsets.get(0).subdivisionType).isEqualTo("State"); - - List entryList = entity.subsets.get(0).entryList; - - assertThat(entryList).isNotNull().hasSize(16); - - CountrySelector.ISO3166_2_EntryEntity entry = entryList.get(0); - - assertThat(entry.code).isEqualTo("DE-BW"); - assertThat(entry.name).isEqualTo("Baden-Württemberg"); - } - - @Test - public void testLoadSubdivisions() throws Exception { - runBlocked(countrySelector::init); - - assertThat(countrySelector.subdivisionLabel()).hasNullValue(); - assertThat(countrySelector.subdivisions()).isEmpty(); - - countrySelector.setCountry(new Country("Germany", "DE")); - - assertThat(countrySelector.subdivisionLabel()).hasValue("State"); - assertThat(countrySelector.subdivisions()).hasSize(16); - - countrySelector.setCountry(null); - - assertThat(countrySelector.subdivisionLabel()).hasNullValue(); - assertThat(countrySelector.subdivisions()).isEmpty(); - - } - - @Test - public void testLoadCountries() throws InterruptedException, ExecutionException, TimeoutException { - runBlocked(countrySelector::init); - - assertThat(countrySelector.availableCountries()).hasSize(3); - assertThat(getCountryNames(countrySelector.availableCountries())).contains("Germany", "Austria", "Switzerland"); - - assertThat(countrySelector.subdivisions()).isEmpty(); - assertThat(countrySelector.subdivisionLabel()).hasNullValue(); - - Country germany = getCountryByName(countrySelector.availableCountries(), "Germany"); - - assertThat(germany).isNotNull(); - - countrySelector.setCountry(germany); - - assertThat(countrySelector.subdivisions()).hasSize(16); - assertThat(getSubdivisionNames(countrySelector.subdivisions())).contains("Sachsen", "Bayern", "Hessen"); // only - // test - // some - // examples. - assertThat(countrySelector.subdivisionLabel()).hasValue("State"); - - Country switzerland = getCountryByName(countrySelector.availableCountries(), "Switzerland"); - - countrySelector.setCountry(switzerland); - - assertThat(countrySelector.subdivisions()).hasSize(26); - assertThat(getSubdivisionNames(countrySelector.subdivisions())).contains("Zürich", "Jura", "Bern"); - assertThat(countrySelector.subdivisionLabel()).hasValue("Canton"); - - countrySelector.setCountry(null); - - assertThat(countrySelector.subdivisions()).isEmpty(); - assertThat(countrySelector.subdivisionLabel()).hasNullValue(); - } - - private void runBlocked(Runnable function) { - CompletableFuture blocker = new CompletableFuture<>(); - - countrySelector.inProgressProperty().addListener((obs, oldV, newV) -> { - if (!newV) { - blocker.complete(true); - } - }); - - Platform.runLater(function); - - try { - blocker.get(5, TimeUnit.SECONDS); - } catch (Exception e) { - e.printStackTrace(); - } - } - - private Country getCountryByName(List countries, String name) { - return countries.stream().filter(country -> country.getName().equals(name)).findFirst().orElse(null); - } - - private List getSubdivisionNames(List subdivisions) { - return subdivisions.stream().map(Subdivision::getName).collect(Collectors.toList()); - } - - private List getCountryNames(List countries) { - return countries.stream().map(Country::getName).collect(Collectors.toList()); - } - -} diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/countries/CountrySelectorInterfaceTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/countries/CountrySelectorInterfaceTest.java new file mode 100644 index 000000000..aedd0e232 --- /dev/null +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/countries/CountrySelectorInterfaceTest.java @@ -0,0 +1,118 @@ +package de.saxsys.mvvmfx.examples.contacts.model.countries; + +import de.saxsys.mvvmfx.examples.contacts.model.Country; +import de.saxsys.mvvmfx.examples.contacts.model.Subdivision; +import de.saxsys.mvvmfx.examples.contacts.model.countries.CountrySelector; +import javafx.application.Platform; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import static eu.lestard.assertj.javafx.api.Assertions.assertThat; + +public interface CountrySelectorInterfaceTest { + + CountrySelector getCountrySelector(); + + @Test + default void testLoadSubdivisions() throws Exception { + CountrySelector countrySelector = getCountrySelector(); + runBlocked(countrySelector::init); + + assertThat(countrySelector.subdivisionLabel()).hasNullValue(); + Assertions.assertThat(countrySelector.subdivisions()).isEmpty(); + + countrySelector.setCountry(new Country("Germany", "DE")); + + assertThat(countrySelector.subdivisionLabel()).hasValue("State"); + Assertions.assertThat(countrySelector.subdivisions()).hasSize(16); + + countrySelector.setCountry(new Country("Australia", "AU")); + assertThat(countrySelector.subdivisionLabel()).hasValue("State/Territory"); + + countrySelector.setCountry(new Country("China", "CN")); + assertThat(countrySelector.subdivisionLabel()).hasValue("Municipality/Province/Autonomous region/Special administrative region"); + + countrySelector.setCountry(null); + + assertThat(countrySelector.subdivisionLabel()).hasNullValue(); + Assertions.assertThat(countrySelector.subdivisions()).isEmpty(); + + } + + @Test + default void testLoadCountries() throws InterruptedException, ExecutionException, TimeoutException { + CountrySelector countrySelector = getCountrySelector(); + + runBlocked(countrySelector::init); + + Assertions.assertThat(countrySelector.availableCountries()).hasSize(5); + Assertions.assertThat(getCountryNames(countrySelector.availableCountries())).contains("Germany", "Austria", "Switzerland", "Australia", "China"); + + Assertions.assertThat(countrySelector.subdivisions()).isEmpty(); + assertThat(countrySelector.subdivisionLabel()).hasNullValue(); + + Country germany = getCountryByName(countrySelector.availableCountries(), "Germany"); + + Assertions.assertThat(germany).isNotNull(); + + countrySelector.setCountry(germany); + + Assertions.assertThat(countrySelector.subdivisions()).hasSize(16); + Assertions + .assertThat(getSubdivisionNames(countrySelector.subdivisions())).contains("Sachsen", "Bayern", "Hessen"); // only + // test + // some + // examples. + assertThat(countrySelector.subdivisionLabel()).hasValue("State"); + + Country switzerland = getCountryByName(countrySelector.availableCountries(), "Switzerland"); + + countrySelector.setCountry(switzerland); + + Assertions.assertThat(countrySelector.subdivisions()).hasSize(26); + Assertions.assertThat(getSubdivisionNames(countrySelector.subdivisions())).contains("Zürich", "Jura", "Bern"); + assertThat(countrySelector.subdivisionLabel()).hasValue("Canton"); + + countrySelector.setCountry(null); + + Assertions.assertThat(countrySelector.subdivisions()).isEmpty(); + assertThat(countrySelector.subdivisionLabel()).hasNullValue(); + } + + default void runBlocked(Runnable function) { + CompletableFuture blocker = new CompletableFuture<>(); + + getCountrySelector().inProgressProperty().addListener((obs, oldV, newV) -> { + if (!newV) { + blocker.complete(true); + } + }); + + Platform.runLater(function); + + try { + blocker.get(5, TimeUnit.SECONDS); + } catch (Exception e) { + e.printStackTrace(); + } + } + + default Country getCountryByName(List countries, String name) { + return countries.stream().filter(country -> country.getName().equals(name)).findFirst().orElse(null); + } + + default List getSubdivisionNames(List subdivisions) { + return subdivisions.stream().map(Subdivision::getName).collect(Collectors.toList()); + } + + default List getCountryNames(List countries) { + return countries.stream().map(Country::getName).collect(Collectors.toList()); + } +} diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/countries/DataFxCountrySelectorIntegrationTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/countries/DataFxCountrySelectorIntegrationTest.java new file mode 100644 index 000000000..860173a30 --- /dev/null +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/countries/DataFxCountrySelectorIntegrationTest.java @@ -0,0 +1,74 @@ +package de.saxsys.mvvmfx.examples.contacts.model.countries; + +import de.saxsys.mvvmfx.examples.contacts.model.Country; +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; +import org.datafx.reader.converter.XmlConverter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(JfxToolkitExtension.class) +public class DataFxCountrySelectorIntegrationTest implements CountrySelectorInterfaceTest { + + private CountrySelector countrySelector; + + + @Override public CountrySelector getCountrySelector() { + return countrySelector; + } + + @BeforeEach + public void setup() { + countrySelector = new DataFxCountrySelector(); + } + + @Test + public void testXmlConverterForCountry() throws FileNotFoundException { + XmlConverter converter = new XmlConverter<>("iso_3166_entry", Country.class); + + InputStream iso_3166_xml = this.getClass().getResourceAsStream("/countries/iso_3166.xml"); + + assertThat(iso_3166_xml).isNotNull(); + + converter.initialize(iso_3166_xml); + + Country country = converter.get(); + assertThat(country).isNotNull(); + } + + @Test + public void testXmlConverterForSubdivision() throws Exception { + XmlConverter converter = new XmlConverter<>("iso_3166_country", + ISO3166_2_CountryEntity.class); + + InputStream iso_3166_2_xml = this.getClass().getResourceAsStream("/countries/iso_3166_2.xml"); + + assertThat(iso_3166_2_xml).isNotNull(); + + converter.initialize(iso_3166_2_xml); + + ISO3166_2_CountryEntity entity = converter.get(); + + assertThat(entity).isNotNull(); + assertThat(entity.code).isNotNull().isEqualTo("DE"); + + assertThat(entity.subsets).isNotNull().hasSize(1); + assertThat(entity.subsets.get(0).subdivisionType).isEqualTo("State"); + + List entryList = entity.subsets.get(0).entryList; + + assertThat(entryList).isNotNull().hasSize(16); + + ISO3166_2_EntryEntity entry = entryList.get(0); + + assertThat(entry.code).isEqualTo("DE-BW"); + assertThat(entry.name).isEqualTo("Baden-Württemberg"); + } + +} diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/countries/DomCountrySelectorIntegrationTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/countries/DomCountrySelectorIntegrationTest.java new file mode 100644 index 000000000..ee2c415e6 --- /dev/null +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/countries/DomCountrySelectorIntegrationTest.java @@ -0,0 +1,21 @@ +package de.saxsys.mvvmfx.examples.contacts.model.countries; + +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(JfxToolkitExtension.class) +public class DomCountrySelectorIntegrationTest implements CountrySelectorInterfaceTest { + + private CountrySelector countrySelector; + + @BeforeEach + public void setup(){ + countrySelector = new DomCountrySelector(); + } + + + @Override public CountrySelector getCountrySelector() { + return countrySelector; + } +} diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/countries/JAXBCountrySelectorIntegrationTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/countries/JAXBCountrySelectorIntegrationTest.java new file mode 100644 index 000000000..9e2816239 --- /dev/null +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/countries/JAXBCountrySelectorIntegrationTest.java @@ -0,0 +1,20 @@ +package de.saxsys.mvvmfx.examples.contacts.model.countries; + +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(JfxToolkitExtension.class) +public class JAXBCountrySelectorIntegrationTest implements CountrySelectorInterfaceTest { + + private CountrySelector countrySelector; + + @BeforeEach + public void setup() { + countrySelector = new JAXBCountrySelector(); + } + + @Override public CountrySelector getCountrySelector() { + return countrySelector; + } +} diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/validation/BirthdayValidatorTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/validation/BirthdayValidatorTest.java index 6c7f48358..ef906572a 100644 --- a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/validation/BirthdayValidatorTest.java +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/validation/BirthdayValidatorTest.java @@ -13,8 +13,8 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import de.saxsys.mvvmfx.examples.contacts.util.CentralClock; import de.saxsys.mvvmfx.utils.validation.ValidationStatus; @@ -24,7 +24,7 @@ public class BirthdayValidatorTest { private ValidationStatus result; private ObjectProperty value = new SimpleObjectProperty<>(); - @Before + @BeforeEach public void setup() { ZonedDateTime now = ZonedDateTime .of(LocalDate.of(2014, Month.JANUARY, 1), LocalTime.of(0, 0), ZoneId.systemDefault()); diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/validation/EmailAddressValidatorTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/validation/EmailAddressValidatorTest.java index dd3b7fbcf..34fada138 100644 --- a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/validation/EmailAddressValidatorTest.java +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/validation/EmailAddressValidatorTest.java @@ -7,8 +7,8 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import de.saxsys.mvvmfx.utils.validation.ValidationStatus; @@ -17,7 +17,7 @@ public class EmailAddressValidatorTest { private ValidationStatus result; private StringProperty value = new SimpleStringProperty(); - @Before + @BeforeEach public void setup() { Validator validator = new EmailValidator(value); diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/validation/PhoneNumberValidatorTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/validation/PhoneNumberValidatorTest.java index cbb2e2242..deb4ed61f 100644 --- a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/validation/PhoneNumberValidatorTest.java +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/model/validation/PhoneNumberValidatorTest.java @@ -7,8 +7,8 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import de.saxsys.mvvmfx.utils.validation.ValidationStatus; @@ -17,7 +17,7 @@ public class PhoneNumberValidatorTest { private ValidationStatus result; private StringProperty value = new SimpleStringProperty(); - @Before + @BeforeEach public void setup() { Validator validator = new PhoneValidator(value, "error message"); diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/about/AboutViewModelTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/about/AboutViewModelTest.java index b6a65472d..b32ace847 100644 --- a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/about/AboutViewModelTest.java +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/about/AboutViewModelTest.java @@ -9,8 +9,8 @@ import javafx.beans.property.ReadOnlyStringProperty; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class AboutViewModelTest { @@ -25,7 +25,7 @@ public class AboutViewModelTest { private Consumer onLinkClickedHandler; @SuppressWarnings("unchecked") - @Before + @BeforeEach public void setup() { viewModel = new AboutViewModel(); diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/addressform/AddressFormViewModelTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/addressform/AddressFormViewModelTest.java index 06d8ea6d1..a8992e161 100644 --- a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/addressform/AddressFormViewModelTest.java +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/addressform/AddressFormViewModelTest.java @@ -1,7 +1,7 @@ package de.saxsys.mvvmfx.examples.contacts.ui.addressform; import de.saxsys.mvvmfx.examples.contacts.model.Country; -import de.saxsys.mvvmfx.examples.contacts.model.CountrySelector; +import de.saxsys.mvvmfx.examples.contacts.model.countries.CountrySelector; import de.saxsys.mvvmfx.examples.contacts.model.Subdivision; import de.saxsys.mvvmfx.examples.contacts.ui.scopes.ContactDialogScope; import javafx.beans.property.SimpleBooleanProperty; @@ -9,8 +9,8 @@ import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.ListResourceBundle; import java.util.ResourceBundle; @@ -39,7 +39,7 @@ public class AddressFormViewModelTest { private ContactDialogScope scope; - @Before + @BeforeEach public void setup() { availableCountries.addAll(germany, austria); diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/contactdialog/ContactDialogViewModelTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/contactdialog/ContactDialogViewModelTest.java index 268dd6562..2bb9d18cc 100644 --- a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/contactdialog/ContactDialogViewModelTest.java +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/contactdialog/ContactDialogViewModelTest.java @@ -2,8 +2,8 @@ import de.saxsys.mvvmfx.examples.contacts.ui.scopes.ContactDialogScope; import javafx.beans.property.BooleanProperty; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import static eu.lestard.assertj.javafx.api.Assertions.assertThat; @@ -16,7 +16,7 @@ public class ContactDialogViewModelTest { private BooleanProperty contactFormValid; private BooleanProperty addressFormValid; - @Before + @BeforeEach public void setup() { scope = new ContactDialogScope(); contactFormValid = scope.contactFormValidProperty(); diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/contactdialog/ContactFormViewModelTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/contactdialog/ContactFormViewModelTest.java index 6107d71c3..e27b7d1ab 100644 --- a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/contactdialog/ContactFormViewModelTest.java +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/contactdialog/ContactFormViewModelTest.java @@ -2,8 +2,8 @@ import de.saxsys.mvvmfx.examples.contacts.ui.contactform.ContactFormViewModel; import de.saxsys.mvvmfx.testingutils.GCVerifier; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import static eu.lestard.assertj.javafx.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; @@ -12,7 +12,7 @@ public class ContactFormViewModelTest { private ContactFormViewModel viewModel; - @Before + @BeforeEach public void setup() { viewModel = new ContactFormViewModel(); } diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/detail/DetailViewModelTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/detail/DetailViewModelTest.java index bad440ab4..a25f3f3ba 100644 --- a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/detail/DetailViewModelTest.java +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/detail/DetailViewModelTest.java @@ -7,8 +7,8 @@ import java.time.LocalDate; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import de.saxsys.mvvmfx.examples.contacts.model.Contact; import de.saxsys.mvvmfx.examples.contacts.model.Repository; @@ -26,7 +26,7 @@ public class DetailViewModelTest { private Repository repository; - @Before + @BeforeEach public void setup() { MasterDetailScope masterViewModelMock = mock(MasterDetailScope.class); diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/editcontact/EditContactDialogViewModelTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/editcontact/EditContactDialogViewModelTest.java index d93ce97b7..285ed9e00 100644 --- a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/editcontact/EditContactDialogViewModelTest.java +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/editcontact/EditContactDialogViewModelTest.java @@ -7,7 +7,7 @@ import java.util.ListResourceBundle; import java.util.ResourceBundle; -import org.junit.Before; +import org.junit.jupiter.api.BeforeEach; import de.saxsys.mvvmfx.examples.contacts.model.Repository; import de.saxsys.mvvmfx.examples.contacts.ui.contactdialog.ContactDialogViewModel; @@ -25,7 +25,7 @@ public class EditContactDialogViewModelTest { private ContactDialogScope scope; - @Before + @BeforeEach public void setup() { scope = new ContactDialogScope(); diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/master/MasterTableViewModelTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/master/MasterTableViewModelTest.java index f74bab81a..46bdc3625 100644 --- a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/master/MasterTableViewModelTest.java +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/master/MasterTableViewModelTest.java @@ -8,7 +8,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; -import org.junit.Test; +import org.junit.jupiter.api.Test; import de.saxsys.mvvmfx.examples.contacts.model.Contact; import de.saxsys.mvvmfx.examples.contacts.util.CentralClock; diff --git a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/master/MasterViewModelTest.java b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/master/MasterViewModelTest.java index 6ab0f089c..329b18636 100644 --- a/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/master/MasterViewModelTest.java +++ b/examples/contacts-example/src/test/java/de/saxsys/mvvmfx/examples/contacts/ui/master/MasterViewModelTest.java @@ -11,8 +11,8 @@ import java.util.function.Consumer; import java.util.stream.Collectors; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import de.saxsys.mvvmfx.examples.contacts.events.ContactsUpdatedEvent; import de.saxsys.mvvmfx.examples.contacts.model.Contact; @@ -34,7 +34,7 @@ public class MasterViewModelTest { private Consumer onSelectConsumer; - @Before + @BeforeEach public void setup() { repository = new InmemoryRepository(); viewModel = new MasterViewModel(); @@ -143,12 +143,12 @@ public void testUpdateContactListNoSelectionWhenSelectedItemIsRemoved() { * TableView. * * The TableView doesn't directly show instances of - * {@link de.saxsys.mvvmfx.examples.contacts.model.Contact} but instead + * {@link Contact} but instead * contains instances of - * {@link de.saxsys.mvvmfx.examples.contacts.ui.master.MasterTableViewModel}. + * {@link MasterTableViewModel}. * * Every - * {@link de.saxsys.mvvmfx.examples.contacts.ui.master.MasterTableViewModel} + * {@link MasterTableViewModel} * has an ID attribute corresponding to the ID of the contact that is shown. * This method extracts these IDs and returns them as List. This way we can * verify what Contacts are shown in the Table. @@ -160,8 +160,8 @@ private List getContactIdsInTable() { /** * Returns the - * {@link de.saxsys.mvvmfx.examples.contacts.ui.master.MasterTableViewModel} - * for the given {@link de.saxsys.mvvmfx.examples.contacts.model.Contact} + * {@link MasterTableViewModel} + * for the given {@link Contact} * from the contact list. */ private MasterTableViewModel findTableViewModelForContact(Contact contact) { diff --git a/examples/contacts-example/src/test/resources/countries/iso_3166.xml b/examples/contacts-example/src/test/resources/countries/iso_3166.xml index 7e68fb37b..13ef04a48 100644 --- a/examples/contacts-example/src/test/resources/countries/iso_3166.xml +++ b/examples/contacts-example/src/test/resources/countries/iso_3166.xml @@ -41,4 +41,15 @@ numeric_code="756" name="Switzerland" official_name="Swiss Confederation"/> + + \ No newline at end of file diff --git a/examples/contacts-example/src/test/resources/countries/iso_3166_2.xml b/examples/contacts-example/src/test/resources/countries/iso_3166_2.xml index 7e4f5ba66..03a454dea 100644 --- a/examples/contacts-example/src/test/resources/countries/iso_3166_2.xml +++ b/examples/contacts-example/src/test/resources/countries/iso_3166_2.xml @@ -138,4 +138,106 @@ code="CH-ZH" name="Zürich"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/mini-examples/async-todoapp-futures/pom.xml b/examples/mini-examples/async-todoapp-futures/pom.xml index 58ce74cb0..5612129b0 100644 --- a/examples/mini-examples/async-todoapp-futures/pom.xml +++ b/examples/mini-examples/async-todoapp-futures/pom.xml @@ -5,7 +5,7 @@ mini-examples de.saxsys.mvvmfx - 1.6.0 + 1.7.0 4.0.0 diff --git a/examples/mini-examples/fx-root-example/pom.xml b/examples/mini-examples/fx-root-example/pom.xml index 981d7917b..374ff224e 100644 --- a/examples/mini-examples/fx-root-example/pom.xml +++ b/examples/mini-examples/fx-root-example/pom.xml @@ -5,7 +5,7 @@ mini-examples de.saxsys.mvvmfx - 1.6.0 + 1.7.0 4.0.0 @@ -40,6 +40,10 @@ junit test + + org.junit.jupiter + junit-jupiter-api + org.loadui diff --git a/examples/mini-examples/fx-root-example/src/test/java/de/saxsys/mvvmfx/examples/fx_root_example/LabeledTextFieldTest.java b/examples/mini-examples/fx-root-example/src/test/java/de/saxsys/mvvmfx/examples/fx_root_example/LabeledTextFieldTest.java index 5edbb2217..6654ae188 100644 --- a/examples/mini-examples/fx-root-example/src/test/java/de/saxsys/mvvmfx/examples/fx_root_example/LabeledTextFieldTest.java +++ b/examples/mini-examples/fx-root-example/src/test/java/de/saxsys/mvvmfx/examples/fx_root_example/LabeledTextFieldTest.java @@ -2,14 +2,14 @@ import static eu.lestard.assertj.javafx.api.Assertions.assertThat; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class LabeledTextFieldTest { private LabeledTextFieldViewModel viewModel; - @Before + @BeforeEach public void setup() { viewModel = new LabeledTextFieldViewModel(); } diff --git a/examples/mini-examples/helloworld-custom-fxml-path/README.md b/examples/mini-examples/helloworld-custom-fxml-path/README.md new file mode 100644 index 000000000..70309cd65 --- /dev/null +++ b/examples/mini-examples/helloworld-custom-fxml-path/README.md @@ -0,0 +1,10 @@ +# MvvmFX HelloWorld with custom FXML path + +This module is an example of a simple HelloWorld app that shows +how to develop a View with a CodeBehind class that has another location then +the FXML file. Normally both files should have the same name to be found by mvvmFX +by following a set of [naming conventions](https://github.com/sialcasa/mvvmFX/wiki/Naming-Conventions). + +However, it is also possible to use the `@FxmlPath` annotation +to define a custom path for the FXML file. See [the Wiki](https://github.com/sialcasa/mvvmFX/wiki/Deviating-FXML-location) +for a detailed description. \ No newline at end of file diff --git a/examples/mini-examples/helloworld-custom-fxml-path/pom.xml b/examples/mini-examples/helloworld-custom-fxml-path/pom.xml new file mode 100644 index 000000000..17b49d009 --- /dev/null +++ b/examples/mini-examples/helloworld-custom-fxml-path/pom.xml @@ -0,0 +1,22 @@ + + + + mini-examples + de.saxsys.mvvmfx + 1.7.0 + + 4.0.0 + + helloworld-custom-fxml-path + HelloWorld with custom FXML path + + + + de.saxsys + mvvmfx + + + + \ No newline at end of file diff --git a/examples/mini-examples/helloworld-custom-fxml-path/src/main/java/de/saxsys/mvvmfx/examples/helloworld/HelloView.java b/examples/mini-examples/helloworld-custom-fxml-path/src/main/java/de/saxsys/mvvmfx/examples/helloworld/HelloView.java new file mode 100644 index 000000000..dc1f9feb4 --- /dev/null +++ b/examples/mini-examples/helloworld-custom-fxml-path/src/main/java/de/saxsys/mvvmfx/examples/helloworld/HelloView.java @@ -0,0 +1,20 @@ +package de.saxsys.mvvmfx.examples.helloworld; + +import de.saxsys.mvvmfx.FxmlPath; +import de.saxsys.mvvmfx.FxmlView; +import de.saxsys.mvvmfx.InjectViewModel; +import javafx.fxml.FXML; +import javafx.scene.control.Label; + +@FxmlPath("/some/other/path/HalloWelt.fxml") +public class HelloView implements FxmlView { + @FXML + public Label helloLabel; + + @InjectViewModel + private HelloViewModel viewModel; + + public void initialize() { + helloLabel.textProperty().bind(viewModel.helloMessage()); + } +} diff --git a/examples/mini-examples/helloworld-custom-fxml-path/src/main/java/de/saxsys/mvvmfx/examples/helloworld/HelloViewModel.java b/examples/mini-examples/helloworld-custom-fxml-path/src/main/java/de/saxsys/mvvmfx/examples/helloworld/HelloViewModel.java new file mode 100644 index 000000000..0bbcddeb8 --- /dev/null +++ b/examples/mini-examples/helloworld-custom-fxml-path/src/main/java/de/saxsys/mvvmfx/examples/helloworld/HelloViewModel.java @@ -0,0 +1,23 @@ +package de.saxsys.mvvmfx.examples.helloworld; + +import de.saxsys.mvvmfx.ViewModel; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +public class HelloViewModel implements ViewModel { + + private final StringProperty helloMessage = new SimpleStringProperty("Hello World"); + + public StringProperty helloMessage() { + return helloMessage; + } + + public String getHelloMessage() { + return helloMessage.get(); + } + + public void setHelloMessage(String message) { + helloMessage.set(message); + } + +} diff --git a/examples/mini-examples/helloworld-custom-fxml-path/src/main/java/de/saxsys/mvvmfx/examples/helloworld/Starter.java b/examples/mini-examples/helloworld-custom-fxml-path/src/main/java/de/saxsys/mvvmfx/examples/helloworld/Starter.java new file mode 100644 index 000000000..93db24da3 --- /dev/null +++ b/examples/mini-examples/helloworld-custom-fxml-path/src/main/java/de/saxsys/mvvmfx/examples/helloworld/Starter.java @@ -0,0 +1,30 @@ +package de.saxsys.mvvmfx.examples.helloworld; + +import de.saxsys.mvvmfx.FluentViewLoader; +import de.saxsys.mvvmfx.ViewTuple; +import javafx.application.Application; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +public class Starter extends Application { + + public static void main(String... args) { + Application.launch(args); + } + + @Override + public void start(Stage stage) throws Exception { + stage.setTitle("Hello World Application"); + + final ViewTuple viewTuple = FluentViewLoader + .fxmlView(HelloView.class) + .load(); + + final Parent root = viewTuple.getView(); + stage.setScene(new Scene(root)); + stage.show(); + } + +} + diff --git a/examples/mini-examples/helloworld-custom-fxml-path/src/main/resources/some/other/path/HalloWelt.fxml b/examples/mini-examples/helloworld-custom-fxml-path/src/main/resources/some/other/path/HalloWelt.fxml new file mode 100644 index 000000000..32e158f5b --- /dev/null +++ b/examples/mini-examples/helloworld-custom-fxml-path/src/main/resources/some/other/path/HalloWelt.fxml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/examples/mini-examples/helloworld-without-fxml/pom.xml b/examples/mini-examples/helloworld-without-fxml/pom.xml index 7feeff7ff..e1a6b7305 100644 --- a/examples/mini-examples/helloworld-without-fxml/pom.xml +++ b/examples/mini-examples/helloworld-without-fxml/pom.xml @@ -5,7 +5,7 @@ mini-examples de.saxsys.mvvmfx - 1.6.0 + 1.7.0 4.0.0 diff --git a/examples/mini-examples/helloworld/pom.xml b/examples/mini-examples/helloworld/pom.xml index b74a10f5c..590904ae8 100644 --- a/examples/mini-examples/helloworld/pom.xml +++ b/examples/mini-examples/helloworld/pom.xml @@ -5,7 +5,7 @@ mini-examples de.saxsys.mvvmfx - 1.6.0 + 1.7.0 4.0.0 diff --git a/examples/mini-examples/pom.xml b/examples/mini-examples/pom.xml index be330b12a..5c20b3e74 100644 --- a/examples/mini-examples/pom.xml +++ b/examples/mini-examples/pom.xml @@ -5,7 +5,7 @@ examples de.saxsys.mvvmfx - 1.6.0 + 1.7.0 4.0.0 pom @@ -22,6 +22,7 @@ welcome-example scopes-example async-todoapp-futures + helloworld-custom-fxml-path diff --git a/examples/mini-examples/scopes-example/pom.xml b/examples/mini-examples/scopes-example/pom.xml index d92615620..45f01a170 100644 --- a/examples/mini-examples/scopes-example/pom.xml +++ b/examples/mini-examples/scopes-example/pom.xml @@ -5,7 +5,7 @@ mini-examples de.saxsys.mvvmfx - 1.6.0 + 1.7.0 4.0.0 diff --git a/examples/mini-examples/synchronizefx-example/pom.xml b/examples/mini-examples/synchronizefx-example/pom.xml index c25d80ba8..ca43e6d02 100644 --- a/examples/mini-examples/synchronizefx-example/pom.xml +++ b/examples/mini-examples/synchronizefx-example/pom.xml @@ -5,7 +5,7 @@ mini-examples de.saxsys.mvvmfx - 1.6.0 + 1.7.0 4.0.0 diff --git a/examples/mini-examples/welcome-example/pom.xml b/examples/mini-examples/welcome-example/pom.xml index 59107068f..e0c6779b9 100644 --- a/examples/mini-examples/welcome-example/pom.xml +++ b/examples/mini-examples/welcome-example/pom.xml @@ -5,7 +5,7 @@ mini-examples de.saxsys.mvvmfx - 1.6.0 + 1.7.0 4.0.0 @@ -18,21 +18,19 @@ mvvmfx - - javax.inject - javax.inject - 1 - - de.saxsys mvvmfx-cdi + + org.jboss.weld.se + weld-se-core + org.jboss jandex - 1.2.4.Final + 2.0.3.Final @@ -42,8 +40,8 @@ - junit - junit + org.junit.jupiter + junit-jupiter-api test diff --git a/examples/mini-examples/welcome-example/src/test/java/de/saxsys/mvvmfx/viewmodel/personlogin/PersonLoginViewModelTest.java b/examples/mini-examples/welcome-example/src/test/java/de/saxsys/mvvmfx/viewmodel/personlogin/PersonLoginViewModelTest.java index fbce7d537..83b7332a3 100644 --- a/examples/mini-examples/welcome-example/src/test/java/de/saxsys/mvvmfx/viewmodel/personlogin/PersonLoginViewModelTest.java +++ b/examples/mini-examples/welcome-example/src/test/java/de/saxsys/mvvmfx/viewmodel/personlogin/PersonLoginViewModelTest.java @@ -1,10 +1,10 @@ package de.saxsys.mvvmfx.viewmodel.personlogin; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import javafx.collections.ObservableList; -import org.junit.Test; +import org.junit.jupiter.api.Test; import de.saxsys.mvvmfx.examples.welcome.model.Repository; import de.saxsys.mvvmfx.examples.welcome.viewmodel.personlogin.PersonLoginViewModel; diff --git a/examples/mini-examples/welcome-example/src/test/java/de/saxsys/mvvmfx/viewmodel/personwelcome/PersonWelcomeViewModelTest.java b/examples/mini-examples/welcome-example/src/test/java/de/saxsys/mvvmfx/viewmodel/personwelcome/PersonWelcomeViewModelTest.java index 0728f1ba2..9f671c9cd 100644 --- a/examples/mini-examples/welcome-example/src/test/java/de/saxsys/mvvmfx/viewmodel/personwelcome/PersonWelcomeViewModelTest.java +++ b/examples/mini-examples/welcome-example/src/test/java/de/saxsys/mvvmfx/viewmodel/personwelcome/PersonWelcomeViewModelTest.java @@ -1,9 +1,9 @@ package de.saxsys.mvvmfx.viewmodel.personwelcome; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import de.saxsys.mvvmfx.examples.welcome.model.Gender; import de.saxsys.mvvmfx.examples.welcome.model.Person; @@ -15,7 +15,7 @@ public class PersonWelcomeViewModelTest { private Repository repository; private PersonWelcomeViewModel personWelcomeViewModel; - @Before + @BeforeEach public void setup() { // TODO: this should be mocked repository = new Repository(); diff --git a/examples/pom.xml b/examples/pom.xml index 903d3e289..5627fb827 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -5,7 +5,7 @@ de.saxsys mvvmfx-parent - 1.6.0 + 1.7.0 de.saxsys.mvvmfx diff --git a/examples/todomvc-example/pom.xml b/examples/todomvc-example/pom.xml index f5d291f25..bb48a25d7 100644 --- a/examples/todomvc-example/pom.xml +++ b/examples/todomvc-example/pom.xml @@ -5,7 +5,7 @@ de.saxsys.mvvmfx examples - 1.6.0 + 1.7.0 4.0.0 diff --git a/mvvmfx-archetype/pom.xml b/mvvmfx-archetype/pom.xml index 4ff6f1c08..9f0b56d8e 100644 --- a/mvvmfx-archetype/pom.xml +++ b/mvvmfx-archetype/pom.xml @@ -5,7 +5,7 @@ de.saxsys mvvmfx-parent - 1.6.0 + 1.7.0 diff --git a/mvvmfx-archetype/src/main/resources/archetype-resources/pom.xml b/mvvmfx-archetype/src/main/resources/archetype-resources/pom.xml index 86c619a92..1626dddcb 100644 --- a/mvvmfx-archetype/src/main/resources/archetype-resources/pom.xml +++ b/mvvmfx-archetype/src/main/resources/archetype-resources/pom.xml @@ -16,7 +16,7 @@ de.saxsys mvvmfx-parent - 1.6.0 + 1.7.0 pom import diff --git a/mvvmfx-cdi/pom.xml b/mvvmfx-cdi/pom.xml index 562d0235a..1eddb5484 100644 --- a/mvvmfx-cdi/pom.xml +++ b/mvvmfx-cdi/pom.xml @@ -20,7 +20,7 @@ de.saxsys mvvmfx-parent - 1.6.0 + 1.7.0 mvvmfx-cdi @@ -34,7 +34,7 @@ maven-surefire-plugin - 2.18.1 + 2.19.1 always @@ -52,21 +52,18 @@ provided - - javax.inject - javax.inject - javax.enterprise cdi-api - - org.jboss.weld.se - weld-se-core - + + org.jboss.weld.se + weld-se-core + test + ch.qos.logback logback-classic @@ -78,8 +75,8 @@ test - junit - junit + org.junit.jupiter + junit-jupiter-api test diff --git a/mvvmfx-cdi/src/main/java/de/saxsys/mvvmfx/cdi/MvvmfxCdiApplication.java b/mvvmfx-cdi/src/main/java/de/saxsys/mvvmfx/cdi/MvvmfxCdiApplication.java index e53018be4..62c835de3 100644 --- a/mvvmfx-cdi/src/main/java/de/saxsys/mvvmfx/cdi/MvvmfxCdiApplication.java +++ b/mvvmfx-cdi/src/main/java/de/saxsys/mvvmfx/cdi/MvvmfxCdiApplication.java @@ -20,13 +20,12 @@ import javafx.stage.Stage; import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.se.SeContainer; +import javax.enterprise.inject.se.SeContainerInitializer; import javax.enterprise.inject.spi.BeanManager; import javax.enterprise.inject.spi.InjectionTarget; import javax.inject.Inject; -import org.jboss.weld.environment.se.Weld; -import org.jboss.weld.environment.se.WeldContainer; - import de.saxsys.mvvmfx.MvvmFX; import de.saxsys.mvvmfx.cdi.internal.MvvmfxProducer; @@ -37,29 +36,28 @@ * @author manuel.mauky */ public abstract class MvvmfxCdiApplication extends Application implements MvvmfxApplication { - - + private final BeanManager beanManager; private CreationalContext ctx; - private InjectionTarget injectionTarget; - private final Weld weld; - - @Inject + private InjectionTarget injectionTarget; + private final SeContainer container; + + @Inject private MvvmfxProducer producer; - - public MvvmfxCdiApplication() { - weld = new Weld(); - WeldContainer weldContainer = weld.initialize(); - - MvvmFX.setCustomDependencyInjector((type) -> weldContainer.instance().select(type).get()); - - MvvmfxProducer mvvmfxProducer = weldContainer.instance().select(MvvmfxProducer.class).get(); + + public MvvmfxCdiApplication() { + container = SeContainerInitializer + .newInstance() + .initialize(); + + MvvmFX.setCustomDependencyInjector((type) -> container.select(type).get()); + + MvvmfxProducer mvvmfxProducer = container.select(MvvmfxProducer.class).get(); mvvmfxProducer.setHostServices(getHostServices()); - - beanManager = weldContainer.getBeanManager(); - + + beanManager = container.getBeanManager(); } - + /** * This method is overridden to initialize the mvvmFX framework. Override the * {@link #startMvvmfx(javafx.stage.Stage)} method for your application entry point and startup code instead of this @@ -68,11 +66,11 @@ public MvvmfxCdiApplication() { @Override public final void start(Stage primaryStage) throws Exception { producer.setPrimaryStage(primaryStage); - + startMvvmfx(primaryStage); } - - + + /** * This method is called when the javafx application is initialized. See * {@link javafx.application.Application#init()} for more details. @@ -89,35 +87,35 @@ public final void init() throws Exception { ctx = beanManager.createCreationalContext(null); injectionTarget = beanManager.createInjectionTarget( beanManager.createAnnotatedType((Class) this.getClass())); - + injectionTarget.inject(this, ctx); injectionTarget.postConstruct(this); - + producer.setApplicationParameters(getParameters()); - + initMvvmfx(); } - - + + /** * This method is called when the application should stop. See {@link javafx.application.Application#stop()} for * more details. - * + * * Unlike the original stop method in {@link javafx.application.Application} this method contains logic to release * resources managed by the CDI container. Therefor it's important to call super.stop() when you * override this method. - * + * * @throws Exception */ @Override public final void stop() throws Exception { stopMvvmfx(); - + injectionTarget.preDestroy(this); injectionTarget.dispose(this); - + ctx.release(); - - weld.shutdown(); + + container.close(); } } diff --git a/mvvmfx-cdi/src/test/java/de/saxsys/mvvmfx/cdi/it/IntegrationTest.java b/mvvmfx-cdi/src/test/java/de/saxsys/mvvmfx/cdi/it/IntegrationTest.java index 205a360ce..b6d3395e5 100644 --- a/mvvmfx-cdi/src/test/java/de/saxsys/mvvmfx/cdi/it/IntegrationTest.java +++ b/mvvmfx-cdi/src/test/java/de/saxsys/mvvmfx/cdi/it/IntegrationTest.java @@ -3,12 +3,12 @@ import static org.assertj.core.api.Assertions.assertThat; import javafx.application.Application; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class IntegrationTest { - @Before + @BeforeEach public void setup() { MyApp.wasPostConstructCalled = false; MyApp.wasPreDestroyCalled = false; diff --git a/mvvmfx-easydi/pom.xml b/mvvmfx-easydi/pom.xml index b8315a1bf..8c028eece 100644 --- a/mvvmfx-easydi/pom.xml +++ b/mvvmfx-easydi/pom.xml @@ -5,7 +5,7 @@ mvvmfx-parent de.saxsys - 1.6.0 + 1.7.0 4.0.0 diff --git a/mvvmfx-guice/pom.xml b/mvvmfx-guice/pom.xml index 2dc50d965..13073b5cb 100644 --- a/mvvmfx-guice/pom.xml +++ b/mvvmfx-guice/pom.xml @@ -20,7 +20,7 @@ de.saxsys mvvmfx-parent - 1.6.0 + 1.7.0 mvvmfx-guice @@ -33,7 +33,7 @@ maven-surefire-plugin - 2.18.1 + 2.19.1 always @@ -63,8 +63,8 @@ test - junit - junit + org.junit.jupiter + junit-jupiter-api test diff --git a/mvvmfx-guice/src/test/java/de/saxsys/mvvmfx/guice/DuplicateInjectionBugTest.java b/mvvmfx-guice/src/test/java/de/saxsys/mvvmfx/guice/DuplicateInjectionBugTest.java index 4c4703ad2..f35bed8bc 100644 --- a/mvvmfx-guice/src/test/java/de/saxsys/mvvmfx/guice/DuplicateInjectionBugTest.java +++ b/mvvmfx-guice/src/test/java/de/saxsys/mvvmfx/guice/DuplicateInjectionBugTest.java @@ -7,7 +7,7 @@ import javax.inject.Inject; -import org.junit.Test; +import org.junit.jupiter.api.Test; /** * This test is used to reproduce a bug in the mvvmfx-guice module. A class that is injected into the application is diff --git a/mvvmfx-guice/src/test/java/de/saxsys/mvvmfx/guice/interceptiontest/InterceptorTest.java b/mvvmfx-guice/src/test/java/de/saxsys/mvvmfx/guice/interceptiontest/InterceptorTest.java index af1e31f23..b1171ae7f 100644 --- a/mvvmfx-guice/src/test/java/de/saxsys/mvvmfx/guice/interceptiontest/InterceptorTest.java +++ b/mvvmfx-guice/src/test/java/de/saxsys/mvvmfx/guice/interceptiontest/InterceptorTest.java @@ -1,7 +1,7 @@ package de.saxsys.mvvmfx.guice.interceptiontest; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; diff --git a/mvvmfx-guice/src/test/java/de/saxsys/mvvmfx/guice/it/IntegrationTest.java b/mvvmfx-guice/src/test/java/de/saxsys/mvvmfx/guice/it/IntegrationTest.java index ef42fb349..340af78f8 100644 --- a/mvvmfx-guice/src/test/java/de/saxsys/mvvmfx/guice/it/IntegrationTest.java +++ b/mvvmfx-guice/src/test/java/de/saxsys/mvvmfx/guice/it/IntegrationTest.java @@ -1,14 +1,14 @@ package de.saxsys.mvvmfx.guice.it; import de.saxsys.mvvmfx.guice.internal.GuiceInjector; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class IntegrationTest { - @Before + @BeforeEach public void setup() { MyApp.wasInitCalled = false; MyApp.wasStopCalled = false; diff --git a/mvvmfx-testing-utils/pom.xml b/mvvmfx-testing-utils/pom.xml index d36aaf67d..2c9d3b4c2 100644 --- a/mvvmfx-testing-utils/pom.xml +++ b/mvvmfx-testing-utils/pom.xml @@ -5,7 +5,7 @@ mvvmfx-parent de.saxsys - 1.6.0 + 1.7.0 4.0.0 @@ -25,6 +25,12 @@ junit junit + compile + + + org.junit.jupiter + junit-jupiter-api + compile org.mockito diff --git a/mvvmfx-testing-utils/src/main/java/de/saxsys/mvvmfx/testingutils/FxTestingUtils.java b/mvvmfx-testing-utils/src/main/java/de/saxsys/mvvmfx/testingutils/FxTestingUtils.java index 84101b7fa..f54ddcb6f 100644 --- a/mvvmfx-testing-utils/src/main/java/de/saxsys/mvvmfx/testingutils/FxTestingUtils.java +++ b/mvvmfx-testing-utils/src/main/java/de/saxsys/mvvmfx/testingutils/FxTestingUtils.java @@ -10,10 +10,15 @@ public class FxTestingUtils { - public static void waitForUiThread(long timeout) { + public static void runInFXThread(Runnable code){ + runInFXThread(code, 1000); + } + + public static void runInFXThread(Runnable code, long timeout){ CompletableFuture future = new CompletableFuture<>(); Platform.runLater(() -> { + code.run(); future.complete(null); }); @@ -24,7 +29,19 @@ public static void waitForUiThread(long timeout) { } } + /** + * This method is used to wait until the UI thread has done all work that was queued via + * {@link Platform#runLater(Runnable)}. + */ + public static void waitForUiThread(long timeout) { + runInFXThread(() -> {}, timeout); + } + + /** + * This method is used to wait until the UI thread has done all work that was queued via + * {@link Platform#runLater(Runnable)}. + */ public static void waitForUiThread() { - waitForUiThread(0); + waitForUiThread(1000); } } diff --git a/mvvmfx-testing-utils/src/main/java/de/saxsys/mvvmfx/testingutils/JfxToolkitExtension.java b/mvvmfx-testing-utils/src/main/java/de/saxsys/mvvmfx/testingutils/JfxToolkitExtension.java new file mode 100644 index 000000000..ff0f32a63 --- /dev/null +++ b/mvvmfx-testing-utils/src/main/java/de/saxsys/mvvmfx/testingutils/JfxToolkitExtension.java @@ -0,0 +1,11 @@ +package de.saxsys.mvvmfx.testingutils; + +import javafx.embed.swing.JFXPanel; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class JfxToolkitExtension implements BeforeAllCallback { + @Override public void beforeAll(ExtensionContext extensionContext) throws Exception { + new JFXPanel(); + } +} diff --git a/mvvmfx-testing-utils/src/test/java/de/saxsys/mvvmfx/testingutils/GCVerifierTest.java b/mvvmfx-testing-utils/src/test/java/de/saxsys/mvvmfx/testingutils/GCVerifierTest.java index d3ee95fd0..5a298c934 100644 --- a/mvvmfx-testing-utils/src/test/java/de/saxsys/mvvmfx/testingutils/GCVerifierTest.java +++ b/mvvmfx-testing-utils/src/test/java/de/saxsys/mvvmfx/testingutils/GCVerifierTest.java @@ -1,6 +1,6 @@ package de.saxsys.mvvmfx.testingutils; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; diff --git a/mvvmfx-utils/pom.xml b/mvvmfx-utils/pom.xml index 13d3d33b2..45906dca0 100644 --- a/mvvmfx-utils/pom.xml +++ b/mvvmfx-utils/pom.xml @@ -5,7 +5,7 @@ mvvmfx-parent de.saxsys - 1.6.0 + 1.7.0 4.0.0 @@ -30,8 +30,8 @@ - junit - junit + org.junit.jupiter + junit-jupiter-api test diff --git a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/listener/ListenerManagerTest.java b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/listener/ListenerManagerTest.java index 8a542641c..9e190a4c2 100644 --- a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/listener/ListenerManagerTest.java +++ b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/listener/ListenerManagerTest.java @@ -25,8 +25,8 @@ import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; @@ -53,7 +53,7 @@ public class ListenerManagerTest { private ListenerManager manager; - @Before + @BeforeEach public void setup() { manager = new ListenerManager(); simpleListProperty.set(FXCollections. observableArrayList()); diff --git a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToControlTest.java b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToControlTest.java index 59eb84a45..c4184e322 100644 --- a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToControlTest.java +++ b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToControlTest.java @@ -20,15 +20,15 @@ import javafx.scene.control.Control; import javafx.scene.control.ScrollPane; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class BindSizeToControlTest extends SizeBindingsBuilderTestBase { private Control toControl; - @Before + @BeforeEach public void setUp() { toControl = new ScrollPane(); } diff --git a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToImageViewTest.java b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToImageViewTest.java index 983da976c..6560dcb84 100644 --- a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToImageViewTest.java +++ b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToImageViewTest.java @@ -19,14 +19,14 @@ import javafx.scene.image.ImageView; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class BindSizeToImageViewTest extends SizeBindingsBuilderTestBase { private ImageView toImageView; - @Before + @BeforeEach public void setUp() { toImageView = new ImageView(); } diff --git a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToRectangleTest.java b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToRectangleTest.java index 3745fd323..3519a0f5e 100644 --- a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToRectangleTest.java +++ b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToRectangleTest.java @@ -19,8 +19,8 @@ import javafx.scene.shape.Rectangle; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class BindSizeToRectangleTest extends SizeBindingsBuilderTestBase { @@ -30,7 +30,7 @@ public class BindSizeToRectangleTest extends SizeBindingsBuilderTestBase { /** * Create elements which will bind to each other. */ - @Before + @BeforeEach public void setUp() { toRectangle = new Rectangle(); } diff --git a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToRegionTest.java b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToRegionTest.java index 0983f9789..6beedd19b 100644 --- a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToRegionTest.java +++ b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/BindSizeToRegionTest.java @@ -19,15 +19,15 @@ import javafx.scene.layout.Region; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class BindSizeToRegionTest extends SizeBindingsBuilderTestBase { private Region toRegion; - @Before + @BeforeEach public void setUp() { toRegion = new Region(); } diff --git a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/SizeBindingsBuilderTestBase.java b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/SizeBindingsBuilderTestBase.java index eafd8ad5a..a852c0ccb 100644 --- a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/SizeBindingsBuilderTestBase.java +++ b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/SizeBindingsBuilderTestBase.java @@ -17,7 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; import javafx.beans.property.ReadOnlyDoubleWrapper; import javafx.scene.control.Control; import javafx.scene.control.ScrollPane; @@ -25,12 +25,12 @@ import javafx.scene.layout.Region; import javafx.scene.shape.Rectangle; -import org.junit.Before; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.internal.util.reflection.Whitebox; -@RunWith(JfxRunner.class) +@ExtendWith(JfxToolkitExtension.class) public abstract class SizeBindingsBuilderTestBase { protected static final double SIZEVAL = 100d; @@ -43,7 +43,7 @@ public abstract class SizeBindingsBuilderTestBase { protected ImageView fromImageView; - @Before + @BeforeEach public void setup() { fromRegion = new Region(); mockSize(fromRegion); diff --git a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/UnbindHeightTest.java b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/UnbindHeightTest.java index 2eaf2cc82..91f896ab6 100644 --- a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/UnbindHeightTest.java +++ b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/UnbindHeightTest.java @@ -24,8 +24,8 @@ import javafx.scene.layout.Region; import javafx.scene.shape.Rectangle; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class UnbindHeightTest extends SizeBindingsBuilderTestBase { @@ -35,7 +35,7 @@ public class UnbindHeightTest extends SizeBindingsBuilderTestBase { private Region targetRegion; - @Before + @BeforeEach public void setUp() { targetImageView = new ImageView(); targetControl = new ScrollPane(); diff --git a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/UnbindSizeTest.java b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/UnbindSizeTest.java index d60cd0700..eaffe5e13 100644 --- a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/UnbindSizeTest.java +++ b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/UnbindSizeTest.java @@ -20,8 +20,8 @@ import javafx.scene.image.ImageView; import javafx.scene.layout.Region; import javafx.scene.shape.Rectangle; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import static de.saxsys.mvvmfx.utils.sizebinding.SizeBindingsBuilder.bindSize; import static de.saxsys.mvvmfx.utils.sizebinding.SizeBindingsBuilder.unbindSize; @@ -35,7 +35,7 @@ public class UnbindSizeTest extends SizeBindingsBuilderTestBase { private Region targetRegion; - @Before + @BeforeEach public void setUp() { targetImageView = new ImageView(); targetControl = new ScrollPane(); diff --git a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/UnbindWidthTest.java b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/UnbindWidthTest.java index 2fa7c3edd..a6c015717 100644 --- a/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/UnbindWidthTest.java +++ b/mvvmfx-utils/src/test/java/de/saxsys/mvvmfx/utils/sizebinding/UnbindWidthTest.java @@ -24,8 +24,8 @@ import javafx.scene.layout.Region; import javafx.scene.shape.Rectangle; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class UnbindWidthTest extends SizeBindingsBuilderTestBase { @@ -37,7 +37,7 @@ public class UnbindWidthTest extends SizeBindingsBuilderTestBase { private Region targetRegion; - @Before + @BeforeEach public void setUp() { targetImageView = new ImageView(); targetControl = new ScrollPane(); diff --git a/mvvmfx-validation/pom.xml b/mvvmfx-validation/pom.xml index 6b7ef3f1c..a73159445 100644 --- a/mvvmfx-validation/pom.xml +++ b/mvvmfx-validation/pom.xml @@ -20,7 +20,7 @@ de.saxsys mvvmfx-parent - 1.6.0 + 1.7.0 mvvmfx-validation @@ -38,7 +38,8 @@ org.controlsfx controlsfx - 8.40.9 + 8.40.12 + provided @@ -48,8 +49,8 @@ test - junit - junit + org.junit.jupiter + junit-jupiter-api test diff --git a/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/CompositeValidator.java b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/CompositeValidator.java index 7cb1fb1d3..9fab2cd53 100644 --- a/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/CompositeValidator.java +++ b/mvvmfx-validation/src/main/java/de/saxsys/mvvmfx/utils/validation/CompositeValidator.java @@ -15,15 +15,15 @@ ******************************************************************************/ package de.saxsys.mvvmfx.utils.validation; -import java.util.HashMap; -import java.util.Map; - import javafx.beans.property.ListProperty; import javafx.beans.property.SimpleListProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import java.util.HashMap; +import java.util.Map; + /** * This {@link Validator} implementation is used to compose multiple other validators. *

@@ -33,7 +33,7 @@ */ public class CompositeValidator implements Validator { - CompositeValidationStatus status = new CompositeValidationStatus(); + private CompositeValidationStatus status = new CompositeValidationStatus(); private ListProperty validators = new SimpleListProperty<>(FXCollections.observableArrayList()); private Map> listenerMap = new HashMap<>(); @@ -91,6 +91,13 @@ public CompositeValidator(Validator... validators) { } + /** + * @return an unmodifiable observable list of validators composed by this CompositeValidator. + */ + public ObservableList getValidators() { + return FXCollections.unmodifiableObservableList(this.validators); + } + public void addValidators(Validator... validators) { this.validators.addAll(validators); } diff --git a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/CompositeValidatorTest.java b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/CompositeValidatorTest.java index d35d58495..6ad27b75c 100644 --- a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/CompositeValidatorTest.java +++ b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/CompositeValidatorTest.java @@ -15,20 +15,18 @@ ******************************************************************************/ package de.saxsys.mvvmfx.utils.validation; -import static org.assertj.core.api.Assertions.assertThat; - import com.google.common.base.Strings; - -import org.junit.Before; -import org.junit.Test; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.IntegerBinding; +import javafx.beans.property.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.List; +import java.util.function.Predicate; import java.util.stream.Collectors; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; +import static org.assertj.core.api.Assertions.assertThat; /** * @author manuel.mauky @@ -43,7 +41,7 @@ public class CompositeValidatorTest { private ObservableRuleBasedValidator validator1; private ObservableRuleBasedValidator validator2; - @Before + @BeforeEach public void setup() { validator1 = new ObservableRuleBasedValidator(); validator1.addRule(valid1, ValidationMessage.error("error1")); @@ -246,4 +244,84 @@ private List asStrings(List messages) { .map(ValidationMessage::getMessage) .collect(Collectors.toList()); } + + + /** + * Issue #413 + */ + @Test + public void validatorPercentageTest(){ + + final IntegerProperty integerProperty1 = new SimpleIntegerProperty(30); + final IntegerProperty integerProperty2 = new SimpleIntegerProperty(-20); + final IntegerProperty integerProperty3 = new SimpleIntegerProperty(35); + final IntegerProperty integerProperty4 = new SimpleIntegerProperty(55); + + Predicate predicate1 = v -> v.doubleValue() > 50; + + final Validator validator1 = new FunctionBasedValidator<>(integerProperty1, predicate1, ValidationMessage.error("Value must be bigger than 50")); + final Validator validator2 = new FunctionBasedValidator<>(integerProperty2, predicate1, ValidationMessage.error("Value must be bigger than 50")); + final Validator validator3 = new FunctionBasedValidator<>(integerProperty3, predicate1, ValidationMessage.error("Value must be bigger than 50")); + final Validator validator4 = new FunctionBasedValidator<>(integerProperty4, predicate1, ValidationMessage.error("Value must be bigger than 50")); + + final CompositeValidator compositeValidator = new CompositeValidator(validator1, validator2, validator3); + assertThat(compositeValidator.getValidationStatus().getMessages()).hasSize(3); + + IntegerBinding percentage = Bindings.createIntegerBinding(() -> { + int numberOfValidators = compositeValidator.getValidators().size(); + + if (numberOfValidators == 0) { + return 100; + } else { + int numberOfValidValidators = (int) compositeValidator.getValidators().stream() + .map(Validator::getValidationStatus) + .filter(ValidationStatus::isValid) + .count(); + + return numberOfValidValidators * 100 / numberOfValidators; + } + }, compositeValidator.getValidationStatus().getMessages(), compositeValidator.getValidators()); + + + assertThat(percentage.intValue()).isEqualTo(0); + + // change values + integerProperty1.setValue(70); + assertThat(percentage.intValue()).isEqualTo(33); + integerProperty1.setValue(0); + assertThat(percentage.intValue()).isEqualTo(0); + + + integerProperty2.setValue(100); + assertThat(percentage.intValue()).isEqualTo(33); + + integerProperty1.setValue(70); + assertThat(percentage.intValue()).isEqualTo(66); + + integerProperty2.setValue(50); + assertThat(percentage.intValue()).isEqualTo(33); + + // add new Validator + compositeValidator.addValidators(validator4); + assertThat(percentage.intValue()).isEqualTo(50); + + // 0% valid + integerProperty1.setValue(50); + integerProperty4.setValue(50); + assertThat(percentage.intValue()).isEqualTo(0); + + // 100% valid + integerProperty1.setValue(51); + integerProperty2.setValue(100); + integerProperty3.setValue(80); + integerProperty4.setValue(75); + assertThat(percentage.intValue()).isEqualTo(100); + + // remove validator + compositeValidator.removeValidators(validator1); + assertThat(percentage.intValue()).isEqualTo(100); + integerProperty2.setValue(30); + assertThat(percentage.intValue()).isEqualTo(66); + + } } diff --git a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/CustomValidationMessageTest.java b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/CustomValidationMessageTest.java index 58bda73f7..2a2cb7d0a 100644 --- a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/CustomValidationMessageTest.java +++ b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/CustomValidationMessageTest.java @@ -3,7 +3,7 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.ObservableList; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.function.Predicate; diff --git a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/FunctionBasedValidatorTest.java b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/FunctionBasedValidatorTest.java index 17fbc3a0e..57ade2205 100644 --- a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/FunctionBasedValidatorTest.java +++ b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/FunctionBasedValidatorTest.java @@ -17,7 +17,7 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.function.Function; import java.util.function.Predicate; diff --git a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/HighestMessageBugTest.java b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/HighestMessageBugTest.java index 6b97518d1..cd0825483 100644 --- a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/HighestMessageBugTest.java +++ b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/HighestMessageBugTest.java @@ -24,8 +24,8 @@ import javafx.beans.property.StringProperty; import javafx.collections.ListChangeListener; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; /** * This test case reproduces the bug #264 @@ -40,7 +40,7 @@ public class HighestMessageBugTest { private ValidationStatus validationStatus; - @Before + @BeforeEach public void setUp() throws Exception { value = new SimpleStringProperty(""); validator = new FunctionBasedValidator<>(value, v -> v != null, ValidationMessage.error("error")); diff --git a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ObservableRulesTest.java b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ObservableRulesTest.java index 758f6589f..11ef17931 100644 --- a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ObservableRulesTest.java +++ b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ObservableRulesTest.java @@ -18,7 +18,7 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableBooleanValue; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.regex.Pattern; diff --git a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ObservableRulesValidatorTest.java b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ObservableRulesValidatorTest.java index 8f617f5e7..ac3e86286 100644 --- a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ObservableRulesValidatorTest.java +++ b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ObservableRulesValidatorTest.java @@ -17,8 +17,8 @@ import de.saxsys.mvvmfx.testingutils.GCVerifier; import javafx.beans.property.*; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.regex.Pattern; @@ -33,7 +33,7 @@ public class ObservableRulesValidatorTest { private ObservableRuleBasedValidator validator; - @Before + @BeforeEach public void setup() { rule1 = new SimpleBooleanProperty(); rule2 = new SimpleBooleanProperty(); diff --git a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ValidationMessageTest.java b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ValidationMessageTest.java index d0e7abd12..b6d5ccbb0 100644 --- a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ValidationMessageTest.java +++ b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ValidationMessageTest.java @@ -17,7 +17,7 @@ import nl.jqno.equalsverifier.EqualsVerifier; import nl.jqno.equalsverifier.Warning; -import org.junit.Test; +import org.junit.jupiter.api.Test; public class ValidationMessageTest { diff --git a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ValidationStatusTest.java b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ValidationStatusTest.java index 3c74b55cf..f654e5f1a 100644 --- a/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ValidationStatusTest.java +++ b/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/ValidationStatusTest.java @@ -15,7 +15,7 @@ ******************************************************************************/ package de.saxsys.mvvmfx.utils.validation; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; diff --git a/mvvmfx/pom.xml b/mvvmfx/pom.xml index 85162d2a8..6ca6c9219 100644 --- a/mvvmfx/pom.xml +++ b/mvvmfx/pom.xml @@ -20,7 +20,7 @@ de.saxsys mvvmfx-parent - 1.6.0 + 1.7.0 mvvmfx @@ -44,13 +44,6 @@ doc-annotations - - org.controlsfx - controlsfx - 8.40.9 - provided - - @@ -59,8 +52,8 @@ test - junit - junit + org.junit.jupiter + junit-jupiter-api test diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/FluentViewLoader.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/FluentViewLoader.java index 9df55f7a6..aa8cb29ad 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/FluentViewLoader.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/FluentViewLoader.java @@ -1,5 +1,6 @@ package de.saxsys.mvvmfx; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -8,6 +9,7 @@ import de.saxsys.mvvmfx.internal.viewloader.FxmlViewLoader; import de.saxsys.mvvmfx.internal.viewloader.JavaViewLoader; import de.saxsys.mvvmfx.internal.viewloader.ResourceBundleManager; +import javafx.util.BuilderFactory; /** * Fluent API for loading Views.
@@ -61,7 +63,7 @@ public class FluentViewLoader { public static class JavaViewStep, ViewModelType extends ViewModel> { private final Class viewType; - private ResourceBundle resourceBundle; + private List resourceBundles; private ViewModelType viewModel; private ViewType codeBehind; @@ -94,20 +96,27 @@ public JavaViewStep providedScopes(Collection pr * view. Note: It is possible to provide a global application-wide * resourceBundle via * {@link MvvmFX#setGlobalResourceBundle(ResourceBundle)} method. - * + *

* If there is a global resourceBundle set it will be merged with the * resourceBundle provided by this builder method. The resourceBundle * provided by this method will have a higher priority then the global * one which means that if there are duplicate keys, the values of the * global resourceBundle will be overwritten and the values of this * resourceBundle will be used. + *

+ * It is possible to add multiple resourceBundles by invoking this builder method + * multiple times. In this case the last provided resourceBundle will have the + * highest priority when it comes to overwriting values with the same keys. * * @param resourceBundle * the resource bundle that is used while loading the view. * @return this instance of the builder step. */ public JavaViewStep resourceBundle(ResourceBundle resourceBundle) { - this.resourceBundle = resourceBundle; + if(resourceBundles == null) { + resourceBundles = new ArrayList<>(); + } + resourceBundles.add(resourceBundle); return this; } @@ -152,12 +161,12 @@ public JavaViewStep codeBehind(ViewType codeBehind) { public ViewTuple load() { JavaViewLoader javaViewLoader = new JavaViewLoader(); - return javaViewLoader.loadJavaViewTuple(viewType, - ResourceBundleManager.getInstance().mergeWithGlobal(resourceBundle), viewModel, codeBehind, context, - providedScopes); - } + final ResourceBundle bundle = ResourceBundleManager.getInstance().mergeListWithGlobal(resourceBundles); - } + return javaViewLoader.loadJavaViewTuple(viewType, bundle, viewModel, codeBehind, context, + providedScopes); + } + } /** * This class is the builder step to load a fxml based view. It is accessed @@ -174,13 +183,15 @@ public ViewTuple load() { public static class FxmlViewStep, ViewModelType extends ViewModel> { private final Class viewType; - private ResourceBundle resourceBundle; + private List resourceBundles; private Object root; private ViewType codeBehind; private ViewModelType viewModel; private Context context; private Collection providedScopes; + private List builderFactories; + FxmlViewStep(Class viewType) { this.viewType = viewType; } @@ -207,20 +218,27 @@ public FxmlViewStep providedScopes(Collection pr * view. Note: It is possible to provide a global application-wide * resourceBundle via * {@link MvvmFX#setGlobalResourceBundle(ResourceBundle)} method. - * + *

* If there is a global resourceBundle set it will be merged with the * resourceBundle provided by this builder method. The resourceBundle * provided by this method will have a higher priority then the global * one which means that if there are duplicate keys, the values of the * global resourceBundle will be overwritten and the values of this * resourceBundle will be used. + *

+ * It is possible to add multiple resourceBundles by invoking this builder method + * multiple times. In this case the last provided resourceBundle will have the + * highest priority when it comes to overwriting values with the same keys. * * @param resourceBundle * the resource bundle that is used while loading the view. * @return this instance of the builder step. */ public FxmlViewStep resourceBundle(ResourceBundle resourceBundle) { - this.resourceBundle = resourceBundle; + if(resourceBundles == null) { + resourceBundles = new ArrayList<>(); + } + resourceBundles.add(resourceBundle); return this; } @@ -273,6 +291,34 @@ public FxmlViewStep viewModel(ViewModelType viewModel) return this; } + /** + * This param is used to add a {@link BuilderFactory} that is used when loading the view.
. + * MvvmFX supports multiple builder factories. There are two ways of defining builder factories: + *

    + *
  1. a local builder factory by using this method. + * In this case the builder factory is only used for this loading procedure.
  2. + *
  3. a global builder factory by using {@link MvvmFX#addGlobalBuilderFactory(BuilderFactory)}. + * This defines a global builder factory that is used for all loading procedures.
  4. + *
+ *
+ * For most use cases it's better to define a global builder factory. + * Only if you like to limit the usage of the + * builder factory to only this loading procedure then use this fluent API method instead. + * + * @param builderFactory a builder factory that is used only for this loading procedure. + * + * @return this instance of the builder step. + */ + public FxmlViewStep builderFactory(BuilderFactory builderFactory) { + if(this.builderFactories == null) { + this.builderFactories = new ArrayList<>(); + } + + this.builderFactories.add(builderFactory); + + return this; + } + /** * The final step of the Fluent API. This method loads the view based on * the given params. @@ -282,10 +328,11 @@ public FxmlViewStep viewModel(ViewModelType viewModel) public ViewTuple load() { FxmlViewLoader fxmlViewLoader = new FxmlViewLoader(); - return fxmlViewLoader.loadFxmlViewTuple(viewType, - ResourceBundleManager.getInstance().mergeWithGlobal(resourceBundle), codeBehind, root, viewModel, - context, providedScopes); - } + final ResourceBundle bundle = ResourceBundleManager.getInstance().mergeListWithGlobal(resourceBundles); + + return fxmlViewLoader.loadFxmlViewTuple(viewType, bundle, codeBehind, root, viewModel, + context, providedScopes, builderFactories); + } } /** diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/FxmlPath.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/FxmlPath.java new file mode 100644 index 000000000..ba34993e1 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/FxmlPath.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright 2017 Rafael Guillen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is used to define a {@link FxmlView} custom + * FXML file path. An empty file paths will be ignored. + * + * Note that the full path to the FXML file must be provided. + * + * Please be aware that this annotation only effects the parent + * view that is loaded by the {@link FluentViewLoader}. + * Views that are included via "" tag aren't + * affected because the path is then determined by the value of + * the "src" attribute in the include-tag. + * + * Example:
+ *
+ *
+ * package example.view;
+ *
+ *{@literal @}FxmlPath("/fxml/CustomPathView.fxml")
+ * public class CustomView implements {@link FxmlView} {
+ *
+ *         ...
+ *
+ * }
+ * 
+ * + * @author rafael.guillen + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface FxmlPath { + + /** + * Custom fxml file path, empty by default + * @return path to the fxml file + */ + String value() default ""; +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/Initialize.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/Initialize.java new file mode 100644 index 000000000..681cbf263 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/Initialize.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright 2017 Gleb Koval + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * This annotation is used to mark a method in a ViewModel to be called after all mvvmFX injections are finished. + * It's possible to use this annotation on multiple methods in the ViewModel and all these methods will be invoked. + * However, you may not depend on any invocation order.
+ * + * This approach can be combined with the default naming convention of JavaFX. By this convention a method + * with the signature public initialize() will be invoked after initialization is finished.
+ * + * While the naming convention approach only works with a "public" method, this {@link Initialize} annotation can + * also be used on "private"/"protected"/default scope methods. + * + * Example:
+ *
+ * + *
+ * public class SomeViewModel implements {@link ViewModel} {
+ *
+ *         // mvvmFx injections
+ *        {@literal @}{@link InjectScope}
+ *         private SomeScope someScope;
+ *         ...
+ *
+ *        {@literal @}Initialize
+ *         private void init() {
+ *             someScope.subscribe(...);
+ *             ...
+ *         }
+ * }
+ * 
+ * + * @author Gleb Koval, Manuel Mauky + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Initialize { +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/InjectScope.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/InjectScope.java index 5bec70b34..4f77a9d12 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/InjectScope.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/InjectScope.java @@ -6,18 +6,11 @@ import java.lang.annotation.Target; /** + * This annotation is used to inject a {@link Scope} object into a {@link ViewModel}. * * @author alexander.casall - * */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface InjectScope { - - /** - * the id of the scope. - * Default is "" which means that the scope is global. - */ - String value() default ""; - } \ No newline at end of file diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/MvvmFX.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/MvvmFX.java index c24ac6196..8f710168e 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/MvvmFX.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/MvvmFX.java @@ -15,7 +15,9 @@ ******************************************************************************/ package de.saxsys.mvvmfx; +import de.saxsys.mvvmfx.internal.viewloader.GlobalBuilderFactory; import de.saxsys.mvvmfx.internal.viewloader.ResourceBundleManager; +import javafx.util.BuilderFactory; import javafx.util.Callback; import de.saxsys.mvvmfx.utils.notifications.NotificationCenter; @@ -65,4 +67,29 @@ public static void setCustomDependencyInjector(final Callback, Object> public static void setGlobalResourceBundle(ResourceBundle resourceBundle) { ResourceBundleManager.getInstance().setGlobalResourceBundle(resourceBundle); } + + /** + * Add a {@link BuilderFactory} to be used by mvvmFX. + *
+ * A {@link BuilderFactory} is used to enable custom controls that need special initialization to be used with FXML. + * MvvmFX can manage multiple builder factories. If you add multiple factories that can provide builders for the same type, + * the last added builder factory will be used. This way it's possible to "overwrite" a more abstract builder factory with a more specific + * factory. + *
+ * MvvmFX also takes care for handling the default {@link javafx.fxml.JavaFXBuilderFactory}. If no custom builder factory + * is able to provide a builder for a given type the default JavaFX builder factory will be used as last resort. + * This way you don't have to take care for standard JavaFX types in your builder factory. + *
+ * While most of the time using a global builder factory is the best approach, for some special use cases + * it's needed to define a special builder factory that is only used for a single loading procedure. + * For such use cases one can use the {@link FluentViewLoader} with the parameter + * {@link de.saxsys.mvvmfx.FluentViewLoader.FxmlViewStep#builderFactory(BuilderFactory)} instead. + * In this case the provided builder factory is again combined with the global factories (if defined). + * Builder factories provided via {@link FluentViewLoader} have a higher priority then global builder factories. + * + * @param factory the builder factory + */ + public static void addGlobalBuilderFactory(BuilderFactory factory) { + GlobalBuilderFactory.getInstance().addBuilderFactory(factory); + } } diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/SideEffect.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/SideEffect.java new file mode 100644 index 000000000..7f371c3e3 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/SideEffect.java @@ -0,0 +1,13 @@ +package de.saxsys.mvvmfx.internal; + +/** + * A functional interface that is used in this class to express callbacks that don't take any argument and don't + * return anything. Such a callback has to work only by side effects. + * + * This interface doesn't allow the implementation to throw checked exceptions. + * If checked exceptions are needed, use {@link SideEffectWithException} instead. + */ +@FunctionalInterface +public interface SideEffect { + void call(); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/SideEffectWithException.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/SideEffectWithException.java new file mode 100644 index 000000000..418579ff8 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/SideEffectWithException.java @@ -0,0 +1,13 @@ +package de.saxsys.mvvmfx.internal; + +/** + * A functional interface that is used in this class to express callbacks that don't take any argument and don't + * return anything. Such a callback has to work only by side effects. + * + * This interface allows the implementation to throw checked exceptions. Therefore the caller has to handle this exception. + * If no checked exceptions are needed, use {@link SideEffect} instead. + */ +@FunctionalInterface +public interface SideEffectWithException { + void call() throws Exception; +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/FxmlViewLoader.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/FxmlViewLoader.java index 415c068f4..b900b61dd 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/FxmlViewLoader.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/FxmlViewLoader.java @@ -19,12 +19,14 @@ import de.saxsys.mvvmfx.Scope; import de.saxsys.mvvmfx.ViewModel; import de.saxsys.mvvmfx.ViewTuple; +import de.saxsys.mvvmfx.FxmlPath; import de.saxsys.mvvmfx.internal.ContextImpl; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableBooleanValue; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; +import javafx.util.BuilderFactory; import javafx.util.Callback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,6 +34,8 @@ import java.io.IOException; import java.net.URL; import java.util.Collection; +import java.util.List; +import java.util.Optional; import java.util.ResourceBundle; import java.util.function.Consumer; @@ -47,7 +51,7 @@ public class FxmlViewLoader { /** * Load the viewTuple by it`s ViewType. - * + * * @param viewType * the type of the view to be loaded. * @param resourceBundle @@ -66,30 +70,35 @@ public class FxmlViewLoader { * the generic type of the view. * @param * the generic type of the viewModel. - * @return the loaded ViewTuple. + * @param builderFactories a list of custom builder factories. may be null + * @return the loaded ViewTuple. */ public , ViewModelType extends ViewModel> ViewTuple loadFxmlViewTuple( - Class viewType, ResourceBundle resourceBundle, ViewType codeBehind, Object root, - ViewModelType viewModel, Context context, Collection providedScopes) { + Class viewType, ResourceBundle resourceBundle, ViewType codeBehind, Object root, + ViewModelType viewModel, Context context, Collection providedScopes, + List builderFactories) { final String pathToFXML = createFxmlPath(viewType); - return loadFxmlViewTuple(pathToFXML, resourceBundle, codeBehind, root, viewModel, context, providedScopes); + return loadFxmlViewTuple(pathToFXML, resourceBundle, codeBehind, root, viewModel, context, providedScopes, builderFactories); } /** * This method is used to create a String with the path to the FXML file for * a given View class. - * + * * This is done by taking the package of the view class (if any) and replace * "." with "/". After that the Name of the class and the file ending * ".fxml" is appended. - * + * + * If the View class is annotated with @FxmlPath then the String path supplied + * in the annotation value will be used. + * * Example: de.saxsys.myapp.ui.MainView as view class will be transformed to * "/de/saxsys/myapp/ui/MainView.fxml" - * + * * Example 2: MainView (located in the default package) will be transformed * to "/MainView.fxml" - * + * * @param viewType * the view class type. * @return the path to the fxml file as string. @@ -97,22 +106,32 @@ public , ViewModelType extends Vi private String createFxmlPath(Class viewType) { final StringBuilder pathBuilder = new StringBuilder(); - pathBuilder.append("/"); + final FxmlPath pathAnnotation = viewType.getDeclaredAnnotation(FxmlPath.class); //Get annotation from view + final String fxmlPath = Optional.ofNullable(pathAnnotation) + .map(FxmlPath::value) + .map(String::trim) + .orElse(""); - if (viewType.getPackage() != null) { - pathBuilder.append(viewType.getPackage().getName().replaceAll("\\.", "/")); + if (fxmlPath.isEmpty()) { pathBuilder.append("/"); - } - pathBuilder.append(viewType.getSimpleName()); - pathBuilder.append(".fxml"); + if (viewType.getPackage() != null) { + pathBuilder.append(viewType.getPackage().getName().replaceAll("\\.", "/")); + pathBuilder.append("/"); + } + + pathBuilder.append(viewType.getSimpleName()); + pathBuilder.append(".fxml"); + } else { + pathBuilder.append(fxmlPath); + } return pathBuilder.toString(); } /** * Load the viewTuple by the path of the fxml file. - * + * * @param resource * the string path to the fxml file that is loaded. * @param resourceBundle @@ -131,11 +150,13 @@ private String createFxmlPath(Class viewType) { * the generic type of the view. * @param * the generic type of the viewModel. - * @return the loaded ViewTuple. + * @param builderFactories a list of custom builder factories. may be null + * @return the loaded ViewTuple. */ public , ViewModelType extends ViewModel> ViewTuple loadFxmlViewTuple( - final String resource, ResourceBundle resourceBundle, final ViewType codeBehind, final Object root, - ViewModelType viewModel, Context parentContext, Collection providedScopes) { + final String resource, ResourceBundle resourceBundle, final ViewType codeBehind, final Object root, + ViewModelType viewModel, Context parentContext, Collection providedScopes, + List builderFactories) { try { // FIXME Woanders hin? @@ -145,7 +166,7 @@ public , ViewModelType extends Vi // for the SceneLifecycle we need to know when the view is put into the scene BooleanProperty viewInSceneProperty = new SimpleBooleanProperty(); - final FXMLLoader loader = createFxmlLoader(resource, resourceBundle, codeBehind, root, viewModel, context, viewInSceneProperty); + final FXMLLoader loader = createFxmlLoader(resource, resourceBundle, codeBehind, root, viewModel, context, viewInSceneProperty, builderFactories); loader.load(); @@ -210,7 +231,8 @@ public , ViewModelType extends Vi } private FXMLLoader createFxmlLoader(String resource, ResourceBundle resourceBundle, View codeBehind, Object root, - ViewModel viewModel, ContextImpl context, ObservableBooleanValue viewInSceneProperty) throws IOException { + ViewModel viewModel, ContextImpl context, ObservableBooleanValue viewInSceneProperty, + List builderFactories) throws IOException { // Load FXML file final URL location = FxmlViewLoader.class.getResource(resource); if (location == null) { @@ -223,6 +245,14 @@ private FXMLLoader createFxmlLoader(String resource, ResourceBundle resourceBund fxmlLoader.setResources(resourceBundle); fxmlLoader.setLocation(location); + if(builderFactories == null || builderFactories.isEmpty()) { + fxmlLoader.setBuilderFactory(GlobalBuilderFactory.getInstance()); + } else { + BuilderFactory factory = GlobalBuilderFactory.getInstance().mergeWith(builderFactories); + fxmlLoader.setBuilderFactory(factory); + } + + // when the user provides a viewModel but no codeBehind, we need to use // the custom controller factory. // in all other cases the default factory can be used. @@ -268,6 +298,13 @@ public DefaultControllerFactory(ResourceBundle resourceBundle, ContextImpl conte public Object call(Class type) { Object controller = DependencyInjector.getInstance().getInstanceOf(type); + //throw an exception if the fx:controller was of type ViewModel + if (controller instanceof ViewModel) { + throw new IllegalStateException("A ViewModel class [" + controller.getClass().getCanonicalName() + "] was referenced in an FXML file" + + " as the fx:controller." + + " Instead a class that implements FxmlView has to be defined as the fx:controller in the FXML file."); + } + if (controller instanceof View) { View codeBehind = (View) controller; @@ -377,6 +414,14 @@ public Object call(Class type) { handleInjection(codeBehind, resourceBundle, context, viewInSceneProperty); } + //throw an exception if the fx:controller was of type ViewModel + if (controller instanceof ViewModel) { + throw new IllegalStateException("A ViewModel class [" + controller.getClass().getCanonicalName() + "] was referenced in an FXML file" + + " as the fx:controller." + + " Instead a class that implements FxmlView has to be defined as the fx:controller in the FXML file."); + } + + return controller; } } diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/GlobalBuilderFactory.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/GlobalBuilderFactory.java new file mode 100644 index 000000000..e32c900b8 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/GlobalBuilderFactory.java @@ -0,0 +1,78 @@ +package de.saxsys.mvvmfx.internal.viewloader; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + +import javafx.fxml.JavaFXBuilderFactory; +import javafx.util.Builder; +import javafx.util.BuilderFactory; + +/** + * A {@link BuilderFactory} that can manage multiple custom builder factories. + *
+ * This class is part of the internal API of mvvmFX and may be subject to changes. + * Don't use this class directly. Instead add new builder factories by using public API: + * {@link de.saxsys.mvvmfx.MvvmFX#addGlobalBuilderFactory(BuilderFactory)}. + */ +public class GlobalBuilderFactory implements BuilderFactory { + + private List factories = new ArrayList<>(); + + private static final GlobalBuilderFactory SINGLETON = new GlobalBuilderFactory(); + + private BuilderFactory defaultBuilderFactory = new JavaFXBuilderFactory(); + + private GlobalBuilderFactory() { + } + + public static GlobalBuilderFactory getInstance() { + return SINGLETON; + } + + @Override + public Builder getBuilder(Class type) { + // if no factories are defined yet, use the default factory. + // this is an optimization for performance reasons to prevent unnecessary iterator handling + // for a very common usage scenario + if(factories.isEmpty()) { + return defaultBuilderFactory.getBuilder(type); + } + + Builder builder = null; + + final ListIterator listIterator = factories.listIterator(factories.size()); + + // iterate builder list in reverse order + while(listIterator.hasPrevious() && builder == null) { + final BuilderFactory factory = listIterator.previous(); + + builder = factory.getBuilder(type); + } + + // if no custom builderFactory is suitable for this type, use the default one. + if(builder == null) { + builder = defaultBuilderFactory.getBuilder(type); + } + + return builder; + } + + /** + * Create a new BuilderFactory instance that contains all custom factories of this instance + * combined with the list of factories passed as argument to this method. + *
+ * This instance of the builderFactory is not changed by this method. + */ + public BuilderFactory mergeWith(List factories) { + GlobalBuilderFactory factory = new GlobalBuilderFactory(); + + factory.factories.addAll(factories); + + return factory; + } + + public void addBuilderFactory(BuilderFactory factory) { + this.factories.add(factory); + } +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ReflectionUtils.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ReflectionUtils.java index c9b681bbb..65b3de91e 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ReflectionUtils.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ReflectionUtils.java @@ -15,7 +15,10 @@ ******************************************************************************/ package de.saxsys.mvvmfx.internal.viewloader; +import de.saxsys.mvvmfx.internal.SideEffectWithException; + import java.lang.annotation.Annotation; +import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.security.AccessController; import java.security.PrivilegedAction; @@ -31,15 +34,7 @@ * @author manuel.mauky */ public class ReflectionUtils { - /** - * A functional interface that is used in this class to express callbacks that don't take any argument and don't - * return anything. Such a callback have to work only by side effects. - */ - @FunctionalInterface - public static interface SideEffect { - void call() throws Exception; - } - + /** * Returns all fields with the given annotation. Only fields that are declared in the actual class of the instance * are considered (i.e. no fields from super classes). This includes private fields. @@ -89,13 +84,13 @@ public static List getFieldsFromClassHierarchy(Class type) { /** - * Helper method to execute a callback on a given field. This method encapsulates the error handling logic and the - * handling of accessibility of the field. + * Helper method to execute a callback on a given member. This method encapsulates the error handling logic and the + * handling of accessibility of the member. * - * After the callback is executed the accessibility of the field will be reset to the originally state. + * After the callback is executed the accessibility of the member will be reset to the originally state. * - * @param field - * the field that is made accessible to run the callback + * @param member + * the member that is made accessible to run the callback * @param callable * the callback that will be executed. * @param errorMessage @@ -106,19 +101,19 @@ public static List getFieldsFromClassHierarchy(Class type) { * @throws IllegalStateException * when something went wrong. */ - public static T accessField(final Field field, final Callable callable, String errorMessage) { + public static T accessMember(final AccessibleObject member, final Callable callable, String errorMessage) { if (callable == null) { return null; } return AccessController.doPrivileged((PrivilegedAction) () -> { - boolean wasAccessible = field.isAccessible(); + boolean wasAccessible = member.isAccessible(); try { - field.setAccessible(true); + member.setAccessible(true); return callable.call(); } catch (Exception exception) { throw new IllegalStateException(errorMessage, exception); } finally { - field.setAccessible(wasAccessible); + member.setAccessible(wasAccessible); } }); } @@ -136,21 +131,21 @@ public static T accessField(final Field field, final Callable callable, S * the new value that the field should be set to. */ public static void setField(final Field field, Object target, Object value) { - accessField(field, () -> field.set(target, value), + accessMember(field, () -> field.set(target, value), "Cannot set the field [" + field.getName() + "] of instance [" + target + "] to value [" + value + "]"); } /** - * Helper method to execute a callback on a given field. This method encapsulates the error handling logic and the - * handling of accessibility of the field. The difference to - * {@link ReflectionUtils#accessField(Field, Callable, String)} is that this method takes a callback that doesn't + * Helper method to execute a callback on a given member. This method encapsulates the error handling logic and the + * handling of accessibility of the member. The difference to + * {@link ReflectionUtils#accessMember(AccessibleObject, Callable, String)} is that this method takes a callback that doesn't * return anything but only creates a sideeffect. * - * After the callback is executed the accessibility of the field will be reset to the originally state. + * After the callback is executed the accessibility of the member will be reset to the originally state. * - * @param field - * the field that is made accessible to run the callback + * @param member + * the member that is made accessible to run the callback * @param sideEffect * the callback that will be executed. * @param errorMessage @@ -159,19 +154,19 @@ public static void setField(final Field field, Object target, Object value) { * @throws IllegalStateException * when something went wrong. */ - public static void accessField(final Field field, final SideEffect sideEffect, String errorMessage) { + public static void accessMember(final AccessibleObject member, final SideEffectWithException sideEffect, String errorMessage) { if (sideEffect == null) { return; } - AccessController.doPrivileged((PrivilegedAction) () -> { - boolean wasAccessible = field.isAccessible(); + AccessController.doPrivileged((PrivilegedAction) () -> { + boolean wasAccessible = member.isAccessible(); try { - field.setAccessible(true); + member.setAccessible(true); sideEffect.call(); } catch (Exception exception) { throw new IllegalStateException(errorMessage, exception); } finally { - field.setAccessible(wasAccessible); + member.setAccessible(wasAccessible); } return null; }); diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ResourceBundleManager.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ResourceBundleManager.java index 70de22c47..b9ee75d6a 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ResourceBundleManager.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ResourceBundleManager.java @@ -18,10 +18,12 @@ import eu.lestard.doc.Internal; import sun.util.ResourceBundleEnumeration; +import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.ListResourceBundle; import java.util.Locale; import java.util.Map; @@ -87,9 +89,50 @@ public ResourceBundle mergeWithGlobal(ResourceBundle resourceBundle) { return merge(resourceBundle, globalResourceBundle); } } - } + } + + + private static ResourceBundle reduce(List bundles) { + return bundles.stream() + .filter(Objects::nonNull) + .reduce(EMPTY_RESOURCE_BUNDLE, (a, b) -> merge(b, a)); + } + + /** + * Merges the list of ResourceBundles with the global one (if any). + *

+ * The global resourceBundle has a lower priority then the provided ones. If there is the same key defined in the + * global and in one of the provided resourceBundles, the value from the provided resourceBundle will be used. + *

+ * The order of resourceBundles in the list defines the priority for resourceBundles. + * ResourceBundles at the start of the list have a lower priority compared to bundles + * at the end of the list. This means that the last resourceBundle will overwrite values from previous resourceBundles + * (including the global resourceBundle (if any)). * + * + * @param bundles + * a list of resourceBundles that will be merged. null is accepted. + * @return the merged resourceBundle. + */ + public ResourceBundle mergeListWithGlobal(List bundles) { + if (globalResourceBundle == null) { + if (bundles == null) { + return EMPTY_RESOURCE_BUNDLE; + } else { + return reduce(bundles); + } + } else { + if (bundles == null) { + return new ResourceBundleWrapper(globalResourceBundle); + } else { + final List resourceBundles = new ArrayList<>(); + resourceBundles.add(globalResourceBundle); + resourceBundles.addAll(bundles); + return reduce(resourceBundles); + } + } + } - private ResourceBundle merge(ResourceBundle highPriority, ResourceBundle lowPriority) { + private static ResourceBundle merge(ResourceBundle highPriority, ResourceBundle lowPriority) { return new MergedResourceBundle(highPriority, lowPriority); } diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtils.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtils.java index 30fe3cbc7..c49447fe4 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtils.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtils.java @@ -16,6 +16,7 @@ package de.saxsys.mvvmfx.internal.viewloader; import de.saxsys.mvvmfx.Context; +import de.saxsys.mvvmfx.Initialize; import de.saxsys.mvvmfx.InjectContext; import de.saxsys.mvvmfx.InjectScope; import de.saxsys.mvvmfx.InjectViewModel; @@ -30,10 +31,12 @@ import javax.annotation.PostConstruct; import java.lang.annotation.Annotation; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.AccessController; import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.function.Consumer; @@ -166,7 +169,7 @@ public static , ViewModelType ext Optional fieldOptional = getViewModelField(view.getClass(), viewModelType); if (fieldOptional.isPresent()) { Field field = fieldOptional.get(); - return ReflectionUtils.accessField(field, () -> (ViewModelType) field.get(view), + return ReflectionUtils.accessMember(field, () -> (ViewModelType) field.get(view), "Can't get the viewModel of type <" + viewModelType + ">"); } else { return null; @@ -189,7 +192,7 @@ public static void injectViewModel(final View view, ViewModel viewModel) { final Optional fieldOptional = getViewModelField(view.getClass(), viewModel.getClass()); if (fieldOptional.isPresent()) { Field field = fieldOptional.get(); - ReflectionUtils.accessField(field, () -> { + ReflectionUtils.accessMember(field, () -> { Object existingViewModel = field.get(view); if (existingViewModel == null) { field.set(view, viewModel); @@ -258,7 +261,7 @@ public static , VM extends ViewModel> void createAn if (fieldOptional.isPresent()) { Field field = fieldOptional.get(); - ReflectionUtils.accessField(field, () -> { + ReflectionUtils.accessMember(field, () -> { Object existingViewModel = field.get(view); if (existingViewModel == null) { @@ -294,7 +297,7 @@ static void createAndInjectScopes(Object viewModel, ContextImpl context) { List scopeFields = getScopeFields(viewModel.getClass()); scopeFields.forEach(scopeField -> { - ReflectionUtils.accessField(scopeField, () -> injectScopeIntoField(scopeField, viewModel, context), + ReflectionUtils.accessMember(scopeField, () -> injectScopeIntoField(scopeField, viewModel, context), "Can't inject Scope into ViewModel <" + viewModel.getClass() + ">"); }); } @@ -305,7 +308,7 @@ public static void injectContext(View codeBehind, ContextImpl context) { if (contextField.isPresent()) { Field field = contextField.get(); - ReflectionUtils.accessField(field, () -> { + ReflectionUtils.accessMember(field, () -> { field.set(codeBehind, context); }, "Can't inject Context into the view <" + codeBehind + ">"); } @@ -372,10 +375,10 @@ public static , ViewModelType ext } /** - * If a ViewModel has a method with the signature - * public void initialize() it will be invoked. If no such - * method is available nothing happens. - * + * If a ViewModel has a method annotated with {@link Initialize} + * or method with the signature public void initialize() + * it will be invoked. If no such method is available nothing happens. + * * @param viewModel * the viewModel that's initialize method (if available) will be * invoked. @@ -386,30 +389,46 @@ public static void initializeViewModel(ViewMod if (viewModel == null) { return; } - try { - final Method initMethod = viewModel.getClass().getMethod("initialize"); - // if there is a @PostConstruct annotation, throw an exception to prevent double injection - if(initMethod.isAnnotationPresent(PostConstruct.class)) { - throw new IllegalStateException(String.format("initialize method of ViewModel [%s] is annotated with @PostConstruct. " + - "This will lead to unexpected behaviour and duplicate initialization. " + - "Please rename the method or remove the @PostConstruct annotation. " + - "See mvvmFX wiki for more details: " + - "https://github.com/sialcasa/mvvmFX/wiki/Dependency-Injection#lifecycle-postconstruct", viewModel)); - } - AccessController.doPrivileged((PrivilegedAction) () -> { - try { - return initMethod.invoke(viewModel); - } catch (InvocationTargetException | IllegalAccessException e) { - throw new IllegalStateException( - "mvvmFX wasn't able to call the initialize method of ViewModel [" + viewModel + "].", e); - } - }); - } catch (NoSuchMethodException e) { - // it's perfectly fine that a ViewModel has no initialize method. - } + final Collection initializeMethods = getInitializeMethods(viewModel.getClass()); + + initializeMethods.forEach(initMethod -> { + // if there is a @PostConstruct annotation, throw an exception to prevent double injection + if(initMethod.isAnnotationPresent(PostConstruct.class)) { + throw new IllegalStateException(String.format("initialize method of ViewModel [%s] is annotated with @PostConstruct. " + + "This will lead to unexpected behaviour and duplicate initialization. " + + "Please rename the method or remove the @PostConstruct annotation. " + + "See mvvmFX wiki for more details: " + + "https://github.com/sialcasa/mvvmFX/wiki/Dependency-Injection#lifecycle-postconstruct", viewModel)); + } + + ReflectionUtils.accessMember(initMethod, () -> initMethod.invoke(viewModel), "mvvmFX wasn't able to call the initialize method of ViewModel [" + viewModel + "]."); + }); } + /** + * Returns a collection of {@link Method}s that represent initializer methods. + * A method is an "initializer method" if it either:
+ *

    + *
  1. has a signature of "public void initialize()"
  2. + *
  3. is annotated with {@link Initialize}
  4. + *
+ */ + private static Collection getInitializeMethods(Class classType) { + final List initializeMethods = new ArrayList<>(); + Arrays.stream(classType.getMethods()) + .filter(method -> "initialize".equals(method.getName())) + .findAny() + .ifPresent(initializeMethods::add); + + Arrays.stream(classType.getDeclaredMethods()) + .filter(method -> method.isAnnotationPresent(Initialize.class)) + .forEach(initializeMethods::add); + + return initializeMethods; + } + + /** * This method adds listeners for the {@link SceneLifecycle}. */ diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/BeanListPropertyField.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/BeanListPropertyField.java new file mode 100644 index 000000000..25b5429ae --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/BeanListPropertyField.java @@ -0,0 +1,82 @@ +package de.saxsys.mvvmfx.utils.mapping; + +import de.saxsys.mvvmfx.internal.SideEffect; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ListGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ListSetter; +import javafx.beans.property.ListProperty; +import javafx.beans.property.Property; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * An implementation of {@link PropertyField} that is used when the field of the model class is a {@link List} and + * is not a JavaFX ListProperty but is following the old Java-Beans standard, i.e. there is getter and setter + * method for the field. + * + * @param + * @param + * the type of the list elements. + */ +class BeanListPropertyField, R extends Property> + implements PropertyField { + + private final ListGetter getter; + private final ListSetter setter; + + private List defaultValue; + private final ListProperty targetProperty; + + public BeanListPropertyField(SideEffect updateFunction, ListGetter getter, ListSetter setter, Supplier> propertySupplier) { + this(updateFunction, getter, setter, propertySupplier, Collections.emptyList()); + } + + public BeanListPropertyField(SideEffect updateFunction, ListGetter getter, ListSetter setter, Supplier> propertySupplier, List defaultValue) { + this.defaultValue = defaultValue; + this.getter = getter; + this.setter = setter; + this.targetProperty = propertySupplier.get(); + this.targetProperty.setValue(FXCollections.observableArrayList()); + + this.targetProperty.addListener((ListChangeListener) change -> updateFunction.call()); + } + + @Override + public void commit(M wrappedObject) { + setter.accept(wrappedObject, targetProperty.getValue()); + } + + @Override + public void reload(M wrappedObject) { + targetProperty.setAll(getter.apply(wrappedObject)); + } + + @Override + public void resetToDefault() { + targetProperty.setAll(defaultValue); + } + + @Override + public void updateDefault(final M wrappedObject) { + defaultValue = new ArrayList<>(getter.apply(wrappedObject)); + } + + @Override + public R getProperty() { + return (R) targetProperty; + } + + @Override + public boolean isDifferent(M wrappedObject) { + final List modelValue = getter.apply(wrappedObject); + final List wrapperValue = targetProperty; + + return !Objects.equals(modelValue, wrapperValue); + } +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/BeanPropertyField.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/BeanPropertyField.java new file mode 100644 index 000000000..1e8301fc3 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/BeanPropertyField.java @@ -0,0 +1,72 @@ +package de.saxsys.mvvmfx.utils.mapping; + +import de.saxsys.mvvmfx.internal.SideEffect; +import javafx.beans.property.Property; + +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * An implementation of {@link PropertyField} that is used when the fields of the model class are not JavaFX + * Properties but are following the old Java-Beans standard, i.e. there are getter and setter method for each field. + * + * @param + */ +class BeanPropertyField> implements PropertyField { + + private final R targetProperty; + private T defaultValue; + + private final Function getter; + private final BiConsumer setter; + + public BeanPropertyField(SideEffect updateFunction, Function getter, + BiConsumer setter, Supplier propertySupplier) { + this(updateFunction, getter, setter, null, propertySupplier); + } + + public BeanPropertyField(SideEffect updateFunction, Function getter, + BiConsumer setter, T defaultValue, Supplier propertySupplier) { + this.defaultValue = defaultValue; + this.getter = getter; + this.setter = setter; + this.targetProperty = propertySupplier.get(); + + this.targetProperty.addListener((observable, oldValue, newValue) -> updateFunction.call()); + } + + @Override + public void commit(M wrappedObject) { + setter.accept(wrappedObject, targetProperty.getValue()); + } + + @Override + public void reload(M wrappedObject) { + targetProperty.setValue(getter.apply(wrappedObject)); + } + + @Override + public void resetToDefault() { + targetProperty.setValue(defaultValue); + } + + @Override + public void updateDefault(final M wrappedObject) { + defaultValue = getter.apply(wrappedObject); + } + + @Override + public R getProperty() { + return targetProperty; + } + + @Override + public boolean isDifferent(M wrappedObject) { + final T modelValue = getter.apply(wrappedObject); + final T wrapperValue = targetProperty.getValue(); + + return !Objects.equals(modelValue, wrapperValue); + } +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/FxListPropertyField.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/FxListPropertyField.java new file mode 100644 index 000000000..47e75632e --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/FxListPropertyField.java @@ -0,0 +1,78 @@ +package de.saxsys.mvvmfx.utils.mapping; + +import de.saxsys.mvvmfx.internal.SideEffect; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ListPropertyAccessor; +import javafx.beans.property.ListProperty; +import javafx.beans.property.Property; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * An implementation of {@link PropertyField} that is used when the field of the model class is a {@link List} and + * will be mapped to a JavaFX {@link ListProperty}. + * + * @param + * @param + * the type of the list elements. + */ +class FxListPropertyField, R extends Property> + implements PropertyField { + + private List defaultValue; + private final ListPropertyAccessor accessor; + private final ListProperty targetProperty; + + public FxListPropertyField(SideEffect updateFunction, ListPropertyAccessor accessor, Supplier> propertySupplier) { + this(updateFunction, accessor, propertySupplier, Collections.emptyList()); + } + + public FxListPropertyField(SideEffect updateFunction, ListPropertyAccessor accessor, Supplier> propertySupplier, List defaultValue) { + this.accessor = accessor; + this.defaultValue = defaultValue; + + this.targetProperty = propertySupplier.get(); + this.targetProperty.setValue(FXCollections.observableArrayList()); + + this.targetProperty.addListener((ListChangeListener) change -> updateFunction.call()); + } + + @Override + public void commit(M wrappedObject) { + accessor.apply(wrappedObject).setAll(targetProperty.getValue()); + } + + @Override + public void reload(M wrappedObject) { + targetProperty.setAll(accessor.apply(wrappedObject).getValue()); + } + + @Override + public void resetToDefault() { + targetProperty.setAll(defaultValue); + } + + @Override + public void updateDefault(final M wrappedObject) { + defaultValue = new ArrayList<>(accessor.apply(wrappedObject).getValue()); + } + + @Override + public R getProperty() { + return (R) targetProperty; + } + + @Override + public boolean isDifferent(M wrappedObject) { + final List modelValue = accessor.apply(wrappedObject).getValue(); + final List wrapperValue = targetProperty; + + return !Objects.equals(modelValue, wrapperValue); + } +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/FxPropertyField.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/FxPropertyField.java new file mode 100644 index 000000000..3d9fc4ae3 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/FxPropertyField.java @@ -0,0 +1,68 @@ +package de.saxsys.mvvmfx.utils.mapping; + +import de.saxsys.mvvmfx.internal.SideEffect; +import javafx.beans.property.Property; + +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * An implementation of {@link PropertyField} that is used when the fields of the model class are JavaFX Properties + * too. + * + * @param + */ +class FxPropertyField> implements PropertyField { + + private T defaultValue; + private final Function> accessor; + private final R targetProperty; + + public FxPropertyField(SideEffect updateFunction, Function> accessor, Supplier> propertySupplier) { + this(updateFunction, accessor, null, propertySupplier); + } + + @SuppressWarnings("unchecked") + public FxPropertyField(SideEffect updateFunction, Function> accessor, T defaultValue, + Supplier> propertySupplier) { + this.accessor = accessor; + this.defaultValue = defaultValue; + this.targetProperty = (R) propertySupplier.get(); + + this.targetProperty.addListener((observable, oldValue, newValue) -> updateFunction.call()); + } + + @Override + public void commit(M wrappedObject) { + accessor.apply(wrappedObject).setValue(targetProperty.getValue()); + } + + @Override + public void reload(M wrappedObject) { + targetProperty.setValue(accessor.apply(wrappedObject).getValue()); + } + + @Override + public void resetToDefault() { + targetProperty.setValue(defaultValue); + } + + @Override + public void updateDefault(final M wrappedObject) { + defaultValue = accessor.apply(wrappedObject).getValue(); + } + + @Override + public R getProperty() { + return targetProperty; + } + + @Override + public boolean isDifferent(M wrappedObject) { + final T modelValue = accessor.apply(wrappedObject).getValue(); + final T wrapperValue = targetProperty.getValue(); + + return !Objects.equals(modelValue, wrapperValue); + } +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ImmutableBeanPropertyField.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ImmutableBeanPropertyField.java new file mode 100644 index 000000000..2d0844a02 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ImmutableBeanPropertyField.java @@ -0,0 +1,76 @@ +package de.saxsys.mvvmfx.utils.mapping; + +import de.saxsys.mvvmfx.internal.SideEffect; +import javafx.beans.property.Property; + +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +public class ImmutableBeanPropertyField> implements ImmutablePropertyField { + + private T defaultValue; + private final R targetProperty; + + private final Function getter; + private final BiFunction immutableSetter; + + + public ImmutableBeanPropertyField(SideEffect updateFunction, Function getter, + BiFunction immutableSetter, Supplier propertySupplier) { + this(updateFunction, getter, immutableSetter, null, propertySupplier); + } + + public ImmutableBeanPropertyField(SideEffect updateFunction, Function getter, + BiFunction immutableSetter, T defaultValue, Supplier propertySupplier){ + this.getter = getter; + this.immutableSetter = immutableSetter; + this.defaultValue = defaultValue; + + this.targetProperty = propertySupplier.get(); + this.targetProperty.addListener(((observable, oldValue, newValue) -> updateFunction.call())); + } + + + + @Override + public void commit(M wrappedObject) { + // commit is not supported because the model instance is immutable. + } + + @Override + public M commitImmutable(M wrappedObject) { + return immutableSetter.apply(wrappedObject, targetProperty.getValue()); + } + + + @Override + public void reload(M wrappedObject) { + targetProperty.setValue(getter.apply(wrappedObject)); + } + + + @Override + public void resetToDefault() { + targetProperty.setValue(defaultValue); + } + + @Override + public void updateDefault(M wrappedObject) { + this.defaultValue = getter.apply(wrappedObject); + } + + @Override + public R getProperty() { + return targetProperty; + } + + @Override + public boolean isDifferent(M wrappedObject) { + final T modelValue = getter.apply(wrappedObject); + final T wrapperValue = targetProperty.getValue(); + + return !Objects.equals(modelValue, wrapperValue); + } +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ImmutableListPropertyField.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ImmutableListPropertyField.java new file mode 100644 index 000000000..64cfb4c7a --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ImmutableListPropertyField.java @@ -0,0 +1,81 @@ +package de.saxsys.mvvmfx.utils.mapping; + +import de.saxsys.mvvmfx.internal.SideEffect; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ListGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ListImmutableSetter; +import javafx.beans.property.ListProperty; +import javafx.beans.property.Property; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +public class ImmutableListPropertyField, R extends Property> + implements ImmutablePropertyField { + + private final ListGetter getter; + private final ListImmutableSetter immutableSetter; + + private List defaultValue; + private final ListProperty targetProperty; + + public ImmutableListPropertyField( + SideEffect updateFunction, + ListGetter getter, ListImmutableSetter immutableSetter, + Supplier> propertySupplier) { + this(updateFunction, getter, immutableSetter, propertySupplier, Collections.emptyList()); + } + + public ImmutableListPropertyField(SideEffect updateFunction, ListGetter getter, ListImmutableSetter immutableSetter, Supplier> propertySupplier, List defaultValue) { + this.defaultValue = defaultValue; + this.getter = getter; + this.immutableSetter = immutableSetter; + this.targetProperty = propertySupplier.get(); + this.targetProperty.setValue(FXCollections.observableArrayList()); + + this.targetProperty.addListener((ListChangeListener) change -> updateFunction.call()); + } + + @Override + public void commit(M wrappedObject) { + // commit is not supported because the model instance is immutable. + } + + @Override + public M commitImmutable(M wrappedObject) { + return immutableSetter.apply(wrappedObject, targetProperty.getValue()); + } + + @Override + public void reload(M wrappedObject) { + targetProperty.setAll(getter.apply(wrappedObject)); + } + + @Override + public void resetToDefault() { + targetProperty.setAll(defaultValue); + } + + @Override + public void updateDefault(M wrappedObject) { + defaultValue = new ArrayList<>(getter.apply(wrappedObject)); + } + + @Override + public R getProperty() { + return (R) targetProperty; + } + + @Override + public boolean isDifferent(M wrappedObject) { + final List modelValue = getter.apply(wrappedObject); + final List wrapperValue = targetProperty; + + return !Objects.equals(modelValue, wrapperValue); + } +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ImmutablePropertyField.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ImmutablePropertyField.java new file mode 100644 index 000000000..a420d1590 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ImmutablePropertyField.java @@ -0,0 +1,9 @@ +package de.saxsys.mvvmfx.utils.mapping; + +import javafx.beans.property.Property; + +public interface ImmutablePropertyField> extends PropertyField { + + M commitImmutable(M wrappedObject); + +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapper.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapper.java index d6b9771a9..d67660a8b 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapper.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapper.java @@ -16,41 +16,44 @@ package de.saxsys.mvvmfx.utils.mapping; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.BooleanGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.BooleanImmutableSetter; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.BooleanPropertyAccessor; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.BooleanSetter; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.DoubleGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.DoubleImmutableSetter; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.DoublePropertyAccessor; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.DoubleSetter; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.FloatGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.FloatImmutableSetter; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.FloatPropertyAccessor; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.FloatSetter; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.IntGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.IntImmutableSetter; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.IntPropertyAccessor; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.IntSetter; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ListGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ListImmutableSetter; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ListPropertyAccessor; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ListSetter; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.LongGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.LongImmutableSetter; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.LongPropertyAccessor; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.LongSetter; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ObjectGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ObjectImmutableSetter; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ObjectPropertyAccessor; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.ObjectSetter; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.StringGetter; +import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.StringImmutableSetter; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.StringPropertyAccessor; import de.saxsys.mvvmfx.utils.mapping.accessorfunctions.StringSetter; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.Function; -import java.util.function.Supplier; import eu.lestard.doc.Beta; import javafx.beans.property.BooleanProperty; @@ -73,8 +76,6 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; /** @@ -99,9 +100,9 @@ *
  • if we are creating a new model instance and the user clicks "reset" we want all UI fields to be reset to a * meaningful default value
  • * - * + * *

    - * + * * These requirements are quite common but there is a lot of code needed to copy between the model and the viewModel. * Additionally we have a tight coupling because every time the structure of the model changes (for example a field is * removed) we have several places in the viewModel that need to be adjusted. @@ -111,68 +112,68 @@ *

    * The model class: *

    - * + * *

      * public class Person {
      * 	private String name;
      * 	private String familyName;
      * 	private int age;
    - * 	
    + *
      * 	public String getName() {
      * 		return name;
      * 	}
    - * 	
    + *
      * 	public void setName(String name) {
      * 		this.name = name;
      * 	}
    - * 	
    + *
      * 	public String getFamilyName() {
      * 		return familyName;
      * 	}
    - * 	
    + *
      * 	public void setFamilyName(String familyName) {
      * 		this.familyName = familyName;
      * 	}
    - * 	
    + *
      * 	public int getAge() {
      * 		return age;
      * 	}
    - * 	
    + *
      * 	public void setAge(int age) {
      * 		this.age = age;
      * 	}
      * }
      * 
    - * + * * Without {@link ModelWrapper}: *

    - * + * *

      * public class PersonViewModel implements ViewModel {
    - * 	
    + *
      * 	private StringProperty name = new SimpleStringProperty();
      * 	private StringProperty familyName = new SimpleStringProperty();
      * 	private IntegerProperty age = new SimpleIntegerProperty();
    - * 	
    + *
      * 	private Person person;
    - * 	
    + *
      * 	public void init(Person person) {
      * 		this.person = person;
      * 		reloadFromModel();
      * 	}
    - * 	
    + *
      * 	public void reset() {
      * 		this.name.setValue("");
      * 		this.familyName.setValue("");
      * 		this.age.setValue(0);
      * 	}
    - * 	
    + *
      * 	public void reloadFromModel() {
      * 		this.name.setValue(person.getName());
      * 		this.familyName.setValue(person.getFamilyName());
      * 		this.age.setValue(person.getAge());
      * 	}
    - * 	
    + *
      * 	public void save() {
      * 		if (someValidation() && person != null) {
      * 			person.setName(name.getValue());
    @@ -180,61 +181,61 @@
      * 			person.setAge(age.getValue());
      * 		}
      * 	}
    - * 	
    + *
      * 	public StringProperty nameProperty() {
      * 		return name;
      * 	}
    - * 	
    + *
      * 	public StringProperty familyNameProperty() {
      * 		return familyName;
      * 	}
    - * 	
    + *
      * 	public IntegerProperty ageProperty() {
      * 		return age;
      * 	}
      * }
      * 
    - * + * * With {@link ModelWrapper}: *

    - * + * *

      *         public class PersonViewModel implements ViewModel {
      *              private ModelWrapper{@code} wrapper = new ModelWrapper{@code<>}();
    - * 
    + *
      *             public void init(Person person) {
      *                  wrapper.set(person);
      *                  wrapper.reload();
      *             }
    - * 
    + *
      *             public void reset() {
      *                 wrapper.reset();
      *             }
    - * 
    + *
      *             public void reloadFromModel(){
      *                 wrapper.reload();
      *             }
    - * 
    + *
      *             public void save() {
      *                 if (someValidation()) {
      *                     wrapper.commit();
      *                 }
      *             }
    - * 
    + *
      *             public StringProperty nameProperty(){
      *                 return wrapper.field("name", Person::getName, Person::setName, "");
      *             }
    - * 
    + *
      *             public StringProperty familyNameProperty(){
      *                 return wrapper.field("familyName", Person::getFamilyName, Person::setFamilyName, "");
      *             }
    - * 
    + *
      *             public IntegerProperty ageProperty() {
      *                 return wrapper.field("age", Person::getAge, Person::setAge, 0);
      *             }
      *         }
      * 
    - * + * * In the first example without the {@link ModelWrapper} we have several lines of code that are specific for each field * of the model. If we would add a new field to the model (for example "email") then we would have to update several * pieces of code in the ViewModel. @@ -242,310 +243,35 @@ * On the other hand in the example with the {@link ModelWrapper} there is only the definition of the Property accessors * in the bottom of the class that is specific to the fields of the Model. For each field we have only one place in the * ViewModel that would need an update when the structure of the model changes. - * - * - * + * + * + * * @param * the type of the model class. */ @Beta public class ModelWrapper { - + private final ReadOnlyBooleanWrapper dirtyFlag = new ReadOnlyBooleanWrapper(); private final ReadOnlyBooleanWrapper diffFlag = new ReadOnlyBooleanWrapper(); - /** - * This interface defines the operations that are possible for each field of a wrapped class. - * - * @param - * target type. The base type of the returned property, f.e. {@link String}. - * @param - * model type. The type of the Model class, that is wrapped by this ModelWrapper instance. - * @param - * return type. The type of the Property that is returned via {@link #getProperty()}, f.e. - * {@link StringProperty} or {@link Property}. - */ - private interface PropertyField> { - void commit(M wrappedObject); - - void reload(M wrappedObject); - - void resetToDefault(); - - void updateDefault(final M wrappedObject); - - R getProperty(); - - /** - * Determines if the value in the model object and the property field are different or not. - * - * This method is used to implement the {@link #differentProperty()} flag. - * - * @param wrappedObject - * the wrapped model object - * @return false if both the wrapped model object and the property field have the same value, - * otherwise true - */ - boolean isDifferent(M wrappedObject); - } - - /** - * An implementation of {@link PropertyField} that is used when the fields of the model class are JavaFX Properties - * too. - * - * @param - */ - private class FxPropertyField> implements PropertyField { - - private T defaultValue; - private final Function> accessor; - private final R targetProperty; - - public FxPropertyField(Function> accessor, Supplier> propertySupplier) { - this(accessor, null, propertySupplier); - } - - @SuppressWarnings("unchecked") - public FxPropertyField(Function> accessor, T defaultValue, - Supplier> propertySupplier) { - this.accessor = accessor; - this.defaultValue = defaultValue; - this.targetProperty = (R) propertySupplier.get(); - - this.targetProperty.addListener((observable, oldValue, newValue) -> propertyWasChanged()); - } - - @Override - public void commit(M wrappedObject) { - accessor.apply(wrappedObject).setValue(targetProperty.getValue()); - } - - @Override - public void reload(M wrappedObject) { - targetProperty.setValue(accessor.apply(wrappedObject).getValue()); - } - - @Override - public void resetToDefault() { - targetProperty.setValue(defaultValue); - } - - @Override - public void updateDefault(final M wrappedObject) { - defaultValue = accessor.apply(wrappedObject).getValue(); - } - - @Override - public R getProperty() { - return targetProperty; - } - - @Override - public boolean isDifferent(M wrappedObject) { - final T modelValue = accessor.apply(wrappedObject).getValue(); - final T wrapperValue = targetProperty.getValue(); - - return !Objects.equals(modelValue, wrapperValue); - } - } - - /** - * An implementation of {@link PropertyField} that is used when the fields of the model class are not JavaFX - * Properties but are following the old Java-Beans standard, i.e. there are getter and setter method for each field. - * - * @param - */ - private class BeanPropertyField> implements PropertyField { - - private final R targetProperty; - private T defaultValue; - - private final Function getter; - private final BiConsumer setter; - - public BeanPropertyField(Function getter, - BiConsumer setter, Supplier propertySupplier) { - this(getter, setter, null, propertySupplier); - } - - public BeanPropertyField(Function getter, - BiConsumer setter, T defaultValue, Supplier propertySupplier) { - this.defaultValue = defaultValue; - this.getter = getter; - this.setter = setter; - this.targetProperty = propertySupplier.get(); - - this.targetProperty.addListener((observable, oldValue, newValue) -> propertyWasChanged()); - } - - @Override - public void commit(M wrappedObject) { - setter.accept(wrappedObject, targetProperty.getValue()); - } - - @Override - public void reload(M wrappedObject) { - targetProperty.setValue(getter.apply(wrappedObject)); - } - - @Override - public void resetToDefault() { - targetProperty.setValue(defaultValue); - } - - @Override - public void updateDefault(final M wrappedObject) { - defaultValue = getter.apply(wrappedObject); - } + private final Set> fields = new LinkedHashSet<>(); + private final Map> identifiedFields = new HashMap<>(); - @Override - public R getProperty() { - return targetProperty; - } - - @Override - public boolean isDifferent(M wrappedObject) { - final T modelValue = getter.apply(wrappedObject); - final T wrapperValue = targetProperty.getValue(); - - return !Objects.equals(modelValue, wrapperValue); - } - } - - /** - * An implementation of {@link PropertyField} that is used when the field of the model class is a {@link List} and - * will be mapped to a JavaFX {@link ListProperty}. - * - * @param - * @param - * the type of the list elements. - */ - private class FxListPropertyField, R extends Property> - implements PropertyField { - - private List defaultValue; - private final ListPropertyAccessor accessor; - private final ListProperty targetProperty; - - public FxListPropertyField(ListPropertyAccessor accessor, Supplier> propertySupplier) { - this(accessor, propertySupplier, Collections.emptyList()); - } - - public FxListPropertyField(ListPropertyAccessor accessor, Supplier> propertySupplier, List defaultValue) { - this.accessor = accessor; - this.defaultValue = defaultValue; - - this.targetProperty = propertySupplier.get(); - this.targetProperty.setValue(FXCollections.observableArrayList()); - - this.targetProperty.addListener((ListChangeListener) change -> ModelWrapper.this.propertyWasChanged()); - } - - @Override - public void commit(M wrappedObject) { - accessor.apply(wrappedObject).setAll(targetProperty.getValue()); - } - - @Override - public void reload(M wrappedObject) { - targetProperty.setAll(accessor.apply(wrappedObject).getValue()); - } - - @Override - public void resetToDefault() { - targetProperty.setAll(defaultValue); - } + private final Set> immutableFields = new LinkedHashSet<>(); - @Override - public void updateDefault(final M wrappedObject) { - defaultValue = new ArrayList<>(accessor.apply(wrappedObject).getValue()); - } + private final ObjectProperty model; - @Override - public R getProperty() { - return (R) targetProperty; - } - - @Override - public boolean isDifferent(M wrappedObject) { - final List modelValue = accessor.apply(wrappedObject).getValue(); - final List wrapperValue = targetProperty; - - return !Objects.equals(modelValue, wrapperValue); - } - } - /** - * An implementation of {@link PropertyField} that is used when the field of the model class is a {@link List} and - * is not a JavaFX ListProperty but is following the old Java-Beans standard, i.e. there is getter and setter - * method for the field. - * - * @param - * @param - * the type of the list elements. + * This flag is needed to support immutable fields. Without immutables when {@link #commit()} is invoked, + * the fields of the model instance are changed. With immutables however on commit the instance of the model itself is + * replaced. By default when the model instance is changed a {@link #reload()} is executed. + * This is ok when the user changes the model instance. It is not ok when we replace the model instance because of immutable fields. + * For this reason we need to distinguish between a change of the model instance due to a commit with immutable fields# + * and when the user changes the model instance. Therefore durring commit this flag will switch to true + * to indicate that we are currently executing a commit. */ - private class BeanListPropertyField, R extends Property> - implements PropertyField { - - private final ListGetter getter; - private final ListSetter setter; - - private List defaultValue; - private final ListProperty targetProperty; - - public BeanListPropertyField(ListGetter getter, ListSetter setter, Supplier> propertySupplier) { - this(getter, setter, propertySupplier, Collections.emptyList()); - } - - public BeanListPropertyField(ListGetter getter, ListSetter setter, Supplier> propertySupplier, List defaultValue) { - this.defaultValue = defaultValue; - this.getter = getter; - this.setter = setter; - this.targetProperty = propertySupplier.get(); - this.targetProperty.setValue(FXCollections.observableArrayList()); - - this.targetProperty.addListener((ListChangeListener) change -> propertyWasChanged()); - } - - @Override - public void commit(M wrappedObject) { - setter.accept(wrappedObject, targetProperty.getValue()); - } - - @Override - public void reload(M wrappedObject) { - targetProperty.setAll(getter.apply(wrappedObject)); - } - - @Override - public void resetToDefault() { - targetProperty.setAll(defaultValue); - } - - @Override - public void updateDefault(final M wrappedObject) { - defaultValue = new ArrayList<>(getter.apply(wrappedObject)); - } - - @Override - public R getProperty() { - return (R) targetProperty; - } - - @Override - public boolean isDifferent(M wrappedObject) { - final List modelValue = getter.apply(wrappedObject); - final List wrapperValue = targetProperty; - - return !Objects.equals(modelValue, wrapperValue); - } - } - - private final Set> fields = new LinkedHashSet<>(); - private final Map> identifiedFields = new HashMap<>(); - - private final ObjectProperty model; - + private boolean inCommitPhase = false; /** * Create a new instance of {@link ModelWrapper} that wraps the instance of the Model class wrapped by the property. @@ -558,20 +284,26 @@ public ModelWrapper(ObjectProperty model) { this.model = model; reload(); this.model.addListener((observable, oldValue, newValue) -> { - reload(); + /* + * Only reload the values from the new model instance when it was changed by the user and not when it was changed + * during the commit phase. + */ + if(!inCommitPhase) { + reload(); + } }); } /** * Create a new instance of {@link ModelWrapper} that wraps the given instance of the Model class. - * + * * @param model * the element of the model that will be wrapped. */ public ModelWrapper(M model) { this(new SimpleObjectProperty<>(model)); } - + /** * Create a new instance of {@link ModelWrapper} that is empty at the moment. You have to define the model element * that should be wrapped afterwards with the {@link #set(Object)} method. @@ -579,17 +311,17 @@ public ModelWrapper(M model) { public ModelWrapper() { this(new SimpleObjectProperty<>()); } - + /** * Define the model element that will be wrapped by this {@link ModelWrapper} instance. - * + * * @param model * the element of the model that will be wrapped. */ public void set(M model) { this.model.set(model); } - + /** * @return the wrapped model element if one was defined, otherwise null. */ @@ -623,7 +355,8 @@ public ObjectProperty modelProperty() { */ public void reset() { fields.forEach(PropertyField::resetToDefault); - + immutableFields.forEach(PropertyField::resetToDefault); + calculateDifferenceFlag(); } @@ -636,43 +369,48 @@ public void reset() { * Usage example: *
     	 * ModelWrapper{@code} wrapper = new ModelWrapper{@code<>}();
    -	 * 
    +	 *
     	 * wrapper.field(Person::getName, Person::setName, "oldDefault");
    -	 * 
    +	 *
     	 * Person p = new Person();
     	 * wrapper.set(p);
    -	 * 
    -	 * 
    +	 *
    +	 *
     	 * p.setName("Luise");
    -	 * 
    +	 *
     	 * wrapper.useCurrentValuesAsDefaults(); // now "Luise" is the default value for the name field.
    -	 *  
    -	 * 
    +	 *
    +	 *
     	 * name.set("Hugo");
     	 * wrapper.commit();
    -	 * 
    +	 *
     	 * name.get(); // Hugo
     	 * p.getName(); // Hugo
    -	 * 
    -	 * 
    +	 *
    +	 *
     	 * wrapper.reset(); // reset to the new defaults
     	 * name.get(); // Luise
    -	 * 
    +	 *
     	 * wrapper.commit(); // put values from properties to the wrapped model object
     	 * p.getName(); // Luise
    -	 *   
    -	 *      
    +	 *
    +	 *
     	 * 
    - * + * * * If no model instance is set to be wrapped by the ModelWrapper, nothing will happen when this method is invoked. * Instead the old default values will still be available. - * + * */ public void useCurrentValuesAsDefaults() { - if(model.get() != null) { + M wrappedModelInstance = model.get(); + if(wrappedModelInstance != null) { for (final PropertyField field : fields) { - field.updateDefault(model.get()); + field.updateDefault(wrappedModelInstance); + } + + for (final ImmutablePropertyField field : immutableFields) { + field.updateDefault(wrappedModelInstance); } } } @@ -687,14 +425,33 @@ public void useCurrentValuesAsDefaults() { */ public void commit() { if (model.get() != null) { + + inCommitPhase = true; + fields.forEach(field -> field.commit(model.get())); - + + if(! immutableFields.isEmpty()) { + + M tmp = model.get(); + + for (ImmutablePropertyField immutableField : immutableFields) { + tmp = immutableField.commitImmutable(tmp); + } + + model.set(tmp); + + } + + inCommitPhase = false; + + + dirtyFlag.set(false); - + calculateDifferenceFlag(); } } - + /** * Take the current values from the wrapped model element and put them in the corresponding property fields. *

    @@ -704,9 +461,12 @@ public void commit() { * defined property fields. */ public void reload() { - if (model.get() != null) { - fields.forEach(field -> field.reload(model.get())); - + M wrappedModelInstance = model.get(); + if (wrappedModelInstance != null) { + fields.forEach(field -> field.reload(wrappedModelInstance)); + + immutableFields.forEach(field -> field.reload(wrappedModelInstance)); + dirtyFlag.set(false); calculateDifferenceFlag(); } @@ -732,23 +492,129 @@ private void propertyWasChanged() { dirtyFlag.set(true); calculateDifferenceFlag(); } - + private void calculateDifferenceFlag() { - if (model.get() != null) { + M wrappedModelInstance = model.get(); + + if (wrappedModelInstance != null) { for (final PropertyField field : fields) { - if (field.isDifferent(model.get())) { - diffFlag.set(true); - return; - } - } - diffFlag.set(false); + if (field.isDifferent(wrappedModelInstance)) { + diffFlag.set(true); + return; + } + } + + for (final ImmutablePropertyField field : immutableFields) { + if(field.isDifferent(wrappedModelInstance)) { + diffFlag.set(true); + return; + } + } + + diffFlag.set(false); + } + } + + + private > R add(PropertyField field) { + fields.add(field); + if (model.get() != null) { + field.reload(model.get()); + } + return field.getProperty(); + } + + @SuppressWarnings("unchecked") + private > R addIdentified(String fieldName, PropertyField field) { + if (identifiedFields.containsKey(fieldName)) { + final Property property = identifiedFields.get(fieldName).getProperty(); + return (R) property; + } else { + identifiedFields.put(fieldName, field); + return add(field); + } + } + + private > R addImmutable(ImmutablePropertyField field) { + immutableFields.add(field); + + if(model.get() != null) { + field.reload(model.get()); + } + + return field.getProperty(); + } + + private > R addIdentifiedImmutable(String fieldName, ImmutablePropertyField field) { + if (identifiedFields.containsKey(fieldName)) { + final Property property = identifiedFields.get(fieldName).getProperty(); + return (R) property; + } else { + identifiedFields.put(fieldName, field); + return addImmutable(field); } } - - - - /** Field type String **/ - + + /** + * This boolean flag indicates whether there is a difference of the data between the wrapped model object and the + * properties provided by this wrapper. + *

    + * Note the difference to {@link #dirtyProperty()}: This property will be true if the data of the + * wrapped model is different to the properties of this wrapper. If you change the data back to the initial state so + * that the data is equal again, this property will change back to false while the + * {@link #dirtyProperty()} will still be true. + * + * Simply speaking: This property indicates whether there is a difference in data between the model and the wrapper. + * The {@link #dirtyProperty()} indicates whether there was a change done. + * + * + * Note: Only those changes are observed that are done through the wrapped property fields of this wrapper. If you + * change the data of the model instance directly, this property won't turn to true. + * + * + * @return a read-only property indicating a difference between model and wrapper. + */ + public ReadOnlyBooleanProperty differentProperty() { + return diffFlag.getReadOnlyProperty(); + } + + /** + * See {@link #differentProperty()}. + */ + public boolean isDifferent() { + return diffFlag.get(); + } + + /** + * This boolean flag indicates whether there was a change to at least one wrapped property. + *

    + * Note the difference to {@link #differentProperty()}: This property will turn to true when the value + * of one of the wrapped properties is changed. It will only change back to false when either the + * {@link #commit()} or {@link #reload()} method is called. This property will stay true even if + * afterwards another change is done so that the data is equal again. In this case the {@link #differentProperty()} + * will switch back to false. + *

    + * + * Simply speaking: This property indicates whether there was a change done to the wrapped properties or not. The + * {@link #differentProperty()} indicates whether there is a difference in data at the moment. + * + * @return a read only boolean property indicating if there was a change done. + */ + public ReadOnlyBooleanProperty dirtyProperty() { + return dirtyFlag.getReadOnlyProperty(); + } + + /** + * See {@link #dirtyProperty()}. + */ + public boolean isDirty() { + return dirtyFlag.get(); + } + + + + /* Field type String */ + /** * Add a new field of type String to this instance of the wrapper. This method is used for model elements that are * following the normal Java-Beans-standard i.e. the model fields are only available via getter and setter methods @@ -761,10 +627,10 @@ private void calculateDifferenceFlag() { * *

     	 * ModelWrapper{@code} personWrapper = new ModelWrapper{@code<>}();
    -	 * 
    +	 *
     	 * StringProperty wrappedNameProperty = personWrapper.field(person -> person.getName(), (person, value)
     	 * 	 -> person.setName(value));
    -	 * 
    +	 *
     	 * // or with a method reference
     	 * StringProperty wrappedNameProperty = personWrapper.field(Person::getName, Person::setName);
     	 *
    @@ -777,34 +643,95 @@ private void calculateDifferenceFlag() {
     	 * @param setter
     	 *            a function that sets the given value to the given model element. Typically you will use a method
     	 *            reference to the setter method of the model element.
    -	 * 			
    +	 *
     	 * @return The wrapped property instance.
     	 */
     	public StringProperty field(StringGetter getter, StringSetter setter) {
    -		return add(new BeanPropertyField<>(getter, setter, SimpleStringProperty::new));
    +		return add(new BeanPropertyField<>(this::propertyWasChanged, getter, setter, SimpleStringProperty::new));
     	}
    -	
    +
     	/**
    -	 * Add a new field of type String to this instance of the wrapper. See {@link #field(StringGetter, StringSetter)}.
    -	 * This method additionally has a parameter to define the default value that is used when the {@link #reset()}
    -	 * method is used.
    +	 * Add a new immutable field of type String to this instance of the wrapper. This method is used for immutable
    +	 * model elements that have getters to get values for it's fields but not setters.
    +	 * Instead, immutables have methods that take a new value for a field and return a new cloned instance of the
    +	 * model element with only this field updated to the new value. The old model instance isn't changed.
     	 *
    +	 * 

    * - * @param getter - * a function that returns the current value of the field for a given model element. Typically you will - * use a method reference to the getter method of the model element. - * @param setter - * a function that sets the given value to the given model element. Typically you will use a method - * reference to the setter method of the model element. - * @param defaultValue - * the default value that is used when {@link #reset()} is invoked. - * - * @return The wrapped property instance. - */ - public StringProperty field(StringGetter getter, StringSetter setter, String defaultValue) { - return add(new BeanPropertyField<>(getter, setter, defaultValue, SimpleStringProperty::new)); + * Example: + *

    + * + *

    +	 * ModelWrapper{@code} personWrapper = new ModelWrapper{@code<>}();
    +	 *
    +	 * StringProperty wrappedNameProperty = personWrapper.field(person -> person.getName(), (person, value)
    +	 * 	 -> {
    +	 * 	     Person clone = person.withName(value);
    +	 * 	     return clone;
    +	 * 	 });
    +	 *
    +	 * // or with a method reference
    +	 * StringProperty wrappedNameProperty = personWrapper.field(Person::getName, Person::withName);
    +	 *
    +	 * 
    + * + * + * @param getter + * a function that returns the current value of the field for a given model element. Typically you will + * use a method reference to the getter method of the model element. + * @param immutableSetter + * a function that returns a clone of this the given model element that has the given value set. Typically you will use a method + * reference to the immutable setter method of the model element. + * + * @return The wrapped property instance. + */ + public StringProperty immutableField(StringGetter getter, StringImmutableSetter immutableSetter){ + return addImmutable(new ImmutableBeanPropertyField<>(this::propertyWasChanged, getter, immutableSetter, SimpleStringProperty::new)); + } + + /** + * Add a new field of type String to this instance of the wrapper. See {@link #field(StringGetter, StringSetter)}. + * This method additionally has a parameter to define the default value that is used when the {@link #reset()} + * method is used. + * + * + * @param getter + * a function that returns the current value of the field for a given model element. Typically you will + * use a method reference to the getter method of the model element. + * @param setter + * a function that sets the given value to the given model element. Typically you will use a method + * reference to the setter method of the model element. + * @param defaultValue + * the default value that is used when {@link #reset()} is invoked. + * + * @return The wrapped property instance. + */ + public StringProperty field(StringGetter getter, StringSetter setter, String defaultValue) { + return add(new BeanPropertyField<>(this::propertyWasChanged, getter, setter, defaultValue, + SimpleStringProperty::new)); + } + + /** + * Ad a new immutable field of type String to this instance of the wrapper. See {@link #immutableField(StringGetter, StringImmutableSetter)}. + * This method additionally has a parameter to define the default value that is used when the {@link #reset()} + * method is used. + * + * + * @param getter + * a function that returns the current value of the field for a given model element. Typically you will + * use a method reference to the getter method of the model element. + * @param immutableSetter + * a function that returns a clone of this the given model element that has the given value set. Typically you will use a method + * reference to the immutable setter method of the model element. + * @param defaultValue + * the default value that is used when {@link #reset()} is invoked. + * + * @return The wrapped property instance. + */ + public StringProperty immutableField(StringGetter getter, StringImmutableSetter immutableSetter, String defaultValue){ + return addImmutable(new ImmutableBeanPropertyField<>(this::propertyWasChanged, getter, immutableSetter, defaultValue, SimpleStringProperty::new)); } - + /** * Add a new field of type {@link String} to this instance of the wrapper. This method is used for model elements * that are following the enhanced JavaFX-Beans-standard i.e. the model fields are available as JavaFX Properties. @@ -815,9 +742,9 @@ public StringProperty field(StringGetter getter, StringSetter setter, Stri * *
     	 * ModelWrapper{@code} personWrapper = new ModelWrapper{@code<>}();
    -	 * 
    +	 *
     	 * StringProperty wrappedNameProperty = personWrapper.field(person -> person.nameProperty());
    -	 * 
    +	 *
     	 * // or with a method reference
     	 * StringProperty wrappedNameProperty = personWrapper.field(Person::nameProperty);
     	 *
    @@ -826,18 +753,18 @@ public StringProperty field(StringGetter getter, StringSetter setter, Stri
     	 * @param accessor
     	 *            a function that returns the property for a given model instance. Typically you will use a method
     	 *            reference to the javafx-property accessor method.
    -	 * 			
    +	 *
     	 * @return The wrapped property instance.
     	 */
     	public StringProperty field(StringPropertyAccessor accessor) {
    -		return add(new FxPropertyField<>(accessor::apply, SimpleStringProperty::new));
    +		return add(new FxPropertyField<>(this::propertyWasChanged, accessor, SimpleStringProperty::new));
     	}
    -	
    +
     	/**
     	 * Add a new field of type String to this instance of the wrapper. See {@link #field(StringGetter, StringSetter)}.
     	 * This method additionally has a parameter to define the default value that is used when the {@link #reset()}
     	 * method is used.
    -	 * 
    +	 *
     	 * @param accessor
     	 *            a function that returns the property for a given model instance. Typically you will use a method
     	 *            reference to the javafx-property accessor method.
    @@ -846,16 +773,17 @@ public StringProperty field(StringPropertyAccessor accessor) {
     	 * @return The wrapped property instance.
     	 */
     	public StringProperty field(StringPropertyAccessor accessor, String defaultValue) {
    -		return add(new FxPropertyField<>(accessor::apply, defaultValue, SimpleStringProperty::new));
    +		return add(new FxPropertyField<>(this::propertyWasChanged, accessor, defaultValue,
    +				SimpleStringProperty::new));
     	}
    -	
    -	
    -	
    -	
    +
    +
    +
    +
     	/**
     	 * Add a new field of type String to this instance of the wrapper. See {@link #field(StringGetter, StringSetter)}.
     	 * This method additionally takes a string identifier as first parameter.
    -	 *
    +	 * 

    * This identifier is used to return the same property instance even when the method is invoked multiple times. * * @param identifier @@ -869,15 +797,40 @@ public StringProperty field(StringPropertyAccessor accessor, String defaultVa * @return The wrapped property instance. */ public StringProperty field(String identifier, StringGetter getter, StringSetter setter) { - return addIdentified(identifier, new BeanPropertyField<>(getter, setter, () -> new SimpleStringProperty(null, identifier))); + return addIdentified(identifier, new BeanPropertyField<>(this::propertyWasChanged, getter, setter, + () -> new SimpleStringProperty(null, identifier))); } - + public StringProperty field(String identifier, StringGetter getter, StringSetter setter, String defaultValue) { - return addIdentified(identifier, new BeanPropertyField<>(getter, setter, defaultValue, + return addIdentified(identifier, new BeanPropertyField<>(this::propertyWasChanged, getter, setter, defaultValue, () -> new SimpleStringProperty(null, identifier))); } - + + /** + * Add a new immutable field of type String to this instance of the wrapper. See {@link #immutableField(StringGetter, StringImmutableSetter}). + * This method additionally takes a string identifier as first parameter. + *

    + * This identifier is used to return the same property instance even when the method is invoked multiple times. + * + * @param identifier + * an identifier for the field. + * @param getter + * a function that returns the current value of the field for a given model element. Typically you will + * use a method reference to the getter method of the model element. + * @param immutableSetter + * a function that returns a clone of this the given model element that has the given value set. Typically you will use a method + * reference to the immutable setter method of the model element. + * @return The wrapped property instance. + */ + public StringProperty immutableField(String identifier, StringGetter getter, StringImmutableSetter immutableSetter){ + return addIdentifiedImmutable(identifier, new ImmutableBeanPropertyField<>(this::propertyWasChanged, getter, immutableSetter, () -> new SimpleStringProperty(null, identifier))); + } + + public StringProperty immutableField(String identifier, StringGetter getter, StringImmutableSetter immutableSetter, String defaultValue){ + return addIdentifiedImmutable(identifier, new ImmutableBeanPropertyField<>(this::propertyWasChanged, getter, immutableSetter, defaultValue, () -> new SimpleStringProperty(null, identifier))); + } + /** * Add a new field of type String to this instance of the wrapper. See {@link #field(StringPropertyAccessor)}. This * method additionally takes a string identifier as first parameter. @@ -886,424 +839,617 @@ public StringProperty field(String identifier, StringGetter getter, StringSet * * @param identifier * an identifier for the field. - * + * * @param accessor * a function that returns the property for a given model instance. Typically you will use a method * reference to the javafx-property accessor method. * @return The wrapped property instance. */ public StringProperty field(String identifier, StringPropertyAccessor accessor) { - return addIdentified(identifier, new FxPropertyField<>(accessor, () -> new SimpleStringProperty(null, identifier))); + return addIdentified(identifier, new FxPropertyField<>(this::propertyWasChanged, accessor, + () -> new SimpleStringProperty(null, identifier))); } - + public StringProperty field(String identifier, StringPropertyAccessor accessor, String defaultValue) { return addIdentified(identifier, - new FxPropertyField<>(accessor, defaultValue, () -> new SimpleStringProperty(null, identifier))); + new FxPropertyField<>(this::propertyWasChanged, accessor, defaultValue, + () -> new SimpleStringProperty(null, identifier))); } - - /** Field type Boolean **/ - + + /* Field type Boolean */ + public BooleanProperty field(BooleanGetter getter, BooleanSetter setter) { - return add(new BeanPropertyField<>(getter, setter, SimpleBooleanProperty::new)); + return add(new BeanPropertyField<>(this::propertyWasChanged, getter, setter, SimpleBooleanProperty::new)); + } + + public BooleanProperty immutableField(BooleanGetter getter, BooleanImmutableSetter immutableSetter){ + return addImmutable(new ImmutableBeanPropertyField<>(this::propertyWasChanged, getter, immutableSetter, SimpleBooleanProperty::new)); } - + public BooleanProperty field(BooleanGetter getter, BooleanSetter setter, boolean defaultValue) { - return add(new BeanPropertyField<>(getter, setter, defaultValue, SimpleBooleanProperty::new)); + return add(new BeanPropertyField<>(this::propertyWasChanged, getter, setter, defaultValue, + SimpleBooleanProperty::new)); + } + + public BooleanProperty immutableField(BooleanGetter getter, BooleanImmutableSetter immutableSetter, boolean defaultValue){ + return addImmutable(new ImmutableBeanPropertyField<>(this::propertyWasChanged, getter, immutableSetter, defaultValue, SimpleBooleanProperty::new)); } - + public BooleanProperty field(BooleanPropertyAccessor accessor) { - return add(new FxPropertyField<>(accessor, SimpleBooleanProperty::new)); + return add(new FxPropertyField<>(this::propertyWasChanged, accessor, SimpleBooleanProperty::new)); } - + public BooleanProperty field(BooleanPropertyAccessor accessor, boolean defaultValue) { - return add(new FxPropertyField<>(accessor, defaultValue, SimpleBooleanProperty::new)); + return add(new FxPropertyField<>(this::propertyWasChanged, accessor, defaultValue, SimpleBooleanProperty::new)); } - + public BooleanProperty field(String identifier, BooleanGetter getter, BooleanSetter setter) { - return addIdentified(identifier, new BeanPropertyField<>(getter, setter, () -> new SimpleBooleanProperty(null, identifier))); + return addIdentified(identifier, new BeanPropertyField<>(this::propertyWasChanged, getter, setter, + () -> new SimpleBooleanProperty(null, identifier))); } - + public BooleanProperty field(String identifier, BooleanGetter getter, BooleanSetter setter, boolean defaultValue) { - return addIdentified(identifier, new BeanPropertyField<>(getter, setter, defaultValue, + return addIdentified(identifier, new BeanPropertyField<>(this::propertyWasChanged, getter, setter, defaultValue, () -> new SimpleBooleanProperty(null, identifier))); } - + + public BooleanProperty immutableField(String identifier, BooleanGetter getter, BooleanImmutableSetter immutableSetter){ + return addIdentifiedImmutable(identifier, new ImmutableBeanPropertyField<>(this::propertyWasChanged, getter, immutableSetter, () -> new SimpleBooleanProperty(null, identifier))); + } + + public BooleanProperty immutableField(String identifier, BooleanGetter getter, BooleanImmutableSetter immutableSetter, boolean defaultValue){ + return addIdentifiedImmutable(identifier, new ImmutableBeanPropertyField<>(this::propertyWasChanged, getter, immutableSetter, defaultValue, () -> new SimpleBooleanProperty(null, identifier))); + } + public BooleanProperty field(String identifier, BooleanPropertyAccessor accessor) { - return addIdentified(identifier, new FxPropertyField<>(accessor, () -> new SimpleBooleanProperty(null, identifier))); + return addIdentified(identifier, new FxPropertyField<>(this::propertyWasChanged, accessor, + () -> new SimpleBooleanProperty(null, identifier))); } - + public BooleanProperty field(String identifier, BooleanPropertyAccessor accessor, boolean defaultValue) { - return addIdentified(identifier, new FxPropertyField<>(accessor, defaultValue, () -> new SimpleBooleanProperty(null, identifier))); - } - - - - /** Field type Double **/ - - + return addIdentified(identifier, new FxPropertyField<>(this::propertyWasChanged, accessor, defaultValue, + () -> new SimpleBooleanProperty(null, identifier))); + } + + + + /* Field type Double */ + + public DoubleProperty field(DoubleGetter getter, DoubleSetter setter) { - final ModelWrapper.BeanPropertyField beanPropertyField = new BeanPropertyField<>( + return add(new BeanPropertyField<>( + this::propertyWasChanged, getter::apply, (m, number) -> setter.accept(m, number.doubleValue()), - SimpleDoubleProperty::new); - return add(beanPropertyField); + SimpleDoubleProperty::new)); } - + + + public DoubleProperty immutableField(DoubleGetter getter, DoubleImmutableSetter immutableSetter){ + return addImmutable(new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.doubleValue()), + SimpleDoubleProperty::new + )); + } + public DoubleProperty field(DoubleGetter getter, DoubleSetter setter, double defaultValue) { - final ModelWrapper.BeanPropertyField beanPropertyField = new BeanPropertyField<>( + return add(new BeanPropertyField<>( + this::propertyWasChanged, getter::apply, (m, number) -> setter.accept(m, number.doubleValue()), defaultValue, - SimpleDoubleProperty::new); - return add(beanPropertyField); + SimpleDoubleProperty::new)); } - + + public DoubleProperty immutableField(DoubleGetter getter, DoubleImmutableSetter immutableSetter, double defaultValue){ + return addImmutable(new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.doubleValue()), + defaultValue, + SimpleDoubleProperty::new)); + } + public DoubleProperty field(DoublePropertyAccessor accessor) { - return add(new FxPropertyField<>(accessor::apply, SimpleDoubleProperty::new)); + return add(new FxPropertyField<>(this::propertyWasChanged, accessor::apply, SimpleDoubleProperty::new)); } - + public DoubleProperty field(DoublePropertyAccessor accessor, double defaultValue) { - return add(new FxPropertyField<>(accessor::apply, defaultValue, SimpleDoubleProperty::new)); + return add(new FxPropertyField<>(this::propertyWasChanged, accessor::apply, defaultValue, + SimpleDoubleProperty::new)); } - + public DoubleProperty field(String identifier, DoubleGetter getter, DoubleSetter setter) { - final ModelWrapper.BeanPropertyField beanPropertyField = - new BeanPropertyField<>( - getter::apply, (m, number) -> setter.accept(m, number.doubleValue()), - () -> new SimpleDoubleProperty(null, identifier)); - - return addIdentified(identifier, beanPropertyField); + + return addIdentified(identifier, new BeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> setter.accept(m, number.doubleValue()), + () -> new SimpleDoubleProperty(null, identifier))); } - + public DoubleProperty field(String identifier, DoubleGetter getter, DoubleSetter setter, double defaultValue) { - ModelWrapper.BeanPropertyField beanPropertyField = new BeanPropertyField<>( - getter::apply, (m, number) -> setter.accept(m, number.doubleValue()), + return addIdentified(identifier, new BeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> setter.accept(m, number.doubleValue()), defaultValue, - () -> new SimpleDoubleProperty(null, identifier)); - return addIdentified(identifier, beanPropertyField); + () -> new SimpleDoubleProperty(null, identifier))); } - + + + public DoubleProperty immutableField(String identifier, DoubleGetter getter, DoubleImmutableSetter immutableSetter){ + return addIdentifiedImmutable(identifier, new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.doubleValue()), + () -> new SimpleDoubleProperty(null, identifier))); + } + + public DoubleProperty immutableField(String identifier, DoubleGetter getter, DoubleImmutableSetter immutableSetter, double defaultValue){ + return addIdentifiedImmutable(identifier, new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.doubleValue()), + defaultValue, + () -> new SimpleDoubleProperty(null, identifier))); + } + public DoubleProperty field(String identifier, DoublePropertyAccessor accessor) { - return addIdentified(identifier, new FxPropertyField<>(accessor::apply, () -> new SimpleDoubleProperty(null, identifier))); + return addIdentified(identifier, new FxPropertyField<>( + this::propertyWasChanged, + accessor::apply, + () -> new SimpleDoubleProperty(null, identifier))); } - + public DoubleProperty field(String identifier, DoublePropertyAccessor accessor, double defaultValue) { return addIdentified(identifier, - new FxPropertyField<>(accessor::apply, defaultValue, () -> new SimpleDoubleProperty(null, identifier))); - } - - - - - /** Field type Float **/ - + new FxPropertyField<>(this::propertyWasChanged, accessor::apply, defaultValue, + () -> new SimpleDoubleProperty(null, identifier))); + } + + + + + /* Field type Float */ + public FloatProperty field(FloatGetter getter, FloatSetter setter) { - final ModelWrapper.BeanPropertyField beanPropertyField = new BeanPropertyField<>( + return add(new BeanPropertyField<>( + this::propertyWasChanged, getter::apply, (m, number) -> setter.accept(m, number.floatValue()), - SimpleFloatProperty::new); - return add(beanPropertyField); + SimpleFloatProperty::new)); + } + + public FloatProperty immutableField(FloatGetter getter, FloatImmutableSetter immutableSetter){ + return addImmutable(new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.floatValue()), + SimpleFloatProperty::new + )); } - + public FloatProperty field(FloatGetter getter, FloatSetter setter, float defaultValue) { - ModelWrapper.BeanPropertyField beanPropertyField = new BeanPropertyField<>( + return add(new BeanPropertyField<>( + this::propertyWasChanged, getter::apply, (m, number) -> setter.accept(m, number.floatValue()), defaultValue, - SimpleFloatProperty::new); - return add(beanPropertyField); + SimpleFloatProperty::new)); } - + + public FloatProperty immutableField(FloatGetter getter, FloatImmutableSetter immutableSetter, float defaultValue){ + return addImmutable(new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.floatValue()), + defaultValue, + SimpleFloatProperty::new)); + } + public FloatProperty field(FloatPropertyAccessor accessor) { - return add(new FxPropertyField<>(accessor::apply, SimpleFloatProperty::new)); + return add(new FxPropertyField<>(this::propertyWasChanged, accessor::apply, SimpleFloatProperty::new)); } - + public FloatProperty field(FloatPropertyAccessor accessor, float defaultValue) { - return add(new FxPropertyField<>(accessor::apply, defaultValue, SimpleFloatProperty::new)); + return add(new FxPropertyField<>(this::propertyWasChanged, accessor::apply, defaultValue, + SimpleFloatProperty::new)); } - + public FloatProperty field(String identifier, FloatGetter getter, FloatSetter setter) { - ModelWrapper.BeanPropertyField beanPropertyField = new BeanPropertyField<>( + return addIdentified(identifier, new BeanPropertyField<>( + this::propertyWasChanged, getter::apply, (m, number) -> setter.accept(m, number.floatValue()), - () -> new SimpleFloatProperty(null, identifier)); - return addIdentified(identifier, beanPropertyField); + () -> new SimpleFloatProperty(null, identifier))); } - + public FloatProperty field(String identifier, FloatGetter getter, FloatSetter setter, float defaultValue) { - - ModelWrapper.BeanPropertyField beanPropertyField = new BeanPropertyField<>( + + return addIdentified(identifier, new BeanPropertyField<>( + this::propertyWasChanged, getter::apply, (m, number) -> setter.accept(m, number.floatValue()), defaultValue, - () -> new SimpleFloatProperty(null, identifier)); - return addIdentified(identifier, beanPropertyField); + () -> new SimpleFloatProperty(null, identifier))); + } + + + public FloatProperty immutableField(String identifier, FloatGetter getter, FloatImmutableSetter immutableSetter){ + return addIdentifiedImmutable(identifier, new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.floatValue()), + () -> new SimpleFloatProperty(null, identifier))); + } + + public FloatProperty immutableField(String identifier, FloatGetter getter, FloatImmutableSetter immutableSetter, float defaultValue){ + return addIdentifiedImmutable(identifier, new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.floatValue()), + defaultValue, + () -> new SimpleFloatProperty(null, identifier))); } - + public FloatProperty field(String identifier, FloatPropertyAccessor accessor) { - return addIdentified(identifier, new FxPropertyField<>(accessor::apply, () -> new SimpleFloatProperty(null, identifier))); + return addIdentified(identifier, new FxPropertyField<>(this::propertyWasChanged, accessor::apply, + () -> new SimpleFloatProperty(null, identifier))); } - + public FloatProperty field(String identifier, FloatPropertyAccessor accessor, float defaultValue) { return addIdentified(identifier, - new FxPropertyField<>(accessor::apply, defaultValue, () -> new SimpleFloatProperty(null, identifier))); + new FxPropertyField<>(this::propertyWasChanged, accessor::apply, defaultValue, + () -> new SimpleFloatProperty(null, identifier))); } - - - /** Field type Integer **/ - - + + + /* Field type Integer */ + + public IntegerProperty field(IntGetter getter, IntSetter setter) { - ModelWrapper.BeanPropertyField beanPropertyField = new BeanPropertyField<>( + return add(new BeanPropertyField<>( + this::propertyWasChanged, getter::apply, (m, number) -> setter.accept(m, number.intValue()), - SimpleIntegerProperty::new); - return add(beanPropertyField); + SimpleIntegerProperty::new)); } - + + + public IntegerProperty immutableField(IntGetter getter, IntImmutableSetter immutableSetter){ + return addImmutable(new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.intValue()), + SimpleIntegerProperty::new + )); + } + public IntegerProperty field(IntGetter getter, IntSetter setter, int defaultValue) { - ModelWrapper.BeanPropertyField beanPropertyField = new BeanPropertyField<>( + return add(new BeanPropertyField<>( + this::propertyWasChanged, getter::apply, (m, number) -> setter.accept(m, number.intValue()), defaultValue, - SimpleIntegerProperty::new); - return add(beanPropertyField); + SimpleIntegerProperty::new)); } - - + + + public IntegerProperty immutableField(IntGetter getter, IntImmutableSetter immutableSetter, int defaultValue){ + return addImmutable(new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.intValue()), + defaultValue, + SimpleIntegerProperty::new)); + } + + public IntegerProperty field(IntPropertyAccessor accessor) { - return add(new FxPropertyField<>(accessor::apply, SimpleIntegerProperty::new)); + return add(new FxPropertyField<>(this::propertyWasChanged, accessor::apply, SimpleIntegerProperty::new)); } - + public IntegerProperty field(IntPropertyAccessor accessor, int defaultValue) { - return add(new FxPropertyField<>(accessor::apply, defaultValue, SimpleIntegerProperty::new)); + return add(new FxPropertyField<>(this::propertyWasChanged, accessor::apply, defaultValue, + SimpleIntegerProperty::new)); } - + public IntegerProperty field(String identifier, IntGetter getter, IntSetter setter) { - ModelWrapper.BeanPropertyField beanPropertyField = new BeanPropertyField<>( + return addIdentified(identifier, new BeanPropertyField<>( + this::propertyWasChanged, getter::apply, (m, number) -> setter.accept(m, number.intValue()), - () -> new SimpleIntegerProperty(null, identifier)); - return addIdentified(identifier, beanPropertyField); + () -> new SimpleIntegerProperty(null, identifier))); } - + public IntegerProperty field(String identifier, IntGetter getter, IntSetter setter, int defaultValue) { - ModelWrapper.BeanPropertyField beanPropertyField = new BeanPropertyField<>( + return addIdentified(identifier, new BeanPropertyField<>( + this::propertyWasChanged, getter::apply, (m, number) -> setter.accept(m, number.intValue()), defaultValue, - () -> new SimpleIntegerProperty(null, identifier)); - return addIdentified(identifier, beanPropertyField); + () -> new SimpleIntegerProperty(null, identifier))); } - - + + + + public IntegerProperty immutableField(String identifier, IntGetter getter, IntImmutableSetter immutableSetter){ + return addIdentifiedImmutable(identifier, new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.intValue()), + () -> new SimpleIntegerProperty(null, identifier))); + } + + public IntegerProperty immutableField(String identifier, IntGetter getter, IntImmutableSetter immutableSetter, int defaultValue){ + return addIdentifiedImmutable(identifier, new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.intValue()), + defaultValue, + () -> new SimpleIntegerProperty(null, identifier))); + } + public IntegerProperty field(String identifier, IntPropertyAccessor accessor) { - return addIdentified(identifier, new FxPropertyField<>(accessor::apply, () -> new SimpleIntegerProperty(null, identifier))); + return addIdentified(identifier, new FxPropertyField<>(this::propertyWasChanged, accessor::apply, + () -> new SimpleIntegerProperty(null, identifier))); } - + public IntegerProperty field(String identifier, IntPropertyAccessor accessor, int defaultValue) { - return addIdentified(identifier, new FxPropertyField<>(accessor::apply, defaultValue, - () -> new SimpleIntegerProperty(null, identifier))); - } - - - - /** Field type Long **/ - + return addIdentified(identifier, new FxPropertyField<>(this::propertyWasChanged, accessor::apply, defaultValue, + () -> new SimpleIntegerProperty(null, identifier))); + } + + + + /* Field type Long */ + public LongProperty field(LongGetter getter, LongSetter setter) { - ModelWrapper.BeanPropertyField beanPropertyField = new BeanPropertyField<>( + return add(new BeanPropertyField<>( + this::propertyWasChanged, getter::apply, (m, number) -> setter.accept(m, number.longValue()), - SimpleLongProperty::new); - return add(beanPropertyField); + SimpleLongProperty::new)); + } + + public LongProperty immutableField(LongGetter getter, LongImmutableSetter immutableSetter){ + return addImmutable(new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.longValue()), + SimpleLongProperty::new + )); } - + public LongProperty field(LongGetter getter, LongSetter setter, long defaultValue) { - ModelWrapper.BeanPropertyField beanPropertyField = new BeanPropertyField<>( + return add(new BeanPropertyField<>( + this::propertyWasChanged, getter::apply, (m, number) -> setter.accept(m, number.longValue()), defaultValue, - SimpleLongProperty::new); - return add(beanPropertyField); + SimpleLongProperty::new)); } - + + public LongProperty immutableField(LongGetter getter, LongImmutableSetter immutableSetter, long defaultValue){ + return addImmutable(new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.longValue()), + defaultValue, + SimpleLongProperty::new)); + } + public LongProperty field(LongPropertyAccessor accessor) { - return add(new FxPropertyField<>(accessor::apply, SimpleLongProperty::new)); + return add(new FxPropertyField<>(this::propertyWasChanged, accessor::apply, SimpleLongProperty::new)); } - + public LongProperty field(LongPropertyAccessor accessor, long defaultValue) { - return add(new FxPropertyField<>(accessor::apply, defaultValue, SimpleLongProperty::new)); + return add(new FxPropertyField<>(this::propertyWasChanged, accessor::apply, defaultValue, + SimpleLongProperty::new)); } - - + + public LongProperty field(String identifier, LongGetter getter, LongSetter setter) { - ModelWrapper.BeanPropertyField beanPropertyField = new BeanPropertyField<>( + return addIdentified(identifier, new BeanPropertyField<>( + this::propertyWasChanged, getter::apply, (m, number) -> setter.accept(m, number.longValue()), - () -> new SimpleLongProperty(null, identifier)); - return addIdentified(identifier, beanPropertyField); + () -> new SimpleLongProperty(null, identifier))); } - + public LongProperty field(String identifier, LongGetter getter, LongSetter setter, long defaultValue) { - ModelWrapper.BeanPropertyField beanPropertyField = new BeanPropertyField<>( - getter::apply, (m, number) -> setter.accept(m, number.longValue()), - defaultValue, - () -> new SimpleLongProperty(null, identifier)); return addIdentified(identifier, - beanPropertyField); + new BeanPropertyField<>( + this::propertyWasChanged, + getter::apply, (m, number) -> setter.accept(m, number.longValue()), + defaultValue, + () -> new SimpleLongProperty(null, identifier))); } - + + + public LongProperty immutableField(String identifier, LongGetter getter, LongImmutableSetter immutableSetter){ + return addIdentifiedImmutable(identifier, new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.longValue()), + () -> new SimpleLongProperty(null, identifier))); + } + + public LongProperty immutableField(String identifier, LongGetter getter, LongImmutableSetter immutableSetter, long defaultValue){ + return addIdentifiedImmutable(identifier, new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter::apply, + (m, number) -> immutableSetter.apply(m, number.longValue()), + defaultValue, + () -> new SimpleLongProperty(null, identifier))); + } + + public LongProperty field(String identifier, LongPropertyAccessor accessor) { - return addIdentified(identifier, new FxPropertyField<>(accessor::apply, () -> new SimpleLongProperty(null, identifier))); + return addIdentified(identifier, new FxPropertyField<>(this::propertyWasChanged, accessor::apply, + () -> new SimpleLongProperty(null, identifier))); } - + public LongProperty field(String identifier, LongPropertyAccessor accessor, long defaultValue) { - return addIdentified(identifier, new FxPropertyField<>(accessor::apply, defaultValue, () -> new SimpleLongProperty(null, identifier))); - } - - - - /** Field type generic **/ - - + return addIdentified(identifier, new FxPropertyField<>(this::propertyWasChanged, accessor::apply, defaultValue, + () -> new SimpleLongProperty(null, identifier))); + } + + + + /* Field type generic */ + + public ObjectProperty field(ObjectGetter getter, ObjectSetter setter) { - return add(new BeanPropertyField<>(getter, setter, SimpleObjectProperty::new)); + return add(new BeanPropertyField<>(this::propertyWasChanged, getter, setter, SimpleObjectProperty::new)); } - + + + public ObjectProperty immutableField(ObjectGetter getter, ObjectImmutableSetter immutableSetter){ + return addImmutable(new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter, + immutableSetter, + SimpleObjectProperty::new + )); + } + public ObjectProperty field(ObjectGetter getter, ObjectSetter setter, T defaultValue) { - return add(new BeanPropertyField<>(getter, setter, defaultValue, SimpleObjectProperty::new)); + return add(new BeanPropertyField<>(this::propertyWasChanged, getter, setter, defaultValue, + SimpleObjectProperty::new)); + } + + public ObjectProperty immutableField(ObjectGetter getter, ObjectImmutableSetter immutableSetter, T defaultValue){ + return addImmutable(new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter, + immutableSetter, + defaultValue, + SimpleObjectProperty::new)); } - + public ObjectProperty field(ObjectPropertyAccessor accessor) { - return add(new FxPropertyField<>(accessor, SimpleObjectProperty::new)); + return add(new FxPropertyField<>(this::propertyWasChanged, accessor, SimpleObjectProperty::new)); } - + public ObjectProperty field(ObjectPropertyAccessor accessor, T defaultValue) { - return add(new FxPropertyField<>(accessor, defaultValue, SimpleObjectProperty::new)); + return add(new FxPropertyField<>(this::propertyWasChanged, accessor, defaultValue, SimpleObjectProperty::new)); } - - + + public ObjectProperty field(String identifier, ObjectGetter getter, ObjectSetter setter) { - return addIdentified(identifier, new BeanPropertyField<>(getter, setter, () -> new SimpleObjectProperty(null, identifier))); + return addIdentified(identifier, new BeanPropertyField<>(this::propertyWasChanged, getter, setter, + () -> new SimpleObjectProperty(null, identifier))); } - + public ObjectProperty field(String identifier, ObjectGetter getter, ObjectSetter setter, T defaultValue) { - return addIdentified(identifier, new BeanPropertyField<>(getter, setter, defaultValue, + return addIdentified(identifier, new BeanPropertyField<>(this::propertyWasChanged, getter, setter, defaultValue, + () -> new SimpleObjectProperty(null, identifier))); + } + + + + public ObjectProperty immutableField(String identifier, ObjectGetter getter, ObjectImmutableSetter immutableSetter){ + return addIdentifiedImmutable(identifier, new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter, + immutableSetter, + () -> new SimpleObjectProperty(null, identifier))); + } + + public ObjectProperty immutableField(String identifier, ObjectGetter getter, ObjectImmutableSetter immutableSetter, T defaultValue){ + return addIdentifiedImmutable(identifier, new ImmutableBeanPropertyField<>( + this::propertyWasChanged, + getter, + immutableSetter, + defaultValue, () -> new SimpleObjectProperty(null, identifier))); } - + public ObjectProperty field(String identifier, ObjectPropertyAccessor accessor) { - return addIdentified(identifier, new FxPropertyField<>(accessor, () -> new SimpleObjectProperty(null, identifier))); + return addIdentified(identifier, new FxPropertyField<>(this::propertyWasChanged, accessor, + () -> new SimpleObjectProperty(null, identifier))); } - + public ObjectProperty field(String identifier, ObjectPropertyAccessor accessor, T defaultValue) { return addIdentified(identifier, - new FxPropertyField<>(accessor, defaultValue, () -> new SimpleObjectProperty(null, identifier))); + new FxPropertyField<>(this::propertyWasChanged, accessor, defaultValue, + () -> new SimpleObjectProperty(null, identifier))); } - - - /** Field type list **/ - + + + /* Field type list */ + public ListProperty field(ListGetter getter, ListSetter setter) { - return add(new BeanListPropertyField<>(getter, + return add(new BeanListPropertyField<>(this::propertyWasChanged, getter, (m, list) -> setter.accept(m, FXCollections.observableArrayList(list)), SimpleListProperty::new)); } - + + + public ListProperty immutableField(ListGetter getter, ListImmutableSetter immutableSetter){ + return addImmutable(new ImmutableListPropertyField<>( + this::propertyWasChanged, + getter, + (m, list) -> immutableSetter.apply(m, FXCollections.observableArrayList(list)), + SimpleListProperty::new + )); + } + public ListProperty field(ListGetter getter, ListSetter setter, List defaultValue) { - return add(new BeanListPropertyField<>(getter, - (m, list) -> setter.accept(m, FXCollections.observableArrayList(list)), SimpleListProperty::new, defaultValue)); + return add(new BeanListPropertyField<>(this::propertyWasChanged, getter, + (m, list) -> setter.accept(m, FXCollections.observableArrayList(list)), SimpleListProperty::new, + defaultValue)); + } + + + public ListProperty immutableField(ListGetter getter, ListImmutableSetter immutableSetter, List defaultValue){ + return addImmutable(new ImmutableListPropertyField<>( + this::propertyWasChanged, + getter, + (m, list) -> immutableSetter.apply(m, FXCollections.observableArrayList(list)), + SimpleListProperty::new, + defaultValue)); } - + public ListProperty field(ListPropertyAccessor accessor) { - return add(new FxListPropertyField<>(accessor, SimpleListProperty::new)); + return add(new FxListPropertyField<>(this::propertyWasChanged, accessor, SimpleListProperty::new)); } - + public ListProperty field(ListPropertyAccessor accessor, List defaultValue) { - return add(new FxListPropertyField<>(accessor, SimpleListProperty::new, defaultValue)); + return add( + new FxListPropertyField<>(this::propertyWasChanged, accessor, SimpleListProperty::new, defaultValue)); } - - + + public ListProperty field(String identifier, ListGetter getter, ListSetter setter) { - return addIdentified(identifier, new BeanListPropertyField<>(getter, - (m, list) -> setter.accept(m, FXCollections.observableArrayList(list)), () -> new SimpleListProperty<>(null, identifier))); + return addIdentified(identifier, new BeanListPropertyField<>(this::propertyWasChanged, getter, + (m, list) -> setter.accept(m, FXCollections.observableArrayList(list)), + () -> new SimpleListProperty<>(null, identifier))); } - + public ListProperty field(String identifier, ListGetter getter, ListSetter setter, List defaultValue) { - return addIdentified(identifier, new BeanListPropertyField<>(getter, - (m, list) -> setter.accept(m, FXCollections.observableArrayList(list)), () -> new SimpleListProperty<>(null, identifier), defaultValue)); - } - - public ListProperty field(String identifier, ListPropertyAccessor accessor) { - return addIdentified(identifier, new FxListPropertyField<>(accessor, () -> new SimpleListProperty<>(null, identifier))); - } - - public ListProperty field(String identifier, ListPropertyAccessor accessor, List defaultValue) { - return addIdentified(identifier, new FxListPropertyField<>(accessor, () -> new SimpleListProperty<>(null, identifier), defaultValue)); - } - - private > R add(PropertyField field) { - fields.add(field); - if (model.get() != null) { - field.reload(model.get()); - } - return field.getProperty(); - } - - @SuppressWarnings("unchecked") - private > R addIdentified(String fieldName, PropertyField field) { - if (identifiedFields.containsKey(fieldName)) { - final Property property = identifiedFields.get(fieldName).getProperty(); - return (R) property; - } else { - identifiedFields.put(fieldName, field); - return add(field); - } + return addIdentified(identifier, new BeanListPropertyField<>(this::propertyWasChanged, getter, + (m, list) -> setter.accept(m, FXCollections.observableArrayList(list)), + () -> new SimpleListProperty<>(null, identifier), defaultValue)); } - - /** - * This boolean flag indicates whether there is a difference of the data between the wrapped model object and the - * properties provided by this wrapper. - *

    - * Note the difference to {@link #dirtyProperty()}: This property will be true if the data of the - * wrapped model is different to the properties of this wrapper. If you change the data back to the initial state so - * that the data is equal again, this property will change back to false while the - * {@link #dirtyProperty()} will still be true. - * - * Simply speaking: This property indicates whether there is a difference in data between the model and the wrapper. - * The {@link #dirtyProperty()} indicates whether there was a change done. - * - * - * Note: Only those changes are observed that are done through the wrapped property fields of this wrapper. If you - * change the data of the model instance directly, this property won't turn to true. - * - * - * @return a read-only property indicating a difference between model and wrapper. - */ - public ReadOnlyBooleanProperty differentProperty() { - return diffFlag.getReadOnlyProperty(); + + public ListProperty immutableField(String identifier, ListGetter getter, ListImmutableSetter immutableSetter){ + return addIdentifiedImmutable(identifier, new ImmutableListPropertyField<>( + this::propertyWasChanged, + getter, + (m, list) -> immutableSetter.apply(m, FXCollections.observableArrayList(list)), + () -> new SimpleListProperty<>(null, identifier))); } - - /** - * See {@link #differentProperty()}. - */ - public boolean isDifferent() { - return diffFlag.get(); + + public ListProperty immutableField(String identifier, ListGetter getter, ListImmutableSetter immutableSetter, List defaultValue){ + return addIdentifiedImmutable(identifier, new ImmutableListPropertyField<>( + this::propertyWasChanged, + getter, + (m, list) -> immutableSetter.apply(m, FXCollections.observableArrayList(list)), + () -> new SimpleListProperty<>(null, identifier), + defaultValue)); } - - /** - * This boolean flag indicates whether there was a change to at least one wrapped property. - *

    - * Note the difference to {@link #differentProperty()}: This property will turn to true when the value - * of one of the wrapped properties is changed. It will only change back to false when either the - * {@link #commit()} or {@link #reload()} method is called. This property will stay true even if - * afterwards another change is done so that the data is equal again. In this case the {@link #differentProperty()} - * will switch back to false. - * - * Simply speaking: This property indicates whether there was a change done to the wrapped properties or not. The - * {@link #differentProperty()} indicates whether there is a difference in data at the moment. - * - * @return a read only boolean property indicating if there was a change done. - */ - public ReadOnlyBooleanProperty dirtyProperty() { - return dirtyFlag.getReadOnlyProperty(); + + public ListProperty field(String identifier, ListPropertyAccessor accessor) { + return addIdentified(identifier, new FxListPropertyField<>(this::propertyWasChanged, accessor, + () -> new SimpleListProperty<>(null, identifier))); } - - /** - * See {@link #dirtyProperty()}. - */ - public boolean isDirty() { - return dirtyFlag.get(); + + public ListProperty field(String identifier, ListPropertyAccessor accessor, List defaultValue) { + return addIdentified(identifier, new FxListPropertyField<>(this::propertyWasChanged, accessor, + () -> new SimpleListProperty<>(null, identifier), defaultValue)); } - - } diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/PropertyField.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/PropertyField.java new file mode 100644 index 000000000..a53e41dc9 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/PropertyField.java @@ -0,0 +1,39 @@ +package de.saxsys.mvvmfx.utils.mapping; + +import javafx.beans.property.Property; +import javafx.beans.property.StringProperty; + +/** + * This interface defines the operations that are possible for each field of a wrapped class. + * + * @param + * target type. The base type of the returned property, f.e. {@link String}. + * @param + * model type. The type of the Model class, that is wrapped by this ModelWrapper instance. + * @param + * return type. The type of the Property that is returned via {@link #getProperty()}, f.e. + * {@link StringProperty} or {@link Property }. + */ +interface PropertyField> { + void commit(M wrappedObject); + + void reload(M wrappedObject); + + void resetToDefault(); + + void updateDefault(final M wrappedObject); + + R getProperty(); + + /** + * Determines if the value in the model object and the property field are different or not. + * + * This method is used to implement the {@link ModelWrapper#differentProperty()} flag. + * + * @param wrappedObject + * the wrapped model object + * @return false if both the wrapped model object and the property field have the same value, + * otherwise true + */ + boolean isDifferent(M wrappedObject); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/BooleanImmutableSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/BooleanImmutableSetter.java new file mode 100644 index 000000000..aa1c3feb6 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/BooleanImmutableSetter.java @@ -0,0 +1,25 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.BiFunction; + +/** + * A functional interface to define an immutable "setter" method of type {@link Boolean}. + * As the model element is immutable this method is not a real "setter". + * Instead it returns a new immutable copy of the original model element that has the + * specified field updated to the new value. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface BooleanImmutableSetter extends BiFunction { + + /** + * @param model + * the model instance. + * @param newValue + * the new value to be set. + */ + @Override + M apply(M model, Boolean newValue); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/DoubleImmutableSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/DoubleImmutableSetter.java new file mode 100644 index 000000000..c4403858d --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/DoubleImmutableSetter.java @@ -0,0 +1,25 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.BiFunction; + +/** + * A functional interface to define an immutable "setter" method of type {@link Double}. + * As the model element is immutable this method is not a real "setter". + * Instead it returns a new immutable copy of the original model element that has the + * specified field updated to the new value. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface DoubleImmutableSetter extends BiFunction { + + /** + * @param model + * the model instance. + * @param newValue + * the new value to be set. + */ + @Override + M apply(M model, Double newValue); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/FloatImmutableSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/FloatImmutableSetter.java new file mode 100644 index 000000000..4cefc3e4a --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/FloatImmutableSetter.java @@ -0,0 +1,25 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.BiFunction; + +/** + * A functional interface to define an immutable "setter" method of type {@link Float}. + * As the model element is immutable this method is not a real "setter". + * Instead it returns a new immutable copy of the original model element that has the + * specified field updated to the new value. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface FloatImmutableSetter extends BiFunction { + + /** + * @param model + * the model instance. + * @param newValue + * the new value to be set. + */ + @Override + M apply(M model, Float newValue); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/IntImmutableSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/IntImmutableSetter.java new file mode 100644 index 000000000..c5faca659 --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/IntImmutableSetter.java @@ -0,0 +1,24 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.BiFunction; + +/** + * A functional interface to define an immutable "setter" method of type {@link Integer}. + * As the model element is immutable this method is not a real "setter". + * Instead it returns a new immutable copy of the original model element that has the + * specified field updated to the new value. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface IntImmutableSetter extends BiFunction { + /** + * @param model + * the model instance. + * @param newValue + * the new value to be set. + */ + @Override + M apply(M model, Integer newValue); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ListImmutableSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ListImmutableSetter.java new file mode 100644 index 000000000..4a7fa6b4a --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ListImmutableSetter.java @@ -0,0 +1,26 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.List; +import java.util.function.BiFunction; + +/** + * A functional interface to define an immutable "setter" method of type {@link List}. + * As the model element is immutable this method is not a real "setter". + * Instead it returns a new immutable copy of the original model element that has the + * specified field updated to the new value. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface ListImmutableSetter extends BiFunction, M> { + + /** + * @param model + * the model instance. + * @param newElements + * the new elements of this list field. + */ + @Override + M apply(M model, List newElements); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/LongImmutableSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/LongImmutableSetter.java new file mode 100644 index 000000000..767b8498b --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/LongImmutableSetter.java @@ -0,0 +1,26 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.BiFunction; + +/** + * A functional interface to define an immutable "setter" method of type {@link Long}. + * As the model element is immutable this method is not a real "setter". + * Instead it returns a new immutable copy of the original model element that has the + * specified field updated to the new value. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface LongImmutableSetter extends BiFunction { + + /** + * @param model + * the model instance. + * @param newValue + * the new value to be set. + */ + @Override + M apply(M model, Long newValue); +} + diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ObjectImmutableSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ObjectImmutableSetter.java new file mode 100644 index 000000000..71e664dbb --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/ObjectImmutableSetter.java @@ -0,0 +1,25 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.BiFunction; + +/** + * A functional interface to define an immutable "setter" method of a generic type. + * As the model element is immutable this method is not a real "setter". + * Instead it returns a new immutable copy of the original model element that has the + * specified field updated to the new value. + * + * @param the generic type of the model. + * @param the generic type of the field. + */ +@FunctionalInterface +public interface ObjectImmutableSetter extends BiFunction { + + /** + * @param model + * the model instance. + * @param newValue + * the new value to be set. + */ + @Override + M apply(M model, T newValue); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/StringImmutableSetter.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/StringImmutableSetter.java new file mode 100644 index 000000000..670295cbd --- /dev/null +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/mapping/accessorfunctions/StringImmutableSetter.java @@ -0,0 +1,25 @@ +package de.saxsys.mvvmfx.utils.mapping.accessorfunctions; + +import java.util.function.BiFunction; + +/** + * A functional interface to define an immutable "setter" method of type {@link String}. + * As the model element is immutable this method is not a real "setter". + * Instead it returns a new immutable copy of the original model element that has the + * specified field updated to the new value. + * + * @param + * the generic type of the model. + */ +@FunctionalInterface +public interface StringImmutableSetter extends BiFunction { + + /** + * @param model + * the model instance. + * @param newValue + * the new value to be set. + */ + @Override + M apply(M model, String newValue); +} diff --git a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelper.java b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelper.java index a911aa50c..8af397e14 100644 --- a/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelper.java +++ b/mvvmfx/src/main/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelper.java @@ -111,6 +111,14 @@ public void receivedNotification(String key, Object... payload) { notifications.add(new Pair<>(key, payload)); } + /** + * @return the list of received notifications + */ + public List> getReceivedNotifications() { + waitForUiThread(); + return notifications; + } + /** * @return the number of received notifications. */ diff --git a/mvvmfx/src/test/java/FxmlViewinDefaultPackageTest.java b/mvvmfx/src/test/java/FxmlViewinDefaultPackageTest.java index 18d851e3a..44c82eb4f 100644 --- a/mvvmfx/src/test/java/FxmlViewinDefaultPackageTest.java +++ b/mvvmfx/src/test/java/FxmlViewinDefaultPackageTest.java @@ -16,9 +16,10 @@ import de.saxsys.mvvmfx.FluentViewLoader; import de.saxsys.mvvmfx.ViewTuple; import de.saxsys.mvvmfx.internal.viewloader.example.TestViewModel; -import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; -import org.junit.Test; -import org.junit.runner.RunWith; +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + import static org.assertj.core.api.Assertions.assertThat; @@ -27,7 +28,7 @@ * * A FxmlView located in the default package couldn't be loaded because a NullPointerException was thrown. */ -@RunWith(JfxRunner.class) +@ExtendWith(JfxToolkitExtension.class) public class FxmlViewinDefaultPackageTest { diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/DependencyInjectorTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/DependencyInjectorTest.java index 87260abdd..23703e12e 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/DependencyInjectorTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/DependencyInjectorTest.java @@ -20,12 +20,12 @@ import javafx.util.Callback; import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; /** - * This test verifies the behaviour of the {@link de.saxsys.mvvmfx.internal.viewloader.DependencyInjector}. + * This test verifies the behaviour of the {@link DependencyInjector}. */ public class DependencyInjectorTest { @@ -57,7 +57,7 @@ private ExampleWithPrivateConstructor() { } } - @Before + @BeforeEach public void setup() { injector = new DependencyInjector(); } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_API_Test.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_API_Test.java index c424de914..991cba526 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_API_Test.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_API_Test.java @@ -27,8 +27,8 @@ import de.saxsys.mvvmfx.ViewTuple; import javafx.scene.layout.VBox; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import de.saxsys.mvvmfx.internal.viewloader.example.TestFxmlView; import de.saxsys.mvvmfx.internal.viewloader.example.TestJavaView; @@ -36,7 +36,7 @@ /** - * This test verifies the API of the {@link de.saxsys.mvvmfx.FluentViewLoader}. The functionality of loading Views is + * This test verifies the API of the {@link FluentViewLoader}. The functionality of loading Views is * not part of this test as it is already tested in other tests for the ViewLoader itself. */ public class FluentViewLoader_API_Test { @@ -44,7 +44,7 @@ public class FluentViewLoader_API_Test { private ResourceBundle resourceBundle; - @Before + @BeforeEach public void setup() throws IOException { resourceBundle = new PropertyResourceBundle(new StringReader("")); } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_FxmlView_Test.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_FxmlView_Test.java index 20f324120..797834a1b 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_FxmlView_Test.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_FxmlView_Test.java @@ -21,12 +21,13 @@ import de.saxsys.mvvmfx.ViewTuple; import de.saxsys.mvvmfx.internal.viewloader.example.*; import de.saxsys.mvvmfx.testingutils.ExceptionUtils; -import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; import javafx.fxml.LoadException; import javafx.scene.layout.VBox; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import java.io.IOException; import java.io.StringReader; @@ -42,12 +43,12 @@ * * @author manuel.mauky */ -@RunWith(JfxRunner.class) +@ExtendWith(JfxToolkitExtension.class) public class FluentViewLoader_FxmlView_Test { private ResourceBundle resourceBundle; - @Before + @BeforeEach public void setup() throws Exception { resourceBundle = new PropertyResourceBundle(new StringReader("")); } @@ -233,10 +234,13 @@ public void testUseExistingCodeBehindFailWhenControllerIsDefinedInFXML() { } } - @Test(expected = RuntimeException.class) + @Test public void testLoadFailNoSuchFxmlFile() { - ViewTuple viewTuple = FluentViewLoader.fxmlView(InvalidFxmlTestView.class) - .load(); + Assertions.assertThrows(RuntimeException.class, () -> { + ViewTuple viewTuple = FluentViewLoader + .fxmlView(InvalidFxmlTestView.class) + .load(); + }); } /** @@ -470,4 +474,143 @@ public void testExistingViewModelWithoutInjectionInView() { assertThat(TestViewModel.wasInitialized).isFalse(); } + + /** + * Method annotated with {@link de.saxsys.mvvmfx.Initialize} annotation initializes the ViewModel + * */ + @Test + public void testViewModelIsInitializedWithAnnotatatedMethod() { + TestViewModelWithAnnotatedInitialize.wasInitialized = false; + + ViewTuple tuple + = FluentViewLoader.fxmlView(TestFxmlViewWithViewModelWithAnnotatedInitialize.class).load(); + + TestViewModelWithAnnotatedInitialize viewModel = tuple.getViewModel(); + + assertThat(TestViewModelWithAnnotatedInitialize.wasInitialized).isTrue(); + + } + + @Test + public void testViewModelHasMultipleInitializeAnnotations() { + TestViewModelWithMultipleInitializeAnnotations.init1 = false; + TestViewModelWithMultipleInitializeAnnotations.init2 = false; + TestViewModelWithMultipleInitializeAnnotations.initialize = false; + + ViewTuple viewTuple = FluentViewLoader + .fxmlView(TestFxmlViewWithViewModelWithMultipleInitializeAnnotations.class).load(); + + assertThat(TestViewModelWithMultipleInitializeAnnotations.init1).isTrue(); + assertThat(TestViewModelWithMultipleInitializeAnnotations.init2).isTrue(); + assertThat(TestViewModelWithMultipleInitializeAnnotations.initialize).isTrue(); + } + + @Test + public void testLoadFxmlViewTupleWithCustomPath() throws IOException { + + TestFxmlPathView.instanceCounter = 0; + TestViewModel.instanceCounter = 0; + + TestViewModel.wasInitialized = false; + + final ViewTuple viewTuple = FluentViewLoader.fxmlView(TestFxmlPathView.class) + .resourceBundle(resourceBundle).load(); + + assertThat(viewTuple).isNotNull(); + + assertThat(viewTuple.getView()).isNotNull().isInstanceOf(VBox.class); + assertThat(viewTuple.getCodeBehind()).isNotNull(); + + final TestFxmlPathView codeBehind = viewTuple.getCodeBehind(); + assertThat(codeBehind.getViewModel()).isNotNull(); + assertThat(codeBehind.resourceBundle).hasSameContent(resourceBundle); + + assertThat(codeBehind.viewModelWasNull).isFalse(); + + assertThat(TestFxmlPathView.instanceCounter).isEqualTo(1); + assertThat(TestViewModel.instanceCounter).isEqualTo(1); + assertThat(TestViewModel.wasInitialized).isTrue(); + } + + @Test + public void testFxmlViewModelAsControllerException(){ + try { + FluentViewLoader.fxmlView(TestFxmlViewModelAsController.class) + .load(); + } catch (RuntimeException e){ + assertThat(ExceptionUtils.getRootCause(e)).isInstanceOf(IllegalStateException.class); + assertThat(ExceptionUtils.getRootCause(e)) + .hasMessageContaining("A ViewModel class") + .hasMessageContaining("was referenced in an FXML file") + .hasMessageContaining("as the fx:controller"); + } + + //with ControllerFactoryWithCustomViewModel + try { + FluentViewLoader.fxmlView(TestFxmlViewModelAsController.class) + .viewModel(new TestFxmlViewModelAsControllerViewModel()) + .load(); + } catch (RuntimeException e){ + assertThat(ExceptionUtils.getRootCause(e)).isInstanceOf(IllegalStateException.class); + assertThat(ExceptionUtils.getRootCause(e)) + .hasMessageContaining("A ViewModel class") + .hasMessageContaining("was referenced in an FXML file") + .hasMessageContaining("as the fx:controller"); + } + } + + @Test + public void testFxmlViewModelAsControllerWithCustomPath(){ + try { + FluentViewLoader.fxmlView(TestFxmlViewModelAsControllerWithCustomPathView.class) + .load(); + } catch (RuntimeException e){ + assertThat(ExceptionUtils.getRootCause(e)).isInstanceOf(IllegalStateException.class); + assertThat(ExceptionUtils.getRootCause(e)) + .hasMessageContaining("A ViewModel class") + .hasMessageContaining("was referenced in an FXML file") + .hasMessageContaining("as the fx:controller"); + } + + //with ControllerFactoryWithCustomViewModel + try { + FluentViewLoader.fxmlView(TestFxmlViewModelAsControllerWithCustomPathView.class) + .viewModel(new TestFxmlViewModelAsControllerWithCustomPathViewModel()) + .load(); + } catch (RuntimeException e){ + assertThat(ExceptionUtils.getRootCause(e)).isInstanceOf(IllegalStateException.class); + assertThat(ExceptionUtils.getRootCause(e)) + .hasMessageContaining("A ViewModel class") + .hasMessageContaining("was referenced in an FXML file") + .hasMessageContaining("as the fx:controller"); + } + + } + + @Test + public void testFxmlViewModelAsControllerFxInclude(){ + try { + FluentViewLoader.fxmlView(TestFxmlViewModelAsControllerParent.class) + .load(); + } catch (RuntimeException e){ + assertThat(ExceptionUtils.getRootCause(e)).isInstanceOf(IllegalStateException.class); + assertThat(ExceptionUtils.getRootCause(e)) + .hasMessageContaining("A ViewModel class") + .hasMessageContaining("was referenced in an FXML file") + .hasMessageContaining("as the fx:controller"); + } + + //with ControllerFactoryWithCustomViewModel + try { + FluentViewLoader.fxmlView(TestFxmlViewModelAsControllerParent.class) + .viewModel(new TestFxmlViewModelAsControllerParentViewModel()) + .load(); + } catch (RuntimeException e){ + assertThat(ExceptionUtils.getRootCause(e)).isInstanceOf(IllegalStateException.class); + assertThat(ExceptionUtils.getRootCause(e)) + .hasMessageContaining("A ViewModel class") + .hasMessageContaining("was referenced in an FXML file") + .hasMessageContaining("as the fx:controller"); + } + } } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_JavaView_Test.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_JavaView_Test.java index 93bff07c3..77e67becb 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_JavaView_Test.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_JavaView_Test.java @@ -24,9 +24,9 @@ import de.saxsys.mvvmfx.testingutils.ExceptionUtils; import javafx.fxml.Initializable; import javafx.scene.layout.VBox; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.io.StringReader; import java.lang.reflect.Constructor; @@ -41,32 +41,32 @@ /** - * This test verifies that loading views of type {@link de.saxsys.mvvmfx.JavaView} works correctly. + * This test verifies that loading views of type {@link JavaView} works correctly. * * This includes the handling of initialization and injection of the ViewModel and the resourceBundle. * * The injection and initialization is similar to that of FXML files. It can be done explicit by the use of the - * {@link javafx.fxml.Initializable} interface or implicit by using the naming conventions of the + * {@link Initializable} interface or implicit by using the naming conventions of the * {@link javafx.fxml.FXMLLoader}. * * This naming conventions are: *

      * - *
    • a public field of type {@link java.util.ResourceBundle} named "resources" gets the current ResourceBundle + *
    • a public field of type {@link ResourceBundle} named "resources" gets the current ResourceBundle * injected.
    • *
    • a public no-arg method named "initialize" is called after the injection of other resources is finished.
    • * *
    * * The third convention of the FXMLLoader to inject the path of the FXML file to a public field of type - * {@link java.net.URL} named "location" is NOT done by mvvmfx because it doesn't make sense for Java written Views (as + * {@link URL} named "location" is NOT done by mvvmfx because it doesn't make sense for Java written Views (as * there is no FXML file at all). */ public class FluentViewLoader_JavaView_Test { private ResourceBundle resourceBundle; - @Before + @BeforeEach public void before() throws Exception { resourceBundle = new PropertyResourceBundle(new StringReader("")); @@ -90,14 +90,14 @@ public void before() throws Exception { }); } - @After + @AfterEach public void after() { MvvmFX.setCustomDependencyInjector(null); } /** - * Verify that the loaded {@link de.saxsys.mvvmfx.ViewTuple} contains all expected references. + * Verify that the loaded {@link ViewTuple} contains all expected references. */ @Test public void testViewTuple() { @@ -402,7 +402,7 @@ void initialize() throws Exception { /** - * When the {@link javafx.fxml.Initializable} interface is implemented, the implicit initialize method may not be + * When the {@link Initializable} interface is implemented, the implicit initialize method may not be * called. */ @Test @@ -497,7 +497,7 @@ public ResourceBundle getResources() { /** * The naming conventions say that the field for the resourceBundle may be named "resources". The injection is still - * working when the type of the field is not {@link java.util.ResourceBundle}. + * working when the type of the field is not {@link ResourceBundle}. */ @Test public void testResourcesFieldHasOtherTypeAndIsStillInjected() { @@ -548,7 +548,7 @@ public void initialize(URL location, ResourceBundle resources) { } /** - * When the {@link javafx.fxml.Initializable} interface is implemented, no implicit injection should be done. + * When the {@link Initializable} interface is implemented, no implicit injection should be done. */ @Test public void testResourceBundleIsNotInjectedImplicitWhenInitializeableIsImplemented() { diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_ResourceBundle_Test.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_ResourceBundle_Test.java index 00983d8f0..d9446e248 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_ResourceBundle_Test.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/FluentViewLoader_ResourceBundle_Test.java @@ -29,18 +29,20 @@ import de.saxsys.mvvmfx.resourcebundle.global.TestView; import de.saxsys.mvvmfx.resourcebundle.global.TestViewModel; import de.saxsys.mvvmfx.testingutils.ExceptionUtils; +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; import javafx.scene.layout.VBox; -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import de.saxsys.mvvmfx.FluentViewLoader; import de.saxsys.mvvmfx.InjectResourceBundle; import de.saxsys.mvvmfx.JavaView; import de.saxsys.mvvmfx.ViewTuple; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.runner.RunWith; /** @@ -49,14 +51,14 @@ * A resourceBundle can be injected into the View with default behaviour of JavaFX. Additionally the user can use the * mvvmfx annotation {@link InjectResourceBundle} to inject the resourceBundle in the View and in the ViewModel. */ -@RunWith(JfxRunner.class) +@ExtendWith(JfxToolkitExtension.class) public class FluentViewLoader_ResourceBundle_Test { private ResourceBundle resourceBundle; private ResourceBundle globalResourceBundle; - @Before + @BeforeEach public void setup() throws Exception { resourceBundle = new ListResourceBundle() { @Override @@ -82,7 +84,7 @@ protected Object[][] getContents() { MvvmFX.setGlobalResourceBundle(null); } - @After + @AfterEach public void tearDown() { MvvmFX.setGlobalResourceBundle(null); } @@ -210,7 +212,7 @@ public void success_java_injectionWithExistingViewModel() { * but no resourceBundle was provided at loading time. Therefore an exception is thrown. */ @Test - // @Ignore("until fixed. See issue #435") + // @Disabled("until fixed. See issue #435") public void fail_noResourceBundleGivenForViewAndViewModel() { MvvmFX.setGlobalResourceBundle(null); diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/MockableViewLoaderTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/MockableViewLoaderTest.java index f15cce5c7..db29ff3e8 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/MockableViewLoaderTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/MockableViewLoaderTest.java @@ -16,7 +16,7 @@ package de.saxsys.mvvmfx.internal.viewloader; import de.saxsys.mvvmfx.ViewTuple; -import org.junit.Test; +import org.junit.jupiter.api.Test; import de.saxsys.mvvmfx.FluentViewLoader; import de.saxsys.mvvmfx.internal.viewloader.example.TestFxmlView; diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/ResourceBundleInjectorTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/ResourceBundleInjectorTest.java index f2be52263..f51a1f198 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/ResourceBundleInjectorTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/ResourceBundleInjectorTest.java @@ -21,8 +21,9 @@ import java.util.PropertyResourceBundle; import java.util.ResourceBundle; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import de.saxsys.mvvmfx.InjectResourceBundle; @@ -33,7 +34,7 @@ public class ResourceBundleInjectorTest { private ResourceBundle resourceBundle; - @Before + @BeforeEach public void setup() throws Exception { resourceBundle = new PropertyResourceBundle(new StringReader("")); } @@ -66,7 +67,7 @@ class Example { assertThat(example.resourceBundle).isEqualTo(resourceBundle); } - @Test(expected = IllegalStateException.class) + @Test public void fail_wrongType() { class Example { @InjectResourceBundle @@ -74,8 +75,10 @@ class Example { } Example example = new Example(); - - ResourceBundleInjector.injectResourceBundle(example, resourceBundle); + + Assertions.assertThrows(IllegalStateException.class, () -> { + ResourceBundleInjector.injectResourceBundle(example, resourceBundle); + }); } @Test @@ -91,7 +94,7 @@ class Example { assertThat(example.resourceBundle).isNull(); } - @Test(expected = IllegalStateException.class) + @Test public void fail_annotationIsPresentButNoResourceBundleProvided() { class Example { @InjectResourceBundle @@ -99,8 +102,10 @@ class Example { } Example example = new Example(); - - ResourceBundleInjector.injectResourceBundle(example, ResourceBundleManager.EMPTY_RESOURCE_BUNDLE); + + Assertions.assertThrows(IllegalStateException.class, () -> { + ResourceBundleInjector.injectResourceBundle(example, ResourceBundleManager.EMPTY_RESOURCE_BUNDLE); + }); } /** @@ -123,7 +128,7 @@ class Example { * If the annotation is present, even when the type of the field is wrong, an exception has to be thrown when no * resourceBundle was provided. */ - @Test(expected = IllegalStateException.class) + @Test public void fail_wrongTypeAndNoResourceBundleProvided() { class Example { @InjectResourceBundle @@ -131,8 +136,10 @@ class Example { } Example example = new Example(); - - ResourceBundleInjector.injectResourceBundle(example, ResourceBundleManager.EMPTY_RESOURCE_BUNDLE); + + Assertions.assertThrows(IllegalStateException.class, () -> { + ResourceBundleInjector.injectResourceBundle(example, ResourceBundleManager.EMPTY_RESOURCE_BUNDLE); + }); } @@ -173,7 +180,7 @@ class Example { /** * When the type of the field is wrong, an exception should be thrown even if the optional attribute is set to true. */ - @Test(expected = IllegalStateException.class) + @Test public void fail_optionalIsTrueButWrongType() { class Example { @InjectResourceBundle(optional = true) @@ -181,8 +188,10 @@ class Example { } Example example = new Example(); - - ResourceBundleInjector.injectResourceBundle(example, resourceBundle); + + Assertions.assertThrows(IllegalStateException.class, () -> { + ResourceBundleInjector.injectResourceBundle(example, resourceBundle); + }); } /** @@ -200,9 +209,9 @@ class Example { } Example example = new Example(); - + ResourceBundleInjector.injectResourceBundle(example, resourceBundle); - + assertThat(example.resourceBundle).isEqualTo(resourceBundle); assertThat(example.resourceBundleToo).isEqualTo(resourceBundle); } @@ -211,7 +220,7 @@ class Example { * When multiple fields are available, an exception is thrown when at least one of the fields has a wrong type. This * is true even if one of the fields is correct. */ - @Test(expected = IllegalStateException.class) + @Test public void fail_multipleResourceBundleFieldsOneHasWrongType() { class Example { @InjectResourceBundle @@ -222,8 +231,10 @@ class Example { } Example example = new Example(); - - ResourceBundleInjector.injectResourceBundle(example, resourceBundle); + + Assertions.assertThrows(IllegalStateException.class, () -> { + ResourceBundleInjector.injectResourceBundle(example, resourceBundle); + }); } } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/ResourceBundleManagerTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/ResourceBundleManagerTest.java index f6dfed8bc..178131f5b 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/ResourceBundleManagerTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/ResourceBundleManagerTest.java @@ -15,8 +15,8 @@ ******************************************************************************/ package de.saxsys.mvvmfx.internal.viewloader; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.ListResourceBundle; import java.util.MissingResourceException; @@ -46,7 +46,7 @@ public class ResourceBundleManagerTest { private ResourceBundle global; private ResourceBundle other; - @Before + @BeforeEach public void setup(){ manager = new ResourceBundleManager(); diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtilsTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtilsTest.java index cd660e897..655566f44 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtilsTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/ViewLoaderReflectionUtilsTest.java @@ -18,7 +18,8 @@ import de.saxsys.mvvmfx.ViewModel; import de.saxsys.mvvmfx.internal.viewloader.example.TestViewModel; import de.saxsys.mvvmfx.internal.viewloader.example.TestViewModelWithDoubleInjection; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -55,11 +56,13 @@ class TestView implements View { assertThat(viewModel).isNull(); } - @Test(expected = IllegalStateException.class) + @Test public void testDoubleInjection() { class TestView implements View {} ViewModel viewModel = ViewLoaderReflectionUtils.createViewModel(new TestView()); - ViewLoaderReflectionUtils.initializeViewModel(viewModel); + Assertions.assertThrows(IllegalStateException.class, () -> { + ViewLoaderReflectionUtils.initializeViewModel(viewModel); + }); } } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/builderfactory/BuilderFactoryTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/builderfactory/BuilderFactoryTest.java new file mode 100644 index 000000000..dcde803bb --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/builderfactory/BuilderFactoryTest.java @@ -0,0 +1,97 @@ +package de.saxsys.mvvmfx.internal.viewloader.builderfactory; + + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import java.util.ArrayList; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.internal.util.reflection.Whitebox; + +import de.saxsys.mvvmfx.FluentViewLoader; +import de.saxsys.mvvmfx.MvvmFX; +import de.saxsys.mvvmfx.internal.viewloader.GlobalBuilderFactory; +import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; +import javafx.fxml.LoadException; +import javafx.util.Builder; +import javafx.util.BuilderFactory; + +@RunWith(JfxRunner.class) +public class BuilderFactoryTest { + + public static BuilderFactory customBuilderFactoryOne = type -> { + if(CustomTextField.class.isAssignableFrom(type)) { + return (Builder) () -> new CustomTextField("Test 1"); + } else { + return null; + } + }; + + public static BuilderFactory customBuilderFactoryTwo = type -> { + if(CustomTextField.class.isAssignableFrom(type)) { + return (Builder) () -> new CustomTextField("Test 2"); + } else { + return null; + } + }; + + @Before + @After + public void clearFactories() { + ArrayList factories = (ArrayList) Whitebox.getInternalState(GlobalBuilderFactory.getInstance(), "factories"); + + factories.clear(); + } + + @Test + public void testWithoutFactory() { + try { + FluentViewLoader.fxmlView(BuilderFactoryTestView.class).load(); + fail("expected loading to fail"); + } catch (Exception e) { + assertThat(e).hasCauseInstanceOf(LoadException.class); + assertThat(e).hasRootCauseInstanceOf(NoSuchMethodException.class); + } + } + + + @Test + public void testWithCustomFactory() { + MvvmFX.addGlobalBuilderFactory(customBuilderFactoryOne); + FluentViewLoader.fxmlView(BuilderFactoryTestView.class).load(); + } + + @Test + public void testWithMultipleFactories() { + MvvmFX.addGlobalBuilderFactory(customBuilderFactoryOne); + MvvmFX.addGlobalBuilderFactory(customBuilderFactoryTwo); + BuilderFactoryTestView codeBehind = FluentViewLoader.fxmlView(BuilderFactoryTestView.class).load().getCodeBehind(); + + assertThat(codeBehind.textField.getSpecial()).isEqualTo("Test 2"); + } + + @Test + public void testWithBuilderAtLoadingTime() { + MvvmFX.addGlobalBuilderFactory(customBuilderFactoryOne); + + BuilderFactoryTestView codeBehind = FluentViewLoader + .fxmlView(BuilderFactoryTestView.class) + .load().getCodeBehind(); + + // loading without parameter to FluentViewLoader results into the first factory to be used. + assertThat(codeBehind.textField.getSpecial()).isEqualTo("Test 1"); + + + codeBehind = FluentViewLoader + .fxmlView(BuilderFactoryTestView.class) + .builderFactory(customBuilderFactoryTwo) + .load().getCodeBehind(); + + // passing a factory as parameter results into this factory being used instead of the global one. + assertThat(codeBehind.textField.getSpecial()).isEqualTo("Test 2"); + } +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/builderfactory/BuilderFactoryTestView.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/builderfactory/BuilderFactoryTestView.java new file mode 100644 index 000000000..8c81a10de --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/builderfactory/BuilderFactoryTestView.java @@ -0,0 +1,17 @@ +package de.saxsys.mvvmfx.internal.viewloader.builderfactory; + +import de.saxsys.mvvmfx.FxmlView; +import de.saxsys.mvvmfx.InjectViewModel; +import javafx.fxml.FXML; + +public class BuilderFactoryTestView implements FxmlView { + + @FXML + public CustomTextField textField; + + @InjectViewModel + private BuilderFactoryTestViewModel viewModel; + + public void initialize() { + } +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/builderfactory/BuilderFactoryTestViewModel.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/builderfactory/BuilderFactoryTestViewModel.java new file mode 100644 index 000000000..20ebfc4cb --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/builderfactory/BuilderFactoryTestViewModel.java @@ -0,0 +1,7 @@ +package de.saxsys.mvvmfx.internal.viewloader.builderfactory; + +import de.saxsys.mvvmfx.ViewModel; + +public class BuilderFactoryTestViewModel implements ViewModel { + +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/builderfactory/CustomTextField.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/builderfactory/CustomTextField.java new file mode 100644 index 000000000..8865eb510 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/builderfactory/CustomTextField.java @@ -0,0 +1,23 @@ +package de.saxsys.mvvmfx.internal.viewloader.builderfactory; + +import javafx.scene.control.TextField; + +/** + * A custom TextField to test builder factories + */ +public class CustomTextField extends TextField { + + private final String special; + + /** + * Due to this constructor the custom control can't be used directly in FXML + * without the builder factory. + */ + public CustomTextField(String special) { + this.special = special; + } + + public String getSpecial() { + return special; + } +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlPathView.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlPathView.java new file mode 100644 index 000000000..a346aa579 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlPathView.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright 2015 Alexander Casall, Manuel Mauky + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx.internal.viewloader.example; + +import de.saxsys.mvvmfx.FxmlPath; +import de.saxsys.mvvmfx.FxmlView; +import de.saxsys.mvvmfx.InjectViewModel; +import javafx.fxml.Initializable; + +import java.net.URL; +import java.util.ResourceBundle; + + +/** + * This class is used as example View class that uses a custom path FXML. + * + * @authors manuel.mauky, rafael.guillen + */ +@FxmlPath(value = "/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithCustomPath.fxml") +public class TestFxmlPathView implements FxmlView, Initializable { + public static int instanceCounter = 0; + public URL url; + public ResourceBundle resourceBundle; + public boolean viewModelWasNull = true; + @InjectViewModel + private TestViewModel viewModel; + + public TestFxmlPathView() { + instanceCounter++; + } + + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + this.url = url; + this.resourceBundle = resourceBundle; + + viewModelWasNull = viewModel == null; + } + + public TestViewModel getViewModel() { + return viewModel; + } + +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsController.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsController.java new file mode 100644 index 000000000..c4f529dd9 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsController.java @@ -0,0 +1,13 @@ +package de.saxsys.mvvmfx.internal.viewloader.example; + +import de.saxsys.mvvmfx.FxmlPath; +import de.saxsys.mvvmfx.FxmlView; +import de.saxsys.mvvmfx.InjectViewModel; +import de.saxsys.mvvmfx.internal.viewloader.View; + +public class TestFxmlViewModelAsController implements FxmlView { + + @InjectViewModel + TestFxmlViewModelAsControllerViewModel viewModel; + +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerChild.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerChild.java new file mode 100644 index 000000000..be738bfc9 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerChild.java @@ -0,0 +1,6 @@ +package de.saxsys.mvvmfx.internal.viewloader.example; + +import de.saxsys.mvvmfx.FxmlView; + +public class TestFxmlViewModelAsControllerChild implements FxmlView{ +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerChildViewModel.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerChildViewModel.java new file mode 100644 index 000000000..625f2e171 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerChildViewModel.java @@ -0,0 +1,6 @@ +package de.saxsys.mvvmfx.internal.viewloader.example; + +import de.saxsys.mvvmfx.ViewModel; + +public class TestFxmlViewModelAsControllerChildViewModel implements ViewModel{ +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerParent.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerParent.java new file mode 100644 index 000000000..705a59fdb --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerParent.java @@ -0,0 +1,6 @@ +package de.saxsys.mvvmfx.internal.viewloader.example; + +import de.saxsys.mvvmfx.FxmlView; + +public class TestFxmlViewModelAsControllerParent implements FxmlView{ +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerParentViewModel.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerParentViewModel.java new file mode 100644 index 000000000..8b2aaa155 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerParentViewModel.java @@ -0,0 +1,6 @@ +package de.saxsys.mvvmfx.internal.viewloader.example; + +import de.saxsys.mvvmfx.ViewModel; + +public class TestFxmlViewModelAsControllerParentViewModel implements ViewModel{ +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerViewModel.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerViewModel.java new file mode 100644 index 000000000..b64c2b198 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerViewModel.java @@ -0,0 +1,6 @@ +package de.saxsys.mvvmfx.internal.viewloader.example; + +import de.saxsys.mvvmfx.ViewModel; + +public class TestFxmlViewModelAsControllerViewModel implements ViewModel { +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerWithCustomPathView.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerWithCustomPathView.java new file mode 100644 index 000000000..3ab9c1945 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerWithCustomPathView.java @@ -0,0 +1,8 @@ +package de.saxsys.mvvmfx.internal.viewloader.example; + +import de.saxsys.mvvmfx.FxmlPath; +import de.saxsys.mvvmfx.FxmlView; + +@FxmlPath("/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerWithCustomPath.fxml") +public class TestFxmlViewModelAsControllerWithCustomPathView implements FxmlView { +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerWithCustomPathViewModel.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerWithCustomPathViewModel.java new file mode 100644 index 000000000..db50de7ee --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerWithCustomPathViewModel.java @@ -0,0 +1,6 @@ +package de.saxsys.mvvmfx.internal.viewloader.example; + +import de.saxsys.mvvmfx.ViewModel; + +public class TestFxmlViewModelAsControllerWithCustomPathViewModel implements ViewModel{ +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithAnnotatedInitialize.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithAnnotatedInitialize.java new file mode 100644 index 000000000..2358e739b --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithAnnotatedInitialize.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright 2017 Gleb Koval + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx.internal.viewloader.example; + +import de.saxsys.mvvmfx.FxmlView; +import de.saxsys.mvvmfx.InjectViewModel; + +/** + * This class is used as example View class that uses ViewModel initialized with + * method annotated with {@link de.saxsys.mvvmfx.Initialize} annotation + * + * @author Gleb Koval + */ +public class TestFxmlViewWithViewModelWithAnnotatedInitialize implements FxmlView { + + @InjectViewModel + private TestViewModelWithAnnotatedInitialize viewModel; + +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithMultipleInitializeAnnotations.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithMultipleInitializeAnnotations.java new file mode 100644 index 000000000..8cd41ac74 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithMultipleInitializeAnnotations.java @@ -0,0 +1,11 @@ +package de.saxsys.mvvmfx.internal.viewloader.example; + +import de.saxsys.mvvmfx.FxmlView; +import de.saxsys.mvvmfx.InjectViewModel; + +public class TestFxmlViewWithViewModelWithMultipleInitializeAnnotations implements FxmlView { + + @InjectViewModel + private TestViewModelWithMultipleInitializeAnnotations viewModel; + +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestViewModelWithAnnotatedInitialize.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestViewModelWithAnnotatedInitialize.java new file mode 100644 index 000000000..791abc9f7 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestViewModelWithAnnotatedInitialize.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright 2017 Gleb Koval + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package de.saxsys.mvvmfx.internal.viewloader.example; + +import de.saxsys.mvvmfx.Initialize; +import de.saxsys.mvvmfx.ViewModel; + +/** + * This class is used as example ViewModel class that uses init method annotated with {@link Initialize} + * + * @author Gleb Koval + */ +public class TestViewModelWithAnnotatedInitialize implements ViewModel { + + public static boolean wasInitialized = false; + + @Initialize + private void init() { + wasInitialized = true; + } + +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestViewModelWithMultipleInitializeAnnotations.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestViewModelWithMultipleInitializeAnnotations.java new file mode 100644 index 000000000..43eefbd14 --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/example/TestViewModelWithMultipleInitializeAnnotations.java @@ -0,0 +1,28 @@ +package de.saxsys.mvvmfx.internal.viewloader.example; + +import de.saxsys.mvvmfx.Initialize; +import de.saxsys.mvvmfx.ViewModel; + +public class TestViewModelWithMultipleInitializeAnnotations implements ViewModel { + + public static boolean init1 = false; + public static boolean init2 = false; + public static boolean initialize = false; + + + @Initialize + public void init1() { + init1 = true; + } + + @Initialize + private void init2(){ + init2 = true; + } + + public void initialize() { + initialize = true; + } + + +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/lifecycle/LifecycleTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/lifecycle/LifecycleTest.java index 1cd03af62..7eebe7c57 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/lifecycle/LifecycleTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/internal/viewloader/lifecycle/LifecycleTest.java @@ -15,20 +15,20 @@ import de.saxsys.mvvmfx.internal.viewloader.lifecycle.example_notification.LifecycleNotificationViewModel; import de.saxsys.mvvmfx.internal.viewloader.lifecycle.example_notification_without_lifecycle.NotificationWithoutLifecycleView; import de.saxsys.mvvmfx.internal.viewloader.lifecycle.example_notification_without_lifecycle.NotificationWithoutLifecycleViewModel; +import de.saxsys.mvvmfx.testingutils.FxTestingUtils; import de.saxsys.mvvmfx.testingutils.GCVerifier; -import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; -import de.saxsys.mvvmfx.testingutils.jfxrunner.TestInJfxThread; +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; import de.saxsys.mvvmfx.utils.notifications.DefaultNotificationCenter; import de.saxsys.mvvmfx.utils.notifications.NotificationCenterFactory; import javafx.scene.Scene; import javafx.scene.layout.VBox; import javafx.stage.Stage; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import static org.assertj.core.api.Assertions.assertThat; -@RunWith(JfxRunner.class) +@ExtendWith(JfxToolkitExtension.class) public class LifecycleTest { /** @@ -42,68 +42,70 @@ public class LifecycleTest { * of the lifecycle. */ @Test - @TestInJfxThread public void testLifecycleWithSubViewsWithoutGC() { - LifecycleTestRootViewModel.onViewAddedCalled = 0; - LifecycleTestRootViewModel.onViewRemovedCalled = 0; - LifecycleTestSub1ViewModel.onViewAddedCalled = 0; - LifecycleTestSub1ViewModel.onViewRemovedCalled = 0; - LifecycleTestSub2ViewModel.onViewAddedCalled = 0; - LifecycleTestSub2ViewModel.onViewRemovedCalled = 0; - - ViewTuple viewTuple = FluentViewLoader.fxmlView(LifecycleTestRootView.class).load(); - - // the root view is not directly added to the Scene but encapsulated in - // another container - VBox subContainer = new VBox(); - subContainer.getChildren().add(viewTuple.getView()); - - VBox container = new VBox(); - - Stage stage = new Stage(); - Scene scene = new Scene(container); - stage.setScene(scene); - - - // before adding to scene - assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(0); - assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(0); - assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(0); - assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(0); - assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(0); - assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(0); - - // add rootView to container - container.getChildren().add(subContainer); - - assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(1); - assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(0); - assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(1); - assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(0); - assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(1); - assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(0); - - // remove from container - container.getChildren().clear(); - - assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(1); - assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(1); - assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(1); - assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(1); - assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(1); - assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(1); - - - // add again to container - container.getChildren().add(subContainer); - - // the lifecycle methods are invoked again - assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(2); - assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(1); - assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(2); - assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(1); - assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(2); - assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(1); + FxTestingUtils.runInFXThread(() -> { + LifecycleTestRootViewModel.onViewAddedCalled = 0; + LifecycleTestRootViewModel.onViewRemovedCalled = 0; + LifecycleTestSub1ViewModel.onViewAddedCalled = 0; + LifecycleTestSub1ViewModel.onViewRemovedCalled = 0; + LifecycleTestSub2ViewModel.onViewAddedCalled = 0; + LifecycleTestSub2ViewModel.onViewRemovedCalled = 0; + + ViewTuple viewTuple = FluentViewLoader + .fxmlView(LifecycleTestRootView.class).load(); + + // the root view is not directly added to the Scene but encapsulated in + // another container + VBox subContainer = new VBox(); + subContainer.getChildren().add(viewTuple.getView()); + + VBox container = new VBox(); + + Stage stage = new Stage(); + Scene scene = new Scene(container); + stage.setScene(scene); + + + // before adding to scene + assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(0); + assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(0); + assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(0); + assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(0); + assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(0); + assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(0); + + // add rootView to container + container.getChildren().add(subContainer); + + assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(1); + assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(0); + assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(1); + assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(0); + assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(1); + assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(0); + + // remove from container + container.getChildren().clear(); + + assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(1); + assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(1); + assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(1); + assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(1); + assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(1); + assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(1); + + + // add again to container + container.getChildren().add(subContainer); + + // the lifecycle methods are invoked again + assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(2); + assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(1); + assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(2); + assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(1); + assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(2); + assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(1); + }); } @@ -120,89 +122,91 @@ public void testLifecycleWithSubViewsWithoutGC() { * when the view is added to the scene a second time. */ @Test - @TestInJfxThread public void testLifecycleWithSubViewsWithGC() { - LifecycleTestRootViewModel.onViewAddedCalled = 0; - LifecycleTestRootViewModel.onViewRemovedCalled = 0; - LifecycleTestSub1ViewModel.onViewAddedCalled = 0; - LifecycleTestSub1ViewModel.onViewRemovedCalled = 0; - LifecycleTestSub2ViewModel.onViewAddedCalled = 0; - LifecycleTestSub2ViewModel.onViewRemovedCalled = 0; - - ViewTuple viewTuple = FluentViewLoader.fxmlView(LifecycleTestRootView.class).load(); - - // GC is performed, however as we still have a reference to the viewTuple, - // nothing will be collected yet. - GCVerifier.forceGC(); - - // the root view is not directly added to the Scene. Instead it is encapsulated in - // another container - VBox subContainer = new VBox(); - subContainer.getChildren().add(viewTuple.getView()); - - VBox container = new VBox(); - - Stage stage = new Stage(); - Scene scene = new Scene(container); - stage.setScene(scene); - - - // now we clear the viewTuple reference ... - viewTuple = null; - - // and perform GC a second time. - // This time, both the ViewModel and the CodeBehind would be collected - // if the framework hadn't prevented it. - GCVerifier.forceGC(); - - // before adding to scene - assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(0); - assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(0); - assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(0); - assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(0); - assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(0); - assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(0); - - // add rootView to container - container.getChildren().add(subContainer); - - // onViewAdded is invoked for all viewModels - assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(1); - assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(0); - assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(1); - assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(0); - assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(1); - assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(0); - - GCVerifier.forceGC(); - - // remove from container - container.getChildren().clear(); - - GCVerifier.forceGC(); - - // onViewRemoved is invoked on all ViewModels - assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(1); - assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(1); - assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(1); - assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(1); - assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(1); - assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(1); - - // Here the guarantee of the framework ends. This time the viewModels - // will be collected - GCVerifier.forceGC(); - - // add again to container - container.getChildren().add(subContainer); - - // no methods are invoked. - assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(1); - assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(1); - assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(1); - assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(1); - assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(1); - assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(1); + FxTestingUtils.runInFXThread(() -> { + LifecycleTestRootViewModel.onViewAddedCalled = 0; + LifecycleTestRootViewModel.onViewRemovedCalled = 0; + LifecycleTestSub1ViewModel.onViewAddedCalled = 0; + LifecycleTestSub1ViewModel.onViewRemovedCalled = 0; + LifecycleTestSub2ViewModel.onViewAddedCalled = 0; + LifecycleTestSub2ViewModel.onViewRemovedCalled = 0; + + ViewTuple viewTuple = FluentViewLoader + .fxmlView(LifecycleTestRootView.class).load(); + + // GC is performed, however as we still have a reference to the viewTuple, + // nothing will be collected yet. + GCVerifier.forceGC(); + + // the root view is not directly added to the Scene. Instead it is encapsulated in + // another container + VBox subContainer = new VBox(); + subContainer.getChildren().add(viewTuple.getView()); + + VBox container = new VBox(); + + Stage stage = new Stage(); + Scene scene = new Scene(container); + stage.setScene(scene); + + + // now we clear the viewTuple reference ... + viewTuple = null; + + // and perform GC a second time. + // This time, both the ViewModel and the CodeBehind would be collected + // if the framework hadn't prevented it. + GCVerifier.forceGC(); + + // before adding to scene + assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(0); + assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(0); + assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(0); + assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(0); + assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(0); + assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(0); + + // add rootView to container + container.getChildren().add(subContainer); + + // onViewAdded is invoked for all viewModels + assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(1); + assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(0); + assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(1); + assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(0); + assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(1); + assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(0); + + GCVerifier.forceGC(); + + // remove from container + container.getChildren().clear(); + + GCVerifier.forceGC(); + + // onViewRemoved is invoked on all ViewModels + assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(1); + assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(1); + assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(1); + assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(1); + assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(1); + assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(1); + + // Here the guarantee of the framework ends. This time the viewModels + // will be collected + GCVerifier.forceGC(); + + // add again to container + container.getChildren().add(subContainer); + + // no methods are invoked. + assertThat(LifecycleTestRootViewModel.onViewAddedCalled).isEqualTo(1); + assertThat(LifecycleTestRootViewModel.onViewRemovedCalled).isEqualTo(1); + assertThat(LifecycleTestSub1ViewModel.onViewAddedCalled).isEqualTo(1); + assertThat(LifecycleTestSub1ViewModel.onViewRemovedCalled).isEqualTo(1); + assertThat(LifecycleTestSub2ViewModel.onViewAddedCalled).isEqualTo(1); + assertThat(LifecycleTestSub2ViewModel.onViewRemovedCalled).isEqualTo(1); + }); } @@ -217,42 +221,44 @@ public void testLifecycleWithSubViewsWithGC() { * prevents Garbage collection. */ @Test - @TestInJfxThread public void testGarbageCollectionFailed() { - NotificationCenterFactory.setNotificationCenter(new DefaultNotificationCenter()); + FxTestingUtils.runInFXThread(() -> { + NotificationCenterFactory.setNotificationCenter(new DefaultNotificationCenter()); - ViewTuple viewTuple = FluentViewLoader.fxmlView(NotificationWithoutLifecycleView.class).load(); + ViewTuple viewTuple = FluentViewLoader + .fxmlView(NotificationWithoutLifecycleView.class).load(); - VBox container = new VBox(); + VBox container = new VBox(); - Stage stage = new Stage(); - Scene scene = new Scene(container); - stage.setScene(scene); + Stage stage = new Stage(); + Scene scene = new Scene(container); + stage.setScene(scene); - GCVerifier vmVerifier = GCVerifier.create(viewTuple.getViewModel()); + GCVerifier vmVerifier = GCVerifier.create(viewTuple.getViewModel()); - assertThat(vmVerifier.isAvailableForGC()).isFalse(); + assertThat(vmVerifier.isAvailableForGC()).isFalse(); - container.getChildren().add(viewTuple.getView()); + container.getChildren().add(viewTuple.getView()); - viewTuple = null; + viewTuple = null; - // The ViewModel has a listener subscribed so it isn't available for GC - assertThat(vmVerifier.isAvailableForGC()).isFalse(); + // The ViewModel has a listener subscribed so it isn't available for GC + assertThat(vmVerifier.isAvailableForGC()).isFalse(); - container.getChildren().clear(); + container.getChildren().clear(); - // even after the view isn't used anymore, the ViewModel still can't be garbage collected - // because it is still registered in the notification center - assertThat(vmVerifier.isAvailableForGC()).isFalse(); + // even after the view isn't used anymore, the ViewModel still can't be garbage collected + // because it is still registered in the notification center + assertThat(vmVerifier.isAvailableForGC()).isFalse(); - // only if we replace the notification center ... - NotificationCenterFactory.setNotificationCenter(new DefaultNotificationCenter()); + // only if we replace the notification center ... + NotificationCenterFactory.setNotificationCenter(new DefaultNotificationCenter()); - // the viewModel can be garbage collected. Of cause in practice this isn't a suitable solution - assertThat(vmVerifier.isAvailableForGC()).isTrue(); + // the viewModel can be garbage collected. Of cause in practice this isn't a suitable solution + assertThat(vmVerifier.isAvailableForGC()).isTrue(); + }); } /** @@ -266,39 +272,41 @@ public void testGarbageCollectionFailed() { * Therefore the ViewModel is available for garbage collection afterwards. */ @Test - @TestInJfxThread public void testGarbageCollection() { - NotificationCenterFactory.setNotificationCenter(new DefaultNotificationCenter()); + FxTestingUtils.runInFXThread(() -> { + NotificationCenterFactory.setNotificationCenter(new DefaultNotificationCenter()); - ViewTuple viewTuple = FluentViewLoader.fxmlView(LifecycleNotificationView.class).load(); + ViewTuple viewTuple = FluentViewLoader + .fxmlView(LifecycleNotificationView.class).load(); - VBox container = new VBox(); + VBox container = new VBox(); - Stage stage = new Stage(); - Scene scene = new Scene(container); - stage.setScene(scene); + Stage stage = new Stage(); + Scene scene = new Scene(container); + stage.setScene(scene); - GCVerifier vmVerifier = GCVerifier.create(viewTuple.getViewModel()); + GCVerifier vmVerifier = GCVerifier.create(viewTuple.getViewModel()); - assertThat(vmVerifier.isAvailableForGC()).isFalse(); + assertThat(vmVerifier.isAvailableForGC()).isFalse(); - container.getChildren().add(viewTuple.getView()); + container.getChildren().add(viewTuple.getView()); - viewTuple = null; + viewTuple = null; - // The ViewModel has a listener subscribed so it isn't available for GC now - assertThat(vmVerifier.isAvailableForGC()).isFalse(); + // The ViewModel has a listener subscribed so it isn't available for GC now + assertThat(vmVerifier.isAvailableForGC()).isFalse(); - // this triggeres the lifecycle method which is used to deregister the listener - container.getChildren().clear(); + // this triggeres the lifecycle method which is used to deregister the listener + container.getChildren().clear(); - // therefore the ViewModel can now be garbage collected. - assertThat(vmVerifier.isAvailableForGC()).isTrue(); + // therefore the ViewModel can now be garbage collected. + assertThat(vmVerifier.isAvailableForGC()).isTrue(); - // cleanup notification center to not infer with other tests - NotificationCenterFactory.setNotificationCenter(new DefaultNotificationCenter()); + // cleanup notification center to not infer with other tests + NotificationCenterFactory.setNotificationCenter(new DefaultNotificationCenter()); + }); } @@ -321,40 +329,42 @@ public void testGarbageCollection() { * In the previous implementation this resulted in only the first method was invoked. */ @Test - @TestInJfxThread public void testGcBetweenLifecycleMethods() { - LifecycleGCTestRootViewModel.onViewRemovedCalled = 0; - LifecycleGCTestSub1ViewModel.onViewRemovedCalled = 0; - LifecycleGCTestSub2ViewModel.onViewRemovedCalled = 0; + FxTestingUtils.runInFXThread(() -> { + LifecycleGCTestRootViewModel.onViewRemovedCalled = 0; + LifecycleGCTestSub1ViewModel.onViewRemovedCalled = 0; + LifecycleGCTestSub2ViewModel.onViewRemovedCalled = 0; - ViewTuple viewTuple = FluentViewLoader.fxmlView(LifecycleGCTestRootView.class).load(); + ViewTuple viewTuple = FluentViewLoader + .fxmlView(LifecycleGCTestRootView.class).load(); - VBox subContainer = new VBox(); - subContainer.getChildren().add(viewTuple.getView()); + VBox subContainer = new VBox(); + subContainer.getChildren().add(viewTuple.getView()); - VBox container = new VBox(); + VBox container = new VBox(); - Stage stage = new Stage(); - Scene scene = new Scene(container); - stage.setScene(scene); + Stage stage = new Stage(); + Scene scene = new Scene(container); + stage.setScene(scene); - viewTuple = null; + viewTuple = null; - GCVerifier.forceGC(); + GCVerifier.forceGC(); - assertThat(LifecycleGCTestRootViewModel.onViewRemovedCalled).isEqualTo(0); - assertThat(LifecycleGCTestSub1ViewModel.onViewRemovedCalled).isEqualTo(0); - assertThat(LifecycleGCTestSub2ViewModel.onViewRemovedCalled).isEqualTo(0); + assertThat(LifecycleGCTestRootViewModel.onViewRemovedCalled).isEqualTo(0); + assertThat(LifecycleGCTestSub1ViewModel.onViewRemovedCalled).isEqualTo(0); + assertThat(LifecycleGCTestSub2ViewModel.onViewRemovedCalled).isEqualTo(0); - container.getChildren().add(subContainer); + container.getChildren().add(subContainer); - GCVerifier.forceGC(); + GCVerifier.forceGC(); - container.getChildren().remove(subContainer); + container.getChildren().remove(subContainer); - assertThat(LifecycleGCTestRootViewModel.onViewRemovedCalled).isEqualTo(1); - assertThat(LifecycleGCTestSub1ViewModel.onViewRemovedCalled).isEqualTo(1); - assertThat(LifecycleGCTestSub2ViewModel.onViewRemovedCalled).isEqualTo(1); + assertThat(LifecycleGCTestRootViewModel.onViewRemovedCalled).isEqualTo(1); + assertThat(LifecycleGCTestSub1ViewModel.onViewRemovedCalled).isEqualTo(1); + assertThat(LifecycleGCTestSub2ViewModel.onViewRemovedCalled).isEqualTo(1); + }); } } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/global/GlobalResourceBundleTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/global/GlobalResourceBundleTest.java index db848414e..3a51877bc 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/global/GlobalResourceBundleTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/global/GlobalResourceBundleTest.java @@ -18,11 +18,11 @@ import de.saxsys.mvvmfx.FluentViewLoader; import de.saxsys.mvvmfx.MvvmFX; import de.saxsys.mvvmfx.ViewTuple; -import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import java.util.ResourceBundle; @@ -33,21 +33,23 @@ * * @author manuel.mauky */ -@RunWith(JfxRunner.class) +@ExtendWith(JfxToolkitExtension.class) public class GlobalResourceBundleTest { private ResourceBundle global; private ResourceBundle other; + private ResourceBundle third; - @Before + @BeforeEach public void setup(){ MvvmFX.setGlobalResourceBundle(null); global = ResourceBundle.getBundle(this.getClass().getPackage().getName() + ".global"); other = ResourceBundle.getBundle(this.getClass().getPackage().getName() + ".other"); + third = ResourceBundle.getBundle(this.getClass().getPackage().getName() + ".third"); } - @After + @AfterEach public void tearDown() { MvvmFX.setGlobalResourceBundle(null); } @@ -68,6 +70,31 @@ public void test() { // in this case "other" has the higher priority and overwrites the value defined in "global" assertThat(codeBehind.label.getText()).isEqualTo("other"); } - + + /** + * It's possible to use more then one resourceBundle with {@link FluentViewLoader}. + */ + @Test + public void testThreeResourceBundles() { + MvvmFX.setGlobalResourceBundle(global); + + final ViewTuple viewTuple = FluentViewLoader.fxmlView(TestView.class) + .resourceBundle(other) + .resourceBundle(third) + .load(); + final TestView codeBehind = viewTuple.getCodeBehind(); + + assertThat(codeBehind.resources).isNotNull(); + + assertThat(codeBehind.global_label.getText()).isEqualTo("global"); + assertThat(codeBehind.other_label.getText()).isEqualTo("other"); + + // the "third" bundle was added last in the fluent API. + // for this reason it has the highest priority and overwrites other values + assertThat(codeBehind.label.getText()).isEqualTo("third"); + + assertThat(codeBehind.resources.getString("third_label")).isEqualTo("third"); + + } } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/included/IncludedViewsTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/included/IncludedViewsTest.java index 5837b23ce..b3a782991 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/included/IncludedViewsTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/resourcebundle/included/IncludedViewsTest.java @@ -18,11 +18,11 @@ import de.saxsys.mvvmfx.FluentViewLoader; import de.saxsys.mvvmfx.MvvmFX; import de.saxsys.mvvmfx.ViewTuple; -import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import java.util.ResourceBundle; @@ -33,21 +33,21 @@ * * @author manuel.mauky */ -@RunWith(JfxRunner.class) +@ExtendWith(JfxToolkitExtension.class) public class IncludedViewsTest { private ResourceBundle root; private ResourceBundle included; - @Before + @BeforeEach public void setup(){ MvvmFX.setGlobalResourceBundle(null); root = ResourceBundle.getBundle(this.getClass().getPackage().getName() + ".root"); included = ResourceBundle.getBundle(this.getClass().getPackage().getName() + ".included"); } - @After + @AfterEach public void tearDown() { MvvmFX.setGlobalResourceBundle(null); } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/context/ContextTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/context/ContextTest.java index c3a329119..7a8ad880b 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/context/ContextTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/context/ContextTest.java @@ -3,7 +3,7 @@ import de.saxsys.mvvmfx.FluentViewLoader; import de.saxsys.mvvmfx.scopes.context.views.ScopedFxmlView; import de.saxsys.mvvmfx.scopes.context.views.ScopedFxmlViewModel; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example1/Example1ScopesTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example1/Example1ScopesTest.java index 58485ec77..3b113277f 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example1/Example1ScopesTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example1/Example1ScopesTest.java @@ -2,8 +2,9 @@ import de.saxsys.mvvmfx.FluentViewLoader; import de.saxsys.mvvmfx.scopes.example1.views.*; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.omg.SendingContext.RunTime; public class Example1ScopesTest { @@ -51,64 +52,66 @@ public void testFxmlScopedView() throws Exception { ScopedViewModelG viewModel_B_E_G = parentView.subviewBController.subviewEController.subviewGController.viewModel; - Assert.assertNotNull(viewModel_A_E); - Assert.assertNotNull(viewModel_A_E_F); - Assert.assertNotNull(viewModel_A_E_G); - Assert.assertNotNull(viewModel_B_E); - Assert.assertNotNull(viewModel_B_E_F); - Assert.assertNotNull(viewModel_B_E_G); + Assertions.assertNotNull(viewModel_A_E); + Assertions.assertNotNull(viewModel_A_E_F); + Assertions.assertNotNull(viewModel_A_E_G); + Assertions.assertNotNull(viewModel_B_E); + Assertions.assertNotNull(viewModel_B_E_F); + Assertions.assertNotNull(viewModel_B_E_G); - Assert.assertNotEquals(viewModel_A_E.testScope3, viewModel_B_E.testScope3); + Assertions.assertNotEquals(viewModel_A_E.testScope3, viewModel_B_E.testScope3); - Assert.assertEquals(viewModel_A_E.testScope3, viewModel_A_E_F.testScope3); - Assert.assertEquals(viewModel_A_E.testScope3, viewModel_A_E_G.testScope3); + Assertions.assertEquals(viewModel_A_E.testScope3, viewModel_A_E_F.testScope3); + Assertions.assertEquals(viewModel_A_E.testScope3, viewModel_A_E_G.testScope3); - Assert.assertEquals(viewModel_B_E.testScope3, viewModel_B_E_F.testScope3); - Assert.assertEquals(viewModel_B_E.testScope3, viewModel_B_E_G.testScope3); + Assertions.assertEquals(viewModel_B_E.testScope3, viewModel_B_E_F.testScope3); + Assertions.assertEquals(viewModel_B_E.testScope3, viewModel_B_E_G.testScope3); verifyScopes(viewModelA, viewModelB, viewModelCinA, viewModelCinB, viewModelDinA, viewModelDinB); } - @Test(expected = Exception.class) + @Test public void testErrorWhenNoScopeProviderFound() { final ScopesFxmlParentView parentView = FluentViewLoader.fxmlView(ScopesFxmlParentView.class) .load() .getCodeBehind(); - parentView.subviewAController.subviewCController.loadWrongScopedView(); + Assertions.assertThrows(Exception.class, () ->{ + parentView.subviewAController.subviewCController.loadWrongScopedView(); + }); } private void verifyScopes(ScopedViewModelA viewModelA, ScopedViewModelB viewModelB, ScopedViewModelC viewModelCinA, ScopedViewModelC viewModelCinB, ScopedViewModelD viewModelDinA, ScopedViewModelD viewModelDinB) { - Assert.assertNotNull(viewModelA); - Assert.assertNotNull(viewModelB); - Assert.assertNotNull(viewModelCinA); - Assert.assertNotNull(viewModelCinB); - Assert.assertNotNull(viewModelDinA); - Assert.assertNotNull(viewModelDinB); + Assertions.assertNotNull(viewModelA); + Assertions.assertNotNull(viewModelB); + Assertions.assertNotNull(viewModelCinA); + Assertions.assertNotNull(viewModelCinB); + Assertions.assertNotNull(viewModelDinA); + Assertions.assertNotNull(viewModelDinB); - Assert.assertNotNull(viewModelA.injectedScope1); - Assert.assertNotNull(viewModelB.injectedScope1); - Assert.assertNotNull(viewModelCinA.injectedScope1); - Assert.assertNotNull(viewModelCinB.injectedScope1); - Assert.assertNotNull(viewModelDinA.injectedScope1); - Assert.assertNotNull(viewModelDinA.injectedScope2); - Assert.assertNotNull(viewModelDinB.injectedScope1); - Assert.assertNotNull(viewModelDinB.injectedScope2); + Assertions.assertNotNull(viewModelA.injectedScope1); + Assertions.assertNotNull(viewModelB.injectedScope1); + Assertions.assertNotNull(viewModelCinA.injectedScope1); + Assertions.assertNotNull(viewModelCinB.injectedScope1); + Assertions.assertNotNull(viewModelDinA.injectedScope1); + Assertions.assertNotNull(viewModelDinA.injectedScope2); + Assertions.assertNotNull(viewModelDinB.injectedScope1); + Assertions.assertNotNull(viewModelDinB.injectedScope2); - Assert.assertNotEquals(viewModelA.injectedScope1, viewModelB.injectedScope1); + Assertions.assertNotEquals(viewModelA.injectedScope1, viewModelB.injectedScope1); - Assert.assertEquals(viewModelA.injectedScope1, viewModelCinA.injectedScope1); - Assert.assertEquals(viewModelA.injectedScope1, viewModelDinA.injectedScope1); + Assertions.assertEquals(viewModelA.injectedScope1, viewModelCinA.injectedScope1); + Assertions.assertEquals(viewModelA.injectedScope1, viewModelDinA.injectedScope1); - Assert.assertEquals(viewModelB.injectedScope1, viewModelCinB.injectedScope1); - Assert.assertEquals(viewModelB.injectedScope1, viewModelDinB.injectedScope1); + Assertions.assertEquals(viewModelB.injectedScope1, viewModelCinB.injectedScope1); + Assertions.assertEquals(viewModelB.injectedScope1, viewModelDinB.injectedScope1); } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example2/Example2ScopesTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example2/Example2ScopesTest.java index 7f62ee51c..0ddaa17ce 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example2/Example2ScopesTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example2/Example2ScopesTest.java @@ -6,14 +6,14 @@ import de.saxsys.mvvmfx.scopes.example2.views.ScopedViewB; import de.saxsys.mvvmfx.scopes.example2.views.ScopedViewC; import de.saxsys.mvvmfx.scopes.example2.views.ScopedViewModelA; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; -@Ignore +@Disabled public class Example2ScopesTest { @Test diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example3/Example3Test.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example3/Example3Test.java index e1373e799..b63c640a3 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example3/Example3Test.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example3/Example3Test.java @@ -4,10 +4,10 @@ import de.saxsys.mvvmfx.ViewTuple; import de.saxsys.mvvmfx.scopes.example3.views.MainView; import de.saxsys.mvvmfx.scopes.example3.views.MainViewModel; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; -@Ignore("Ignore until fixed") +@Disabled("Ignore until fixed") public class Example3Test { @Test diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example4/views/Example4Test.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example4/views/Example4Test.java index 3c2fdf750..9d3ceb976 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example4/views/Example4Test.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example4/views/Example4Test.java @@ -4,22 +4,16 @@ import de.saxsys.mvvmfx.FluentViewLoader; import de.saxsys.mvvmfx.ViewTuple; import de.saxsys.mvvmfx.testingutils.FxTestingUtils; -import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import static org.assertj.core.api.Assertions.assertThat; -@RunWith(JfxRunner.class) +@ExtendWith(JfxToolkitExtension.class) public class Example4Test { - // Rule to get exceptions from the JavaFX Thread into the JUnit thread - @Rule - public CatchAllExceptionsRule catchAllExceptionsRule = new CatchAllExceptionsRule(); - - @Test public void test() { diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example5/Example5Test.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example5/Example5Test.java index 8d0410153..f816f9e2b 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example5/Example5Test.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/scopes/example5/Example5Test.java @@ -3,12 +3,10 @@ import de.saxsys.mvvmfx.FluentViewLoader; import de.saxsys.mvvmfx.ViewTuple; import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -@RunWith(JfxRunner.class) public class Example5Test { @Test diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CommandsWithoutUiThreadTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CommandsWithoutUiThreadTest.java index bb5755802..ad6b403ff 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CommandsWithoutUiThreadTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CommandsWithoutUiThreadTest.java @@ -4,7 +4,7 @@ import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.function.Supplier; diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CompositeCommandTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CompositeCommandTest.java index 7d04c1d20..9c87285ec 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CompositeCommandTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/CompositeCommandTest.java @@ -23,27 +23,23 @@ import java.util.concurrent.TimeUnit; import com.cedarsoft.test.utils.CatchAllExceptionsRule; -import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import de.saxsys.mvvmfx.testingutils.GCVerifier; +import org.junit.jupiter.api.extension.ExtendWith; -@RunWith(JfxRunner.class) +@ExtendWith(JfxToolkitExtension.class) public class CompositeCommandTest { - // Rule to get exceptions from the JavaFX Thread into the JUnit thread - @Rule - public CatchAllExceptionsRule catchAllExceptionsRule = new CatchAllExceptionsRule(); private BooleanProperty condition1; @@ -53,7 +49,7 @@ public class CompositeCommandTest { private BooleanProperty called2; private DelegateCommand delegateCommand2; - @Before + @BeforeEach public void init() { condition1 = new SimpleBooleanProperty(true); called1 = new SimpleBooleanProperty(); @@ -170,7 +166,7 @@ public void allCommandsAreUnregistered() throws Exception { compositeCommand.unregister(delegateCommand2); } - @Ignore("unstable test. Needs to be fixed. see bug #260") + @Disabled("unstable test. Needs to be fixed. see bug #260") @Test public void longRunningAsyncComposite() throws Exception { diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/DelegateCommandTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/DelegateCommandTest.java index 63db62463..9a67cf140 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/DelegateCommandTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/commands/DelegateCommandTest.java @@ -27,9 +27,9 @@ import java.util.function.BiConsumer; import java.util.function.Supplier; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import com.cedarsoft.test.utils.CatchAllExceptionsRule; @@ -41,15 +41,13 @@ import javafx.beans.value.ChangeListener; import javafx.concurrent.Service; import javafx.concurrent.Task; +import org.junit.jupiter.api.extension.ExtendWith; -@RunWith(JfxRunner.class) +@ExtendWith(JfxToolkitExtension.class) public class DelegateCommandTest { - - // Rule to get exceptions from the JavaFX Thread into the JUnit thread - @Rule - public CatchAllExceptionsRule catchAllExceptionsRule = new CatchAllExceptionsRule(); + @Test public void executable() { @@ -154,7 +152,7 @@ protected void action() { .hasMessage(exceptionReason); } - @Test(expected = RuntimeException.class) + @Test public void commandNotExecutable() { BooleanProperty condition = new SimpleBooleanProperty(false); @@ -163,8 +161,10 @@ public void commandNotExecutable() { protected void action() { } }, condition); - - delegateCommand.execute(); + + Assertions.assertThrows(RuntimeException.class, () -> { + delegateCommand.execute(); + }); } @Test diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/ItemListTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/ItemListTest.java index e1337c2bf..3e07ba127 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/ItemListTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/ItemListTest.java @@ -19,9 +19,9 @@ import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.Collection; import java.util.List; @@ -52,7 +52,7 @@ public class ItemListTest { /** * Prepares the test. */ - @Before + @BeforeEach public void init() { // Create the items in the model @@ -77,7 +77,7 @@ public String apply(Person object) { */ @Test public void mapFromModelToString() { - Assert.assertEquals(PREFIX + PERSON3_NAME, itemList + Assertions.assertEquals(PREFIX + PERSON3_NAME, itemList .stringListProperty().get(2)); } @@ -86,11 +86,11 @@ public void mapFromModelToString() { */ @Test public void addItemToItemList() { - Assert.assertEquals(3, itemList.stringListProperty().size()); - Assert.assertEquals(3, listWithModelObjects.size()); + Assertions.assertEquals(3, itemList.stringListProperty().size()); + Assertions.assertEquals(3, listWithModelObjects.size()); listWithModelObjects.add(new Person("addedPerson")); - Assert.assertEquals(4, itemList.stringListProperty().size()); - Assert.assertEquals(4, listWithModelObjects.size()); + Assertions.assertEquals(4, itemList.stringListProperty().size()); + Assertions.assertEquals(4, listWithModelObjects.size()); } /** @@ -98,56 +98,56 @@ public void addItemToItemList() { */ @Test public void removeItemFromItemList() { - Assert.assertEquals(3, itemList.stringListProperty().size()); - Assert.assertEquals(3, listWithModelObjects.size()); + Assertions.assertEquals(3, itemList.stringListProperty().size()); + Assertions.assertEquals(3, listWithModelObjects.size()); listWithModelObjects.remove(0); - Assert.assertEquals(2, itemList.stringListProperty().size()); - Assert.assertEquals(2, listWithModelObjects.size()); + Assertions.assertEquals(2, itemList.stringListProperty().size()); + Assertions.assertEquals(2, listWithModelObjects.size()); } @Test public void removeMultipleItemsFromItemList() { listWithModelObjects.removeAll(person1, person2); - Assert.assertEquals(1, listWithModelObjects.size()); - Assert.assertEquals(1, itemList.stringListProperty().size()); - Assert.assertEquals(person3, listWithModelObjects.get(0)); - Assert.assertEquals(PREFIX + PERSON3_NAME, itemList + Assertions.assertEquals(1, listWithModelObjects.size()); + Assertions.assertEquals(1, itemList.stringListProperty().size()); + Assertions.assertEquals(person3, listWithModelObjects.get(0)); + Assertions.assertEquals(PREFIX + PERSON3_NAME, itemList .stringListProperty().get(0)); } @Test public void removeAllItemsFromItemList() { listWithModelObjects.clear(); - Assert.assertEquals(0, listWithModelObjects.size()); - Assert.assertEquals(0, itemList.stringListProperty().size()); + Assertions.assertEquals(0, listWithModelObjects.size()); + Assertions.assertEquals(0, itemList.stringListProperty().size()); } @Test public void addItemToItemListAtIndex() { listWithModelObjects.add(1, new Person("addedPerson")); - Assert.assertEquals(4, itemList.stringListProperty().size()); - Assert.assertEquals(4, listWithModelObjects.size()); - Assert.assertEquals(PREFIX + "addedPerson", itemList + Assertions.assertEquals(4, itemList.stringListProperty().size()); + Assertions.assertEquals(4, listWithModelObjects.size()); + Assertions.assertEquals(PREFIX + "addedPerson", itemList .stringListProperty().get(1)); } @Test public void addMultipleItemsToItemList() { listWithModelObjects.addAll(new Person("added1"), new Person("added2")); - Assert.assertEquals(5, listWithModelObjects.size()); - Assert.assertEquals(5, itemList.stringListProperty().size()); - Assert.assertEquals(PREFIX + "added1", itemList.stringListProperty() + Assertions.assertEquals(5, listWithModelObjects.size()); + Assertions.assertEquals(5, itemList.stringListProperty().size()); + Assertions.assertEquals(PREFIX + "added1", itemList.stringListProperty() .get(3)); - Assert.assertEquals(PREFIX + "added2", itemList.stringListProperty() + Assertions.assertEquals(PREFIX + "added2", itemList.stringListProperty() .get(4)); } @Test public void replaceItemInItemListAtIndex() { listWithModelObjects.set(1, new Person("replacedPerson")); - Assert.assertEquals(3, listWithModelObjects.size()); - Assert.assertEquals(3, itemList.stringListProperty().size()); - Assert.assertEquals(PREFIX + "replacedPerson", itemList + Assertions.assertEquals(3, listWithModelObjects.size()); + Assertions.assertEquals(3, itemList.stringListProperty().size()); + Assertions.assertEquals(PREFIX + "replacedPerson", itemList .stringListProperty().get(1)); } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/ListTransformationTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/ListTransformationTest.java index ebc1cc039..98781d89f 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/ListTransformationTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/ListTransformationTest.java @@ -1,6 +1,6 @@ package de.saxsys.mvvmfx.utils.itemlist; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/SelectableItemListTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/SelectableItemListTest.java index ad2b04f2d..17df22b92 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/SelectableItemListTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/itemlist/SelectableItemListTest.java @@ -19,9 +19,9 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; /** * Tests for {@link SelectableItemList}. @@ -41,7 +41,7 @@ public class SelectableItemListTest { /** * Prepares the test. */ - @Before + @BeforeEach public void init() { // Create the items in the model @@ -68,8 +68,8 @@ public String apply(Person object) { */ @Test public void checkStartState() { - Assert.assertEquals(-1, selectableItemList.getSelectedIndex()); - Assert.assertEquals(null, selectableItemList.getSelectedItem()); + Assertions.assertEquals(-1, selectableItemList.getSelectedIndex()); + Assertions.assertEquals(null, selectableItemList.getSelectedItem()); } /** @@ -79,7 +79,7 @@ public void checkStartState() { @Test public void setSelectedItemByIndex() { selectableItemList.select(1); - Assert.assertEquals(listWithModelObjects.get(1), selectableItemList + Assertions.assertEquals(listWithModelObjects.get(1), selectableItemList .selectedItemProperty().get()); } @@ -90,7 +90,7 @@ public void setSelectedItemByIndex() { @Test public void setSelectedIndexByItem() { selectableItemList.select(person3); - Assert.assertEquals(2, selectableItemList.getSelectedIndex()); + Assertions.assertEquals(2, selectableItemList.getSelectedIndex()); } /** @@ -99,14 +99,14 @@ public void setSelectedIndexByItem() { @Test public void setSelectedIndexWithInvalidItem() { selectableItemList.select(person1); - Assert.assertEquals(0, selectableItemList.getSelectedIndex()); - Assert.assertEquals(person1, selectableItemList.getSelectedItem()); + Assertions.assertEquals(0, selectableItemList.getSelectedIndex()); + Assertions.assertEquals(person1, selectableItemList.getSelectedItem()); selectableItemList.select(new Person("Roflcopter")); - Assert.assertEquals(0, selectableItemList.getSelectedIndex()); - Assert.assertEquals(person1, selectableItemList.getSelectedItem()); + Assertions.assertEquals(0, selectableItemList.getSelectedIndex()); + Assertions.assertEquals(person1, selectableItemList.getSelectedItem()); selectableItemList.select(person2); - Assert.assertEquals(1, selectableItemList.getSelectedIndex()); - Assert.assertEquals(person2, selectableItemList.getSelectedItem()); + Assertions.assertEquals(1, selectableItemList.getSelectedIndex()); + Assertions.assertEquals(person2, selectableItemList.getSelectedItem()); } /** @@ -115,44 +115,44 @@ public void setSelectedIndexWithInvalidItem() { @Test public void setSelectedItemWithInvalidIndex() { selectableItemList.select(person1); - Assert.assertEquals(0, selectableItemList.getSelectedIndex()); - Assert.assertEquals(person1, selectableItemList.getSelectedItem()); + Assertions.assertEquals(0, selectableItemList.getSelectedIndex()); + Assertions.assertEquals(person1, selectableItemList.getSelectedItem()); selectableItemList.select(100); - Assert.assertEquals(0, selectableItemList.getSelectedIndex()); - Assert.assertEquals(person1, selectableItemList.getSelectedItem()); + Assertions.assertEquals(0, selectableItemList.getSelectedIndex()); + Assertions.assertEquals(person1, selectableItemList.getSelectedItem()); } @Test public void unselectBySettingSelectedItemToNull() { selectableItemList.select(person2); - Assert.assertEquals(1, selectableItemList.getSelectedIndex()); - Assert.assertEquals(person2, selectableItemList.selectedItemProperty() + Assertions.assertEquals(1, selectableItemList.getSelectedIndex()); + Assertions.assertEquals(person2, selectableItemList.selectedItemProperty() .get()); selectableItemList.select(null); - Assert.assertEquals(-1, selectableItemList.getSelectedIndex()); - Assert.assertNull(selectableItemList.selectedItemProperty().get()); + Assertions.assertEquals(-1, selectableItemList.getSelectedIndex()); + Assertions.assertNull(selectableItemList.selectedItemProperty().get()); } @Test public void unselectBySettingSelectedIndexToMinus1() { selectableItemList.select(person2); - Assert.assertEquals(1, selectableItemList.getSelectedIndex()); - Assert.assertEquals(person2, selectableItemList.selectedItemProperty() + Assertions.assertEquals(1, selectableItemList.getSelectedIndex()); + Assertions.assertEquals(person2, selectableItemList.selectedItemProperty() .get()); selectableItemList.select(-1); - Assert.assertEquals(-1, selectableItemList.getSelectedIndex()); - Assert.assertNull(selectableItemList.selectedItemProperty().get()); + Assertions.assertEquals(-1, selectableItemList.getSelectedIndex()); + Assertions.assertNull(selectableItemList.selectedItemProperty().get()); } @Test public void unselectByClearSelection() { selectableItemList.select(person2); - Assert.assertEquals(1, selectableItemList.getSelectedIndex()); - Assert.assertEquals(person2, selectableItemList.selectedItemProperty() + Assertions.assertEquals(1, selectableItemList.getSelectedIndex()); + Assertions.assertEquals(person2, selectableItemList.selectedItemProperty() .get()); selectableItemList.clearSelection(); - Assert.assertEquals(-1, selectableItemList.getSelectedIndex()); - Assert.assertNull(selectableItemList.selectedItemProperty().get()); + Assertions.assertEquals(-1, selectableItemList.getSelectedIndex()); + Assertions.assertNull(selectableItemList.selectedItemProperty().get()); } } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/FieldMethodOverloadingTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/FieldMethodOverloadingTest.java index 06db12d7e..c630a9c22 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/FieldMethodOverloadingTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/FieldMethodOverloadingTest.java @@ -24,8 +24,8 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.Property; import javafx.beans.property.StringProperty; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.Collections; @@ -59,7 +59,7 @@ public class FieldMethodOverloadingTest { private ModelWrapper wrapper; - @Before + @BeforeEach public void setup() { wrapper = new ModelWrapper<>(); } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapperTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapperTest.java index 45a6d674b..c5c79ead8 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapperTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/ModelWrapperTest.java @@ -20,7 +20,7 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.Collections; @@ -119,7 +119,7 @@ public void testWithGetterAndSetter() { assertThat(nameProperty.getValue()).isEqualTo(null); assertThat(ageProperty.getValue()).isEqualTo(0); - assertThat(nicknamesProperty.getValue().size()).isEqualTo(0); + assertThat(nicknamesProperty.getValue()).isEmpty(); // the wrapped object has still the values from the last commit. assertThat(person.getName()).isEqualTo("hugo"); @@ -254,6 +254,76 @@ public void testWithJavaFXPropertiesField() { assertThat(otherPerson.getAge()).isEqualTo(24); } + + @Test + public void testWithImmutables() { + + PersonImmutable person1 = PersonImmutable.create() + .withName("horst") + .withAge(32) + .withNicknames(Collections.singletonList("captain")); + + ModelWrapper personWrapper = new ModelWrapper<>(person1); + + final StringProperty nameProperty = personWrapper + .immutableField(PersonImmutable::getName, PersonImmutable::withName); + final IntegerProperty ageProperty = personWrapper.immutableField(PersonImmutable::getAge, + PersonImmutable::withAge); + final ListProperty nicknamesProperty = personWrapper.immutableField(PersonImmutable::getNicknames, PersonImmutable::withNicknames); + + assertThat(nameProperty.getValue()).isEqualTo("horst"); + assertThat(ageProperty.getValue()).isEqualTo(32); + assertThat(nicknamesProperty.getValue()).containsOnly("captain"); + + nameProperty.setValue("hugo"); + ageProperty.setValue(33); + nicknamesProperty.add("player"); + + personWrapper.commit(); + + // old person has still the same old values. + assertThat(person1.getName()).isEqualTo("horst"); + assertThat(person1.getAge()).isEqualTo(32); + assertThat(person1.getNicknames()).containsOnly("captain"); + + + PersonImmutable person2 = personWrapper.get(); + + assertThat(person2).isNotEqualTo(person1); + assertThat(person2.getName()).isEqualTo("hugo"); + assertThat(person2.getAge()).isEqualTo(33); + assertThat(person2.getNicknames()).containsOnly("captain", "player"); + + nameProperty.setValue("luise"); + ageProperty.setValue(33); + nicknamesProperty.setValue(FXCollections.observableArrayList("student")); + + personWrapper.reset(); + + assertThat(nameProperty.getValue()).isEqualTo(null); + assertThat(ageProperty.getValue()).isEqualTo(0); + assertThat(nicknamesProperty.getValue()).isEmpty(); + + personWrapper.reload(); + // now the properties have the values from the wrapped object + assertThat(nameProperty.getValue()).isEqualTo("hugo"); + assertThat(ageProperty.getValue()).isEqualTo(33); + assertThat(nicknamesProperty.get()).containsOnly("captain", "player"); + + + PersonImmutable person3 = person1.withName("gisela") + .withAge(23) + .withNicknames(Collections.singletonList("referee")); + + personWrapper.set(person3); + personWrapper.reload(); + + assertThat(nameProperty.getValue()).isEqualTo("gisela"); + assertThat(ageProperty.getValue()).isEqualTo(23); + assertThat(nicknamesProperty.getValue()).containsOnly("referee"); + } + + @Test public void testIdentifiedFields() { Person person = new Person(); @@ -287,6 +357,36 @@ public void testIdentifiedFields() { } + @Test + public void testIdentifiedFieldsWithImmutables() { + + PersonImmutable person1 = PersonImmutable.create() + .withName("horst") + .withAge(32) + .withNicknames(Collections.singletonList("captain")); + + ModelWrapper personWrapper = new ModelWrapper<>(person1); + + final StringProperty nameProperty = personWrapper + .immutableField("name", PersonImmutable::getName, PersonImmutable::withName); + final IntegerProperty ageProperty = personWrapper.immutableField("age", PersonImmutable::getAge, PersonImmutable::withAge); + final ListProperty nicknamesProperty = personWrapper.immutableField("nicknames", PersonImmutable::getNicknames, PersonImmutable::withNicknames); + + + final StringProperty nameProperty2 = personWrapper + .immutableField("name", PersonImmutable::getName, PersonImmutable::withName); + final IntegerProperty ageProperty2 = personWrapper.immutableField("age", PersonImmutable::getAge, PersonImmutable::withAge); + final ListProperty nicknamesProperty2 = personWrapper.immutableField("nicknames", PersonImmutable::getNicknames, PersonImmutable::withNicknames); + + assertThat(nameProperty).isSameAs(nameProperty2); + assertThat(ageProperty).isSameAs(ageProperty2); + assertThat(nicknamesProperty).isSameAs(nicknamesProperty2); + + assertThat(nameProperty.getName()).isEqualTo("name"); + assertThat(ageProperty.getName()).isEqualTo("age"); + assertThat(nicknamesProperty.getName()).isEqualTo("nicknames"); + } + @Test public void testDirtyFlag() { @@ -410,6 +510,70 @@ public void testDirtyFlagWithFxProperties() { assertThat(personWrapper.isDirty()).isFalse(); } + @Test + public void testDirtyFlagWithImmutables() { + + PersonImmutable person = PersonImmutable.create() + .withName("horst") + .withAge(32) + .withNicknames(Collections.singletonList("captain")); + + ModelWrapper personWrapper = new ModelWrapper<>(person); + + assertThat(personWrapper.isDirty()).isFalse(); + + final StringProperty name = personWrapper + .immutableField(PersonImmutable::getName, PersonImmutable::withName); + final IntegerProperty age = personWrapper.immutableField(PersonImmutable::getAge, + PersonImmutable::withAge); + final ListProperty nicknames = personWrapper.immutableField(PersonImmutable::getNicknames, PersonImmutable::withNicknames); + + name.set("hugo"); + + assertThat(personWrapper.isDirty()).isTrue(); + + personWrapper.commit(); + assertThat(personWrapper.isDirty()).isFalse(); + + age.set(33); + assertThat(personWrapper.isDirty()).isTrue(); + + age.set(32); + assertThat(personWrapper.isDirty()).isTrue(); // dirty is still true + + personWrapper.reload(); + assertThat(personWrapper.isDirty()).isFalse(); + + + nicknames.add("player"); + assertThat(personWrapper.isDirty()).isTrue(); + + nicknames.remove("player"); + assertThat(personWrapper.isDirty()).isTrue(); // dirty is still true + + personWrapper.commit(); + assertThat(personWrapper.isDirty()).isFalse(); + + name.set("hans"); + assertThat(personWrapper.isDirty()).isTrue(); + + personWrapper.reset(); + assertThat(personWrapper.isDirty()).isTrue(); + + + personWrapper.reload(); + assertThat(personWrapper.isDirty()).isFalse(); + + nicknames.set(FXCollections.observableArrayList("player")); + assertThat(personWrapper.isDirty()).isTrue(); + + personWrapper.reset(); + assertThat(personWrapper.isDirty()).isTrue(); + + personWrapper.reload(); + assertThat(personWrapper.isDirty()).isFalse(); + } + @Test public void testDifferentFlag() { Person person = new Person(); @@ -567,6 +731,64 @@ public void testDifferentFlagWithFxProperties() { assertThat(personWrapper.isDifferent()).isTrue(); } + @Test + public void testDifferentFlagWithImmutables() { + PersonImmutable person = PersonImmutable.create() + .withName("horst") + .withAge(32) + .withNicknames(Collections.singletonList("captain")); + + ModelWrapper personWrapper = new ModelWrapper<>(person); + + assertThat(personWrapper.isDirty()).isFalse(); + + final StringProperty name = personWrapper + .immutableField(PersonImmutable::getName, PersonImmutable::withName); + final IntegerProperty age = personWrapper.immutableField(PersonImmutable::getAge, + PersonImmutable::withAge); + final ListProperty nicknames = personWrapper.immutableField(PersonImmutable::getNicknames, PersonImmutable::withNicknames); + + + + name.set("hugo"); + assertThat(personWrapper.isDifferent()).isTrue(); + + personWrapper.commit(); + assertThat(personWrapper.isDifferent()).isFalse(); + + + age.set(33); + assertThat(personWrapper.isDifferent()).isTrue(); + + age.set(32); + assertThat(personWrapper.isDifferent()).isFalse(); + + + nicknames.remove("captain"); + assertThat(personWrapper.isDifferent()).isTrue(); + + nicknames.add("captain"); + assertThat(personWrapper.isDifferent()).isFalse(); + + nicknames.add("player"); + assertThat(personWrapper.isDifferent()).isTrue(); + + nicknames.remove("player"); + assertThat(personWrapper.isDifferent()).isFalse(); + + nicknames.setValue(FXCollections.observableArrayList("spectator")); + assertThat(personWrapper.isDifferent()).isTrue(); + + personWrapper.reload(); + assertThat(personWrapper.isDifferent()).isFalse(); + + nicknames.add("captain"); // duplicate captain + assertThat(personWrapper.isDifferent()).isTrue(); + + nicknames.add("player"); + assertThat(personWrapper.isDifferent()).isTrue(); + } + @Test public void defaultValuesCanBeUpdatedToCurrentValues(){ final Person person = new Person(); @@ -574,67 +796,142 @@ public void defaultValuesCanBeUpdatedToCurrentValues(){ person.setAge(32); person.setNicknames(Arrays.asList("captain")); - final ModelWrapper cut = new ModelWrapper<>(person); + final ModelWrapper personWrapper = new ModelWrapper<>(person); - final StringProperty nameField = cut.field(Person::getName, Person::setName, person.getName()); - nameField.set("test"); - cut.commit(); - cut.useCurrentValuesAsDefaults(); - cut.reset(); + final StringProperty name = personWrapper.field(Person::getName, Person::setName, person.getName()); + name.set("test"); + personWrapper.commit(); + personWrapper.useCurrentValuesAsDefaults(); + personWrapper.reset(); assertThat(person.getName()).isEqualTo("test"); - assertThat(nameField.get()).isEqualTo("test"); + assertThat(name.get()).isEqualTo("test"); - final IntegerProperty ageField = cut.field(Person::getAge, Person::setAge, person.getAge()); - ageField.set(42); - cut.commit(); - cut.useCurrentValuesAsDefaults(); - cut.reset(); + final IntegerProperty age = personWrapper.field(Person::getAge, Person::setAge, person.getAge()); + age.set(42); + personWrapper.commit(); + personWrapper.useCurrentValuesAsDefaults(); + personWrapper.reset(); assertThat(person.getAge()).isEqualTo(42); - assertThat(ageField.get()).isEqualTo(42); + assertThat(age.get()).isEqualTo(42); - final ListProperty nicknames = cut.field(Person::getNicknames, Person::setNicknames, person.getNicknames()); + final ListProperty nicknames = personWrapper.field(Person::getNicknames, Person::setNicknames, person.getNicknames()); nicknames.add("myname"); nicknames.remove("captain"); - cut.commit(); - cut.useCurrentValuesAsDefaults(); - cut.reset(); + personWrapper.commit(); + personWrapper.useCurrentValuesAsDefaults(); + personWrapper.reset(); assertThat(person.getNicknames()).containsExactly("myname"); assertThat(nicknames.get()).containsExactly("myname"); } - @Test - public void valuesShouldBeUpdatedWhenModelInstanceChanges() { - final Person person1 = new Person(); - person1.setName("horst"); - person1.setAge(32); - person1.setNicknames(Arrays.asList("captain")); - final Person person2 = new Person(); - person2.setName("dieter"); - person2.setAge(42); - person2.setNicknames(Arrays.asList("robin")); - - final SimpleObjectProperty modelProp = new SimpleObjectProperty<>(person1); - - final ModelWrapper cut = new ModelWrapper<>(modelProp); - - final StringProperty nameField = cut.field(Person::getName, Person::setName, person1.getName()); - final IntegerProperty ageField = cut.field(Person::getAge, Person::setAge, person1.getAge()); - final ListProperty nicknames = cut.field(Person::getNicknames, Person::setNicknames, person1.getNicknames()); - - assertThat(nameField.get()).isEqualTo(person1.getName()); - assertThat(ageField.get()).isEqualTo(person1.getAge()); - assertThat(nicknames.get()).containsExactlyElementsOf(person1.getNicknames()); - - modelProp.set(person2); - assertThat(nameField.get()).isEqualTo(person2.getName()); - assertThat(ageField.get()).isEqualTo(person2.getAge()); - assertThat(nicknames.get()).containsExactlyElementsOf(person2.getNicknames()); - - cut.reset(); - assertThat(nameField.get()).isEqualTo(person1.getName()); - assertThat(ageField.get()).isEqualTo(person1.getAge()); - assertThat(nicknames.get()).containsExactlyElementsOf(person1.getNicknames()); - } + @Test + public void defaultValuesCanBeUpdatedToCurrentValuesWithImmutables(){ + PersonImmutable person = PersonImmutable.create() + .withName("Luise") + .withAge(32) + .withNicknames(Collections.singletonList("captain")); + + ModelWrapper personWrapper = new ModelWrapper<>(person); + + assertThat(personWrapper.isDirty()).isFalse(); + + final StringProperty name = personWrapper + .immutableField(PersonImmutable::getName, PersonImmutable::withName); + final IntegerProperty age = personWrapper.immutableField(PersonImmutable::getAge, + PersonImmutable::withAge); + final ListProperty nicknames = personWrapper.immutableField(PersonImmutable::getNicknames, PersonImmutable::withNicknames); + + name.set("test"); + personWrapper.commit(); + personWrapper.useCurrentValuesAsDefaults(); + personWrapper.reset(); + assertThat(name.get()).isEqualTo("test"); + + age.set(42); + personWrapper.commit(); + personWrapper.useCurrentValuesAsDefaults(); + personWrapper.reset(); + assertThat(age.get()).isEqualTo(42); + + nicknames.add("myname"); + nicknames.remove("captain"); + personWrapper.commit(); + personWrapper.useCurrentValuesAsDefaults(); + personWrapper.reset(); + assertThat(nicknames.get()).containsExactly("myname"); + } + + @Test + public void valuesShouldBeUpdatedWhenModelInstanceChanges() { + final Person person1 = new Person(); + person1.setName("horst"); + person1.setAge(32); + person1.setNicknames(Arrays.asList("captain")); + final Person person2 = new Person(); + person2.setName("dieter"); + person2.setAge(42); + person2.setNicknames(Arrays.asList("robin")); + + final SimpleObjectProperty modelProp = new SimpleObjectProperty<>(person1); + + final ModelWrapper personWrapper = new ModelWrapper<>(modelProp); + + final StringProperty nameField = personWrapper.field(Person::getName, Person::setName, person1.getName()); + final IntegerProperty ageField = personWrapper.field(Person::getAge, Person::setAge, person1.getAge()); + final ListProperty nicknames = personWrapper + .field(Person::getNicknames, Person::setNicknames, person1.getNicknames()); + + assertThat(nameField.get()).isEqualTo(person1.getName()); + assertThat(ageField.get()).isEqualTo(person1.getAge()); + assertThat(nicknames.get()).containsExactlyElementsOf(person1.getNicknames()); + + modelProp.set(person2); + assertThat(nameField.get()).isEqualTo(person2.getName()); + assertThat(ageField.get()).isEqualTo(person2.getAge()); + assertThat(nicknames.get()).containsExactlyElementsOf(person2.getNicknames()); + + personWrapper.reset(); + assertThat(nameField.get()).isEqualTo(person1.getName()); + assertThat(ageField.get()).isEqualTo(person1.getAge()); + assertThat(nicknames.get()).containsExactlyElementsOf(person1.getNicknames()); + } + + @Test + public void valuesShouldBeUpdatedWhenModelInstanceChangesWithImmutables() { + PersonImmutable person1 = PersonImmutable.create() + .withName("horst") + .withAge(32) + .withNicknames(Collections.singletonList("captain")); + + PersonImmutable person2 = PersonImmutable.create() + .withName("dieter") + .withAge(42) + .withNicknames(Collections.singletonList("robin")); + + final SimpleObjectProperty modelProp = new SimpleObjectProperty<>(person1); + + ModelWrapper personWrapper = new ModelWrapper<>(modelProp); + + final StringProperty nameField = personWrapper + .immutableField(PersonImmutable::getName, PersonImmutable::withName, person1.getName()); + final IntegerProperty ageField = personWrapper.immutableField(PersonImmutable::getAge, + PersonImmutable::withAge, person1.getAge()); + final ListProperty nicknames = personWrapper.immutableField(PersonImmutable::getNicknames, PersonImmutable::withNicknames, person1.getNicknames()); + + assertThat(nameField.get()).isEqualTo(person1.getName()); + assertThat(ageField.get()).isEqualTo(person1.getAge()); + assertThat(nicknames.get()).containsExactlyElementsOf(person1.getNicknames()); + + modelProp.set(person2); + assertThat(nameField.get()).isEqualTo(person2.getName()); + assertThat(ageField.get()).isEqualTo(person2.getAge()); + assertThat(nicknames.get()).containsExactlyElementsOf(person2.getNicknames()); + + personWrapper.reset(); + assertThat(nameField.get()).isEqualTo(person1.getName()); + assertThat(ageField.get()).isEqualTo(person1.getAge()); + assertThat(nicknames.get()).containsExactlyElementsOf(person1.getNicknames()); + } @Test public void testUseCurrentValuesAsDefaultWhenModelIsNull() { diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/PersonImmutable.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/PersonImmutable.java new file mode 100644 index 000000000..12aeee92d --- /dev/null +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/mapping/PersonImmutable.java @@ -0,0 +1,70 @@ +package de.saxsys.mvvmfx.utils.mapping; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class PersonImmutable { + private final String name; + private final int age; + private final List nicknames = new ArrayList<>(); + + public static PersonImmutable create(){ + return new PersonImmutable("", 0, Collections.emptyList()); + } + + private PersonImmutable(String name, int age, List nicknames) { + this.name = name; + this.age = age; + this.nicknames.addAll(nicknames); + } + + public String getName() { + return name; + } + + public PersonImmutable withName(String name) { + return new PersonImmutable(name, this.age, this.nicknames); + } + + public int getAge() { + return age; + } + + public PersonImmutable withAge(int age) { + return new PersonImmutable(this.name, age, this.nicknames); + } + + public List getNicknames() { + return Collections.unmodifiableList(nicknames); + } + + public PersonImmutable withNicknames(List nicknames) { + return new PersonImmutable(this.name, this.age, nicknames); + } + + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + PersonImmutable that = (PersonImmutable) o; + + if (age != that.age) + return false; + if (name != null ? !name.equals(that.name) : that.name != null) + return false; + return nicknames.equals(that.nicknames); + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + age; + result = 31 * result + nicknames.hashCode(); + return result; + } +} diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ConcurrentModificationBugTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ConcurrentModificationBugTest.java index 63ef70a7d..0448756c7 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ConcurrentModificationBugTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ConcurrentModificationBugTest.java @@ -15,7 +15,7 @@ ******************************************************************************/ package de.saxsys.mvvmfx.utils.notifications; -import org.junit.Test; +import org.junit.jupiter.api.Test; /** diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenterTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenterTest.java index 552f2e0cf..6152d830a 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenterTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/DefaultNotificationCenterTest.java @@ -17,11 +17,12 @@ package de.saxsys.mvvmfx.utils.notifications; import de.saxsys.mvvmfx.ViewModel; -import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; import javafx.application.Platform; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import java.util.concurrent.CompletableFuture; @@ -31,7 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(JfxRunner.class) +@ExtendWith(JfxToolkitExtension.class) public class DefaultNotificationCenterTest { private static final String TEST_NOTIFICATION = "test_notification"; @@ -44,7 +45,7 @@ public class DefaultNotificationCenterTest { NotificationObserver observer2; NotificationObserver observer3; - @Before + @BeforeEach public void init() { observer1 = Mockito.mock(NotificationObserver.class); observer2 = Mockito.mock(NotificationObserver.class); @@ -178,8 +179,10 @@ public void observerForViewModelIsCalledFromUiThread() throws InterruptedExcepti assertThat(wasCalledOnUiThread).isTrue(); } - @Test(expected = IllegalArgumentException.class) + @Test public void subscribeWithNullObserver() { - defaultCenter.subscribe(TEST_NOTIFICATION,null); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + defaultCenter.subscribe(TEST_NOTIFICATION, null); + }); } } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/MemoryLeakGlobalTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/MemoryLeakGlobalTest.java index 415dba909..01b2f993f 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/MemoryLeakGlobalTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/MemoryLeakGlobalTest.java @@ -1,8 +1,8 @@ package de.saxsys.mvvmfx.utils.notifications; import de.saxsys.mvvmfx.testingutils.GCVerifier; -import org.junit.After; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; import java.util.concurrent.atomic.AtomicInteger; @@ -16,7 +16,7 @@ public class MemoryLeakGlobalTest { - @After + @AfterEach public void tearDown() { NotificationCenterFactory.setNotificationCenter(new DefaultNotificationCenter()); } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelperTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelperTest.java index cf532e97e..125c63aae 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelperTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/NotificationTestHelperTest.java @@ -15,12 +15,14 @@ ******************************************************************************/ package de.saxsys.mvvmfx.utils.notifications; -import static org.assertj.core.api.Assertions.*; +import de.saxsys.mvvmfx.ViewModel; +import javafx.util.Pair; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; -import org.junit.Before; -import org.junit.Test; +import java.util.stream.Stream; -import de.saxsys.mvvmfx.ViewModel; +import static org.assertj.core.api.Assertions.assertThat; /** * @author manuel.mauky @@ -32,7 +34,7 @@ public class MyViewModel implements ViewModel { private MyViewModel viewModel; - @Before + @BeforeEach public void setUp() throws Exception { viewModel = new MyViewModel(); } @@ -114,4 +116,45 @@ public void timeout() { assertThat(helper.numberOfReceivedNotifications()).isEqualTo(1); } + + @Test + public void notificationList() { + NotificationTestHelper helper = new NotificationTestHelper(); + viewModel.subscribe("test", helper); + viewModel.subscribe("something", helper); + + viewModel.publish("test", 1, "a", 2, 3, "b"); + + Pair notification1 = helper.getReceivedNotifications().get(0); + + assertThat(notification1.getValue().length).isEqualTo(5); + long integerValueCount = Stream.of(notification1.getValue()) + .filter(v -> v instanceof Integer) + .count(); + + long stringValueCount = Stream.of(notification1.getValue()) + .filter(v -> v instanceof String) + .count(); + + assertThat(integerValueCount).isEqualTo(3); + assertThat(stringValueCount).isEqualTo(2); + assertThat(notification1.getValue()[1]).isEqualTo("a"); + + //second message + viewModel.publish("test", false); + Pair notification2 = helper.getReceivedNotifications().get(1); + assertThat(notification2.getKey()).isEqualTo("test"); + assertThat(notification2.getValue()[0]).isEqualTo(false); + + //getting an empty message + viewModel.publish("something"); + Pair notification3 = helper.getReceivedNotifications().get(2); + assertThat(notification3.getKey()).isEqualTo("something"); + assertThat(notification3.getValue()).isEmpty(); + + //message should not be received + viewModel.publish("some other message"); + assertThat(helper.getReceivedNotifications().size()).isEqualTo(3); + + } } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ResetNotificationCenterTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ResetNotificationCenterTest.java index 235dd9165..53f7a91ee 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ResetNotificationCenterTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/ResetNotificationCenterTest.java @@ -15,9 +15,9 @@ ******************************************************************************/ package de.saxsys.mvvmfx.utils.notifications; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.concurrent.atomic.AtomicBoolean; @@ -40,7 +40,7 @@ public class ResetNotificationCenterTest { private static final String MY_MESSAGE = "myMessage"; private AtomicBoolean wasCalled = new AtomicBoolean(false); - @Before + @BeforeEach public void setup() { NotificationCenter notificationCenter = NotificationCenterFactory.getNotificationCenter(); @@ -59,7 +59,7 @@ public void setup() { * one of the test cases would fail. * By replacing the instance we assure that each test uses a fresh notification center. */ - @After + @AfterEach public void tearDown() { NotificationCenterFactory.setNotificationCenter(new DefaultNotificationCenter()); diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/MemoryLeakOnViewModelTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/MemoryLeakOnViewModelTest.java index 8af5ccaf7..83d216005 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/MemoryLeakOnViewModelTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/MemoryLeakOnViewModelTest.java @@ -1,12 +1,13 @@ package de.saxsys.mvvmfx.utils.notifications.viewmodel; import de.saxsys.mvvmfx.ViewModel; +import de.saxsys.mvvmfx.testingutils.FxTestingUtils; import de.saxsys.mvvmfx.testingutils.GCVerifier; -import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; import de.saxsys.mvvmfx.utils.notifications.NotificationObserver; import javafx.application.Platform; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -21,7 +22,7 @@ * This test verifies that the communication between View and ViewModel * via notifications doesn't introduce memory leaks. */ -@RunWith(JfxRunner.class) +@ExtendWith(JfxToolkitExtension.class) public class MemoryLeakOnViewModelTest { @@ -43,7 +44,7 @@ public void testViewModelWithViewCommunication() { viewModel.actionThatPublishes(); - waitForUiThread(); + FxTestingUtils.waitForUiThread(); assertThat(view.counter.get()).isEqualTo(1); @@ -79,7 +80,7 @@ public void testViewModelCommunication() { vm.publish("test"); - waitForUiThread(); + FxTestingUtils.waitForUiThread(); assertThat(counter.get()).isEqualTo(1); @@ -115,7 +116,7 @@ public void testStaticObserverDoesntCreateMemoryLeak() { vm.publish("test"); - waitForUiThread(); + FxTestingUtils.waitForUiThread(); assertThat(StaticObserver.counter.get()).isEqualTo(1); @@ -139,19 +140,4 @@ public void receivedNotification(String key, Object... payload) { } } - - /** - * This method is used to wait until the UI thread has done all work that was queued via - * {@link Platform#runLater(Runnable)}. - */ - private void waitForUiThread() { - CompletableFuture future = new CompletableFuture<>(); - Platform.runLater(() -> future.complete(null)); - try { - future.get(1l, TimeUnit.SECONDS); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IllegalStateException(e); - } - } - } diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/ViewModelTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/ViewModelTest.java index b425c421b..a1ca91321 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/ViewModelTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/ViewModelTest.java @@ -17,15 +17,16 @@ import de.saxsys.mvvmfx.MvvmFX; import de.saxsys.mvvmfx.ViewModel; -import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; +import de.saxsys.mvvmfx.testingutils.FxTestingUtils; +import de.saxsys.mvvmfx.testingutils.JfxToolkitExtension; import de.saxsys.mvvmfx.utils.notifications.DefaultNotificationCenter; import de.saxsys.mvvmfx.utils.notifications.DefaultNotificationCenterTest; import de.saxsys.mvvmfx.utils.notifications.NotificationCenterFactory; import de.saxsys.mvvmfx.utils.notifications.NotificationObserver; import javafx.application.Platform; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import java.util.concurrent.CompletableFuture; @@ -36,7 +37,7 @@ /** * This test verifies the communication via notifications between the View and ViewModel. */ -@RunWith(JfxRunner.class) +@ExtendWith(JfxToolkitExtension.class) public class ViewModelTest { private static final String TEST_NOTIFICATION = "test_notification"; @@ -47,7 +48,7 @@ public class ViewModelTest { DummyNotificationObserver observer2; DummyNotificationObserver observer3; - @Before + @BeforeEach public void init() { observer1 = Mockito.mock(DummyNotificationObserver.class); observer2 = Mockito.mock(DummyNotificationObserver.class); @@ -63,7 +64,7 @@ public void observerFromOutsideDoesNotReceiveNotifications() { MvvmFX.getNotificationCenter().subscribe(TEST_NOTIFICATION, observer1); viewModel.publish(TEST_NOTIFICATION); - waitForUiThread(); + FxTestingUtils.waitForUiThread(); Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION); } @@ -72,7 +73,7 @@ public void addObserverAndPublish() throws Exception { viewModel.subscribe(TEST_NOTIFICATION, observer1); viewModel.publish(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); - waitForUiThread(); + FxTestingUtils.waitForUiThread(); Mockito.verify(observer1).receivedNotification(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); } @@ -82,14 +83,14 @@ public void addAndRemoveObserverAndPublish() throws Exception { viewModel.unsubscribe(observer1); viewModel.publish(TEST_NOTIFICATION); - waitForUiThread(); + FxTestingUtils.waitForUiThread(); Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION); viewModel.subscribe(TEST_NOTIFICATION, observer1); viewModel.unsubscribe(TEST_NOTIFICATION, observer1); viewModel.publish(TEST_NOTIFICATION); - waitForUiThread(); + FxTestingUtils.waitForUiThread(); Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION); } @@ -100,7 +101,7 @@ public void addMultipleObserverAndPublish() throws Exception { viewModel.subscribe(TEST_NOTIFICATION, observer3); viewModel.publish(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); - waitForUiThread(); + FxTestingUtils.waitForUiThread(); Mockito.verify(observer1).receivedNotification(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); Mockito.verify(observer2).receivedNotification(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); @@ -116,7 +117,7 @@ public void addMultipleObserverAndRemoveOneAndPublish() throws Exception { viewModel.unsubscribe(observer1); viewModel.publish(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); - waitForUiThread(); + FxTestingUtils.waitForUiThread(); Mockito.verify(observer1, Mockito.never()).receivedNotification(TEST_NOTIFICATION, OBJECT_ARRAY_FOR_NOTIFICATION); @@ -136,20 +137,7 @@ public void removeObserverThatWasNotRegisteredYet() { viewModel.unsubscribe(TEST_NOTIFICATION, observer1); } - - /** - * This method is used to wait until the UI thread has done all work that was queued via - * {@link Platform#runLater(Runnable)}. - */ - private void waitForUiThread() { - CompletableFuture future = new CompletableFuture<>(); - Platform.runLater(() -> future.complete(null)); - try { - future.get(1l, TimeUnit.SECONDS); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IllegalStateException(e); - } - } + private class DummyNotificationObserver implements NotificationObserver { @Override diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/ViewModelWithoutUiThreadTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/ViewModelWithoutUiThreadTest.java index 52385d1e9..b5e751fa9 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/ViewModelWithoutUiThreadTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/ViewModelWithoutUiThreadTest.java @@ -16,7 +16,7 @@ package de.saxsys.mvvmfx.utils.notifications.viewmodel; import de.saxsys.mvvmfx.ViewModel; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; diff --git a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/WeakNotificationsTest.java b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/WeakNotificationsTest.java index deb36f076..dcb5907dd 100644 --- a/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/WeakNotificationsTest.java +++ b/mvvmfx/src/test/java/de/saxsys/mvvmfx/utils/notifications/viewmodel/WeakNotificationsTest.java @@ -1,10 +1,8 @@ package de.saxsys.mvvmfx.utils.notifications.viewmodel; -import de.saxsys.mvvmfx.testingutils.jfxrunner.JfxRunner; import de.saxsys.mvvmfx.utils.notifications.*; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; /** @@ -12,7 +10,6 @@ * To do this most test cases of {@link DefaultNotificationCenterTest} * are reproduced with the weak variant of notifications. */ -@RunWith(JfxRunner.class) public class WeakNotificationsTest { @@ -26,7 +23,7 @@ public class WeakNotificationsTest { NotificationObserver observer2; NotificationObserver observer3; - @Before + @BeforeEach public void init() { observer1 = Mockito.mock(NotificationObserver.class); observer2 = Mockito.mock(NotificationObserver.class); diff --git a/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/builderfactory/BuilderFactoryTestView.fxml b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/builderfactory/BuilderFactoryTestView.fxml new file mode 100644 index 000000000..1d2813416 --- /dev/null +++ b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/builderfactory/BuilderFactoryTestView.fxml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsController.fxml b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsController.fxml new file mode 100644 index 000000000..68f5ce687 --- /dev/null +++ b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsController.fxml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerChild.fxml b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerChild.fxml new file mode 100644 index 000000000..711f3c2e3 --- /dev/null +++ b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerChild.fxml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerParent.fxml b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerParent.fxml new file mode 100644 index 000000000..75ca9f207 --- /dev/null +++ b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerParent.fxml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerWithCustomPath.fxml b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerWithCustomPath.fxml new file mode 100644 index 000000000..9c626ab24 --- /dev/null +++ b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewModelAsControllerWithCustomPath.fxml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithCustomPath.fxml b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithCustomPath.fxml new file mode 100644 index 000000000..036fd7e8d --- /dev/null +++ b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithCustomPath.fxml @@ -0,0 +1,6 @@ + + + + + + diff --git a/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithAnnotatedInitialize.fxml b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithAnnotatedInitialize.fxml new file mode 100644 index 000000000..7bad2467f --- /dev/null +++ b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithAnnotatedInitialize.fxml @@ -0,0 +1,6 @@ + + + + + + diff --git a/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithMultipleInitializeAnnotations.fxml b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithMultipleInitializeAnnotations.fxml new file mode 100644 index 000000000..0eeff62ae --- /dev/null +++ b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/internal/viewloader/example/TestFxmlViewWithViewModelWithMultipleInitializeAnnotations.fxml @@ -0,0 +1,7 @@ + + + + + + diff --git a/mvvmfx/src/test/resources/de/saxsys/mvvmfx/resourcebundle/global/third.properties b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/resourcebundle/global/third.properties new file mode 100644 index 000000000..7cae60549 --- /dev/null +++ b/mvvmfx/src/test/resources/de/saxsys/mvvmfx/resourcebundle/global/third.properties @@ -0,0 +1,2 @@ +third_label=third +label=third \ No newline at end of file diff --git a/pom.xml b/pom.xml index a74e31d7e..fef7fd199 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ de.saxsys mvvmfx-parent pom - 1.6.0 + 1.7.0 mvvmFX parent Application Framework for MVVM with JavaFX. http://www.saxsys.de @@ -149,20 +149,15 @@ - - javax.inject - javax.inject - 1 - javax.enterprise cdi-api - 1.2 + 2.0 org.jboss.weld.se weld-se-core - 2.2.11.Final + 3.0.0.Final com.cathive.fx @@ -188,6 +183,11 @@ junit 4.12 + + org.junit.jupiter + junit-jupiter-api + 5.0.0-RC2 + org.mockito mockito-all @@ -275,6 +275,29 @@ maven-gpg-plugin 1.1 + + org.apache.maven.plugins + maven-surefire-plugin + + 2.19.1 + + + org.junit.platform + junit-platform-surefire-provider + 1.0.0-RC2 + + + org.junit.jupiter + junit-jupiter-engine + 5.0.0-RC2 + + + org.junit.vintage + junit-vintage-engine + 4.12.0-RC2 + + + @@ -326,7 +349,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.16 + 2.19.1 @@ -335,6 +358,23 @@ + + + org.junit.platform + junit-platform-surefire-provider + 1.0.0-RC2 + + + org.junit.jupiter + junit-jupiter-engine + 5.0.0-RC2 + + + org.junit.vintage + junit-vintage-engine + 4.12.0-RC2 + +