diff --git a/.gitignore b/.gitignore index 7b694a1..038f9b6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ hs_err_pid* # Eclipse project information generated by Maven .classpath .project +test-output diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index d3e087c..9862252 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -50,7 +50,7 @@ org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=error org.eclipse.jdt.core.compiler.problem.missingJavadocComments=warning org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled -org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=private +org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=default org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=all_standard_tags org.eclipse.jdt.core.compiler.problem.missingJavadocTags=warning org.eclipse.jdt.core.compiler.problem.missingJavadocTagsMethodTypeParameters=enabled @@ -139,7 +139,7 @@ org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 org.eclipse.jdt.core.formatter.blank_lines_after_package=1 -org.eclipse.jdt.core.formatter.blank_lines_before_field=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=1 org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 diff --git a/README.md b/README.md index b6bc3dc..bb909cd 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,22 @@ This somewhat vague sentiment does not translate to quality! The code is clean, These features are present in the latest release: +### JavaFX + * [ControlPropertyListener](https://github.com/CodeFX-org/LibFX/wiki/ControlPropertyListener): creating listeners for the property map of JavaFX controls * [ListenerHandle](https://github.com/CodeFX-org/LibFX/wiki/ListenerHandle): encapsulating an observable and a listener for easier add/remove of the listener * [Nestings](https://github.com/CodeFX-org/LibFX/wiki/Nestings): using all the power of JavaFX' properties for nested object aggregations -* [SerializableOptional](https://github.com/CodeFX-org/LibFX/wiki/SerializableOptional): serializable wrapper for `Optional` * [WebViewHyperlinkListener](https://github.com/CodeFX-org/LibFX/wiki/WebViewHyperlinkListener): add hyperlink listeners to JavaFX' `WebView` +### Collections + +* [TransformingCollections](https://github.com/CodeFX-org/LibFX/wiki/TransformingCollections): transforming collections to a different parametric type +* [TreeStreams](https://github.com/CodeFX-org/LibFX/wiki/TreeStreams): streaming nodes of a graph + +### Misc + +* [SerializableOptional](https://github.com/CodeFX-org/LibFX/wiki/SerializableOptional): serializable wrapper for `Optional` + ## Documentation @@ -29,7 +39,7 @@ License details can be found in the *LICENSE* file in the project's root folder. Releases are published [on GitHub](https://github.com/CodeFX-org/LibFX/releases). The release notes also contain a link to the artifact in Maven Central and its coordinates. -The current version is [0.2.1](http://search.maven.org/#artifactdetails|org.codefx.libfx|LibFX|0.2.1|jar): +The current version is [0.3.0](http://search.maven.org/#artifactdetails|org.codefx.libfx|LibFX|0.3.0|jar): **Maven**: @@ -37,14 +47,14 @@ The current version is [0.2.1](http://search.maven.org/#artifactdetails|org.code org.codefx.libfx LibFX - 0.2.1 + 0.3.0 ``` **Gradle**: ``` - compile 'org.codefx.libfx:LibFX:0.2.1' + compile 'org.codefx.libfx:LibFX:0.3.0' ``` ## Development @@ -73,6 +83,6 @@ Nicolai Parlog
CodeFX Web: http://codefx.org
-Mail: nipa@codefx.org
Twitter: https://twitter.com/nipafx
+Mail: nipa@codefx.org
PGP-Key: http://keys.gnupg.net/pks/lookup?op=vindex&search=0xA47A795BA5BF8326
diff --git a/pom.xml b/pom.xml index e1a54a4..97bd0ca 100644 --- a/pom.xml +++ b/pom.xml @@ -6,13 +6,13 @@ org.codefx.libfx LibFX - 0.2.1 + 0.3.0 jar LibFX - LibFX provides utility classes for JavaFX. + LibFX provides usability classes for Java and JavaFX. http://libfx.codefx.org @@ -70,14 +70,14 @@ junit junit - 4.11 + 4.12 test org.mockito mockito-all - 1.9.5 + 1.10.19 test @@ -85,6 +85,21 @@ net.sourceforge.nekohtml nekohtml 1.9.21 + test + + + + com.google.guava + guava-testlib + 18.0 + test + + + + com.nitorcreations + junit-runners + 1.2 + test @@ -103,12 +118,25 @@ org.apache.maven.plugins maven-compiler-plugin - 3.2 + 3.3 1.8 1.8 + + + org.codefx.maven.plugin + jdeps-maven-plugin + 0.1 + + + + jdkinternals + + + + org.apache.maven.plugins @@ -127,7 +155,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 2.10.1 + 2.10.3 attach-javadocs @@ -137,14 +165,6 @@ - - - - api_1.8 - https://docs.oracle.com/javase/8/docs/api/ - - org.apache.maven.plugins maven-gpg-plugin - 1.5 + 1.6 sign-artifacts @@ -175,7 +195,7 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.2 + 1.6.5 true ossrh diff --git a/src/demo/java/org/codefx/libfx/collection/transform/EqualityTransformingSetDemo.java b/src/demo/java/org/codefx/libfx/collection/transform/EqualityTransformingSetDemo.java new file mode 100644 index 0000000..2149d79 --- /dev/null +++ b/src/demo/java/org/codefx/libfx/collection/transform/EqualityTransformingSetDemo.java @@ -0,0 +1,58 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Set; + +/** + * Demonstrates how to use the {@link EqualityTransformingSet}. + *

+ * The demonstrated example is based on the situation that we want a set of strings which uses only their length for + * equality comparison. + */ +public class EqualityTransformingSetDemo { + + /** + * A set of strings which uses the length for equality. + */ + private final Set lengthSet; + + /** + * Creates a new demo. + */ + public EqualityTransformingSetDemo() { + lengthSet = EqualityTransformingCollectionBuilder + .forType(String.class) + .withEquals((a, b) -> a.length() == b.length()) + .withHash(String::length) + .buildSet(); + } + + /** + * Runs this demo. + * + * @param args + * command line arguments (will not be used) + */ + public static void main(String[] args) { + EqualityTransformingSetDemo demo = new EqualityTransformingSetDemo(); + + demo.addSomeElements(); + } + + private void addSomeElements() { + print("-- Adding some elements --"); + print(); + + lengthSet.add("a"); + lengthSet.add("b"); + print(lengthSet.toString()); + } + + private static void print() { + System.out.println(); + } + + private static void print(String text) { + System.out.println(text); + } + +} diff --git a/src/demo/java/org/codefx/libfx/collection/transform/OptionalTransformingSetDemo.java b/src/demo/java/org/codefx/libfx/collection/transform/OptionalTransformingSetDemo.java new file mode 100644 index 0000000..b26ab04 --- /dev/null +++ b/src/demo/java/org/codefx/libfx/collection/transform/OptionalTransformingSetDemo.java @@ -0,0 +1,203 @@ +package org.codefx.libfx.collection.transform; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +/** + * Demonstrates how to use the {@link OptionalTransformingSet}. + *

+ * The demonstrated example is based on the situation that some code produces a {@link Set} of {@link Optional} strings. + * Since this is generally undesired, the {@code OptionalTransformingSet} is used to extract the strings. + */ +public class OptionalTransformingSetDemo { + + // #begin FIELDS + + /** + * The inner set, which - for some strange reason - contains {@link Optional}s. + */ + private final Set> innerSet; + + /** + * The transformation without {@link Optional}. + */ + private final Set transformingSet; + + // #end FIELDS + + // #begin CONSTRUCTION & MAIN + + /** + * Creates a new demo. + */ + public OptionalTransformingSetDemo() { + innerSet = new HashSet<>(); + transformingSet = new OptionalTransformingSet(innerSet, String.class, null); + } + + /** + * Runs this demo. + * + * @param args + * command line arguments (will not be used) + */ + public static void main(String[] args) { + print("Outputs are written as 'Modification -> innerSet.toString ~ transformingSet.toString'".toUpperCase()); + print(); + + OptionalTransformingSetDemo demo = new OptionalTransformingSetDemo(); + + demo.modifyingInnerSet(); + demo.modifyingTransformedSet(); + demo.exceptionOnInnerNullElements(); + demo.breakingInverseFunctions(); + } + + // #end CONSTRUCTION & MAIN + + // #begin DEMOS + + private void modifyingInnerSet() { + print("-- Modifying the inner set --"); + + print("Insert optionals for 'A', 'B', 'C'"); + innerSet.add(Optional.of("A")); + innerSet.add(Optional.of("B")); + innerSet.add(Optional.of("C")); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Remove optional for 'B'"); + innerSet.remove(Optional.of("B")); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Insert empty optional"); + innerSet.add(Optional.empty()); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Insert optionals for 'Ax', 'Cx', 'Cy'"); + innerSet.add(Optional.of("Ax")); + innerSet.add(Optional.of("Cx")); + innerSet.add(Optional.of("Cy")); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Remove optionals with content starting with 'C'"); + innerSet.removeIf(optional -> optional.map(string -> string.startsWith("C")).orElse(false)); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Clear"); + innerSet.clear(); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print(); + } + + private void modifyingTransformedSet() { + print("-- Modifying the transforming set --"); + + print("Insert 'A', 'B', 'C'"); + transformingSet.add(("A")); + transformingSet.add(("B")); + transformingSet.add(("C")); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Remove 'B'"); + transformingSet.remove(("B")); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Insert default value for empty optional (which is null)"); + transformingSet.add(null); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Insert 'Ax', 'Cx', 'Cy'"); + transformingSet.add(("Ax")); + transformingSet.add(("Cx")); + transformingSet.add(("Cy")); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Remove strings starting with 'C'"); + transformingSet.removeIf(string -> string != null && string.startsWith("C")); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Clear"); + transformingSet.clear(); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print(); + } + + private void exceptionOnInnerNullElements() { + print("-- Causing a NullPointerException when accessing an inner map with null --"); + print("The 'OptionalTransformingMap' does not allow the inner map to contain null" + + " ('Optional.empty()' should be used instead)."); + + print("Inserting null into inner set will cause no exception"); + innerSet.add(null); + + print("But viewing a set with null will:"); + try { + print("\t -> " + innerSet + " ~ " + transformingSet); + } catch (NullPointerException ex) { + print("\t " + ex.toString()); + } + + print("Clear"); + innerSet.clear(); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print(); + } + + private void breakingInverseFunctions() { + print("-- Breaking the contract with non-inverse functions --"); + print("If a default value is specified which occurs in one of the optionals," + + " the implicitly created transformations are non-inverse."); + print("This can be used to create unexpected behavior..."); + print(); + + print("Creating a map with default value 'DEFAULT'."); + String defaultValue = "DEFAULT"; + Set transformingSet = new OptionalTransformingSet(innerSet, String.class, defaultValue); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Insert empy optional into inner set"); + innerSet.add(Optional.empty()); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Insert optional with default string into inner set"); + innerSet.add(Optional.of(defaultValue)); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Sizes of different sets"); + print("\t inner set: " + innerSet.size()); + print("\t transforming set: " + transformingSet.size()); + print("\t new set: " + new HashSet<>(transformingSet).size() + " (!)"); + + print("Now try to remove the value from the transforming set:"); + print("\t before: " + transformingSet); + print("\t remove 'DEFAULT' (returns " + transformingSet.remove(defaultValue) + ") -> " + transformingSet); + print("\t remove 'DEFAULT' (returns " + transformingSet.remove(defaultValue) + ") -> " + transformingSet); + print("\t Damn it!"); + + print("The transforming set does not contain its own elements:"); + print("\t 'transformingSet.contains(transformingSet.iterator().next())' -> " + + transformingSet.contains(transformingSet.iterator().next())); + + print("Clear"); + innerSet.clear(); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print(); + } + + // #end DEMOS + + private static void print() { + System.out.println(); + } + + private static void print(String text) { + System.out.println(text); + } + +} diff --git a/src/demo/java/org/codefx/libfx/collection/transform/TransformingSetDemo.java b/src/demo/java/org/codefx/libfx/collection/transform/TransformingSetDemo.java new file mode 100644 index 0000000..457705b --- /dev/null +++ b/src/demo/java/org/codefx/libfx/collection/transform/TransformingSetDemo.java @@ -0,0 +1,214 @@ +package org.codefx.libfx.collection.transform; + +import java.util.HashSet; +import java.util.Set; + +/** + * Demonstrates how to use the {@link TransformingSet}. + *

+ * The demonstrated example is based on the situation that a {@link Set} of {@link String}s which only ever contains + * natural numbers as character sequences is to be represented as a {@code Set} of {@link Integer}s. + *

+ * This is not entirely trivial as leading zeros allow multiple strings to be mapped to the same integer which will make + * the transformation function non-inverse. + */ +public class TransformingSetDemo { + + // #begin FIELDS + + /** + * The set of strings which will be the inner/transformed set. + */ + private final Set innerSet; + + /** + * A view on the set which uses integers instead. + */ + private final Set transformingSet; + + // #end FIELDS + + // #begin CONSTRUCTION, MAIN & TRANSFORMATION + + /** + * Creates a new demo. + */ + public TransformingSetDemo() { + innerSet = new HashSet<>(); + transformingSet = TransformingCollectionBuilder + .forInnerAndOuterType(String.class, Integer.class) + .toOuter(this::stringToInteger) + .toInner(this::integerToString) + .transformSet(innerSet); + + print("-- Initial state --"); + print("\t -> " + innerSet + " ~ " + transformingSet); + print(); + } + + private Integer stringToInteger(String string) { + return Integer.parseInt(string); + } + + private String integerToString(Integer integer) { + return integer.toString(); + } + + /** + * Runs this demo. + * + * @param args + * command line arguments (will not be used) + */ + public static void main(String[] args) { + print("Outputs are written as 'Modification -> innerSet.toString ~ transformingSet.toString'".toUpperCase()); + print(); + + TransformingSetDemo demo = new TransformingSetDemo(); + + demo.modifyingInnerSet(); + demo.modifyingTransformedSet(); + demo.breakingInverseFunctions(); + demo.typeSafety(); + } + + // #end CONSTRUCTION, MAIN & TRANSFORMATION + + // #begin DEMOS + + private void modifyingInnerSet() { + print("-- Modifying the inner set --"); + + print("Insert '0', '1', '2'"); + innerSet.add("0"); + innerSet.add("1"); + innerSet.add("2"); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Remove '1'"); + innerSet.remove("1"); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Insert '10', '11', '12'"); + innerSet.add("10"); + innerSet.add("11"); + innerSet.add("12"); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Remove strings ending in '0'"); + innerSet.removeIf(string -> string.endsWith("0")); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Clear"); + innerSet.clear(); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print(); + } + + private void modifyingTransformedSet() { + print("-- Modifying the transforming set --"); + + print("Insert 0, 1, 2"); + transformingSet.add(0); + transformingSet.add(1); + transformingSet.add(2); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Remove 1"); + transformingSet.remove(1); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Insert 10, 11, 12"); + transformingSet.add(10); + transformingSet.add(11); + transformingSet.add(12); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Remove odd numbers"); + transformingSet.removeIf(integer -> integer % 2 != 0); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Clear"); + transformingSet.clear(); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print(); + } + + private void breakingInverseFunctions() { + print("-- Breaking the contract with non-inverse functions --"); + print("The functions are non-inverse:"); + String leadingZeroTenString = "010"; + Integer ten = stringToInteger(leadingZeroTenString); + String tenString = integerToString(ten); + print("\t " + leadingZeroTenString + " -toInteger-> " + ten + " -toString-> " + tenString); + print("This can be used to create unexpected behavior..."); + print(); + + print("Insert '010' into inner set"); + innerSet.add("010"); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Insert '10' into inner set"); + innerSet.add("10"); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print("Sizes of different sets"); + print("\t inner set: " + innerSet.size()); + print("\t transforming set: " + transformingSet.size()); + print("\t new set: " + new HashSet<>(transformingSet).size() + " (!)"); + + print("Now try to remove the value from the transforming set:"); + print("\t before: " + transformingSet); + print("\t remove 10 (returns " + transformingSet.remove(10) + ") -> " + transformingSet); + print("\t remove 10 (returns " + transformingSet.remove(10) + ") -> " + transformingSet); + print("\t Damn it!"); + + print("The transforming set does not contain its own elements:"); + print("\t 'transformingSet.contains(transformingSet.iterator().next())' -> " + + transformingSet.contains(transformingSet.iterator().next())); + + print("Clear"); + innerSet.clear(); + print("\t -> " + innerSet + " ~ " + transformingSet); + + print(); + } + + private void typeSafety() { + print("-- Using type tokens to increase type safety --"); + Set transformingSetWithoutTokens = TransformingCollectionBuilder + . forInnerAndOuterTypeUnknown() + .toOuter(this::stringToInteger) + .toInner(this::integerToString) + .transformSet(innerSet); + + print("Insert 0, 1, 2"); + transformingSet.add(0); + transformingSet.add(1); + transformingSet.add(2); + print("\t -> " + innerSet + " ~ " + transformingSet + " ~ " + transformingSetWithoutTokens); + + print("Calling contains with an 'Object o'"); + Object o = new Object(); + print("\t 'innerSet.contains(o)': " + innerSet.contains(o)); + print("\t 'transformingSet.contains(o)': " + transformingSet.contains(o)); + try { + print("\t 'transformingSetWithoutTokens.contains(o)': " + transformingSetWithoutTokens.contains(o)); + } catch (ClassCastException ex) { + print("\t 'transformingSetWithoutTokens.contains(o)': CLASS CAST EXEPTION"); + } + } + + // #end DEMOS + + private static void print() { + System.out.println(); + } + + private static void print(String text) { + System.out.println(text); + } + +} diff --git a/src/demo/java/org/codefx/libfx/control/ControlPropertyListenerDemo.java b/src/demo/java/org/codefx/libfx/control/properties/ControlPropertyListenerDemo.java similarity index 98% rename from src/demo/java/org/codefx/libfx/control/ControlPropertyListenerDemo.java rename to src/demo/java/org/codefx/libfx/control/properties/ControlPropertyListenerDemo.java index 589a75f..54b3b8a 100644 --- a/src/demo/java/org/codefx/libfx/control/ControlPropertyListenerDemo.java +++ b/src/demo/java/org/codefx/libfx/control/properties/ControlPropertyListenerDemo.java @@ -1,4 +1,4 @@ -package org.codefx.libfx.control; +package org.codefx.libfx.control.properties; import java.util.function.Consumer; @@ -14,7 +14,7 @@ @SuppressWarnings("static-method") public class ControlPropertyListenerDemo { - // #region CONSTRUCTION & MAIN + // #begin CONSTRUCTION & MAIN /** * Creates a new demo. @@ -43,7 +43,7 @@ public static void main(String[] args) { // #end CONSTRUCTION & MAIN - // #region DEMOS + // #begin DEMOS /** * Demonstrates the simple case, in which a value processor is added for some key. diff --git a/src/demo/java/org/codefx/libfx/control/webview/WebViewHyperlinkListenerDemo.java b/src/demo/java/org/codefx/libfx/control/webview/WebViewHyperlinkListenerDemo.java index 35f6a4c..35549b7 100644 --- a/src/demo/java/org/codefx/libfx/control/webview/WebViewHyperlinkListenerDemo.java +++ b/src/demo/java/org/codefx/libfx/control/webview/WebViewHyperlinkListenerDemo.java @@ -20,7 +20,7 @@ */ public class WebViewHyperlinkListenerDemo extends Application { - // #region INITIALIZATION + // #begin INITIALIZATION /** * Runs this demo. @@ -96,7 +96,7 @@ private static CheckBox createCancelEventBox() { // #end INITIALIZATION - // #region LISTENER + // #begin LISTENER /** * Attaches/detaches the specified listener to/from the specified web view according to the specified property's diff --git a/src/demo/java/org/codefx/libfx/listener/handle/ListenerHandleDemo.java b/src/demo/java/org/codefx/libfx/listener/handle/ListenerHandleDemo.java index cd3ac64..00eda6d 100644 --- a/src/demo/java/org/codefx/libfx/listener/handle/ListenerHandleDemo.java +++ b/src/demo/java/org/codefx/libfx/listener/handle/ListenerHandleDemo.java @@ -11,7 +11,7 @@ @SuppressWarnings("static-method") public class ListenerHandleDemo { - // #region CONSTRUCTION & MAIN + // #begin CONSTRUCTION & MAIN /** * Creates a new demo. @@ -36,7 +36,7 @@ public static void main(String[] args) { // #end CONSTRUCTION & MAIN - // #region DEMOS + // #begin DEMOS // construction @@ -121,7 +121,7 @@ private void attachAndDetach() { // #end DEMOS - // #region NESTED CLASSES + // #begin NESTED CLASSES /** * Represents a custom observable instance. Note that it is not necessary to implement {@link Observable} (or any @@ -129,12 +129,12 @@ private void attachAndDetach() { */ private static class MyCustomObservable { - @SuppressWarnings({ "javadoc", "unused" }) + @SuppressWarnings({ "unused" }) public void addListener(MyCustomListener listener) { // do nothing - just for demo } - @SuppressWarnings({ "javadoc", "unused" }) + @SuppressWarnings({ "unused" }) public void removeListener(MyCustomListener listener) { // do nothing - just for demo } diff --git a/src/demo/java/org/codefx/libfx/nesting/Employee.java b/src/demo/java/org/codefx/libfx/nesting/Employee.java index 06a1005..2caab86 100644 --- a/src/demo/java/org/codefx/libfx/nesting/Employee.java +++ b/src/demo/java/org/codefx/libfx/nesting/Employee.java @@ -54,7 +54,7 @@ public Property

addressProperty() { return address; } - // #region INNER CLASSES + // #begin INNER CLASSES /** * A simple demo class which represents an employee's address. diff --git a/src/demo/java/org/codefx/libfx/nesting/NestedDemo.java b/src/demo/java/org/codefx/libfx/nesting/NestedDemo.java index d98ca13..91d8aad 100644 --- a/src/demo/java/org/codefx/libfx/nesting/NestedDemo.java +++ b/src/demo/java/org/codefx/libfx/nesting/NestedDemo.java @@ -17,7 +17,7 @@ */ public class NestedDemo { - // #region FIELDS + // #begin FIELDS /** * The currently selected employee. @@ -26,7 +26,7 @@ public class NestedDemo { //#end FIELDS - // #region CONSTRUCTION & MAIN + // #begin CONSTRUCTION & MAIN /** * Creates a new demo. @@ -50,13 +50,14 @@ public static void main(String[] args) { demo.nestedPropertyCreation(); demo.nestedPropertyCreationWithBuilder(); demo.nestedPropertyBinding(); - demo.nestedPropertyBindingWithMissingInnerObservable(); + demo.nestedPropertyBindingWithMissingInnerObservableAndDefaultBehavior(); + demo.nestedPropertyBindingWithMissingInnerObservableAndCustomizedBehavior(); demo.additionalNestedFeatures(); } //#end CONSTRUCTION & MAIN - // #region DEMOS + // #begin DEMOS /** * Demonstrates how to create a {@link Nesting}. @@ -281,10 +282,11 @@ private void nestedPropertyBinding() { } /** - * Demonstrates how a {@link NestedProperty} behaves when the inner observable is missing. + * Demonstrates how a {@link NestedProperty} behaves by default when the inner observable is missing. */ - private void nestedPropertyBindingWithMissingInnerObservable() { - print("NESTED PROPERTY BINDING WHEN INNER OBSERVABLE IS MISSING"); + private void nestedPropertyBindingWithMissingInnerObservableAndDefaultBehavior() { + print("NESTED PROPERTY BINDING WHEN INNER OBSERVABLE IS MISSING (DEFAULT)"); + currentEmployee.getValue().addressProperty().getValue().streetNameProperty().set("Some Street"); // create a nested property for the current employee's street name NestedStringProperty currentEmployeesStreetName = Nestings.on(currentEmployee) @@ -298,8 +300,43 @@ private void nestedPropertyBindingWithMissingInnerObservable() { print("The inner observable is now missing (is present: \"" + currentEmployeesStreetName.isInnerObservablePresent() + "\")"); - currentEmployeesStreetName.set("Null Street"); - print("The nested property can still be changed: \"" + currentEmployeesStreetName.get() + "\""); + try { + currentEmployeesStreetName.set("Null Street"); + print("You should never see this on the console."); + } catch (Exception ex) { + print("By default, the nested property can not be changed: " + ex.getClass()); + // reset the example to a proper state + currentEmployee.getValue().addressProperty().setValue(new Employee.Address("Some Street")); + } + + print(); + } + + /** + * Demonstrates how a {@link NestedProperty} can be configured to behave differently when the inner observable is + * missing. + */ + private void nestedPropertyBindingWithMissingInnerObservableAndCustomizedBehavior() { + print("NESTED PROPERTY BINDING WHEN INNER OBSERVABLE IS MISSING (CUSTOM)"); + + // create a nested property for the current employee's street name + NestedStringProperty currentEmployeesStreetName = Nestings.on(currentEmployee) + .nest(Employee::addressProperty) + .nestStringProperty(Address::streetNameProperty) + .buildPropertyWithBuilder() + .onInnerObservableMissingSetValue("Null street") + .onUpdateWhenInnerObservableMissingAcceptValues() + .build(); + + print("Nested property's initial street name: \"" + currentEmployeesStreetName.get() + "\""); + + currentEmployee.getValue().addressProperty().setValue(null); + print("The inner observable is now missing (is present: \"" + + currentEmployeesStreetName.isInnerObservablePresent() + "\")"); + print("The street name changed to the specified value: \"" + currentEmployeesStreetName.get() + "\""); + + currentEmployeesStreetName.set("Another Street"); + print("The nested property can be changed: \"" + currentEmployeesStreetName.get() + "\""); currentEmployee.getValue().addressProperty().setValue(new Employee.Address("New Street")); print("When a new inner observable is present (\"" + currentEmployeesStreetName.isInnerObservablePresent() diff --git a/src/demo/java/org/codefx/libfx/serialization/SerializableOptionalDemo.java b/src/demo/java/org/codefx/libfx/serialization/SerializableOptionalDemo.java index 22d4198..3360a3c 100644 --- a/src/demo/java/org/codefx/libfx/serialization/SerializableOptionalDemo.java +++ b/src/demo/java/org/codefx/libfx/serialization/SerializableOptionalDemo.java @@ -154,7 +154,6 @@ private static void print(String text) { /** * A class with methods which have an optional return value or argument. */ - @SuppressWarnings("javadoc") private static class SearchAndLog { Random random = new Random(); diff --git a/src/main/java/org/codefx/libfx/collection/transform/AbstractReadOnlyTransformingCollection.java b/src/main/java/org/codefx/libfx/collection/transform/AbstractReadOnlyTransformingCollection.java new file mode 100644 index 0000000..182580d --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/AbstractReadOnlyTransformingCollection.java @@ -0,0 +1,54 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Collection; +import java.util.function.Predicate; + +/** + * Abstract superclass to read-only {@link Collection}s which transform another collection. + * + * @param + * the inner type, i.e. the type of the elements contained in the wrapped/inner collection + * @param + * the outer type, i.e. the type of elements appearing to be in this collection + * @see AbstractTransformingCollection + */ +abstract class AbstractReadOnlyTransformingCollection extends AbstractTransformingCollection { + + // prevent modification + + @Override + public final boolean add(O element) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean addAll(Collection otherCollection) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean remove(Object object) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean removeIf(Predicate filter) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean removeAll(Collection otherCollection) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean retainAll(Collection otherCollection) { + throw new UnsupportedOperationException(); + } + + @Override + public final void clear() { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/AbstractReadOnlyTransformingMap.java b/src/main/java/org/codefx/libfx/collection/transform/AbstractReadOnlyTransformingMap.java new file mode 100644 index 0000000..9aff80c --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/AbstractReadOnlyTransformingMap.java @@ -0,0 +1,90 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Abstract superclass to read-only {@link Map}s which transform another map. + * + * @param + * the inner key type, i.e. the type of the keys contained in the wrapped/inner map + * @param + * the outer key type, i.e. the type of keys appearing to be in this map + * @param + * the inner value type, i.e. the type of the values contained in the wrapped/inner map + * @param + * the outer value type, i.e. the type of values appearing to be in this map + * @see AbstractTransformingMap + */ +abstract class AbstractReadOnlyTransformingMap + extends AbstractTransformingMap { + + // prevent modification + + @Override + public final OV put(OK key, OV value) { + throw new UnsupportedOperationException(); + } + + @Override + public final OV putIfAbsent(OK key, OV value) { + throw new UnsupportedOperationException(); + } + + @Override + public final void putAll(Map outerMap) { + throw new UnsupportedOperationException(); + } + + @Override + public final OV compute(OK key, BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public final OV computeIfAbsent(OK key, Function mappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public final OV computeIfPresent(OK key, BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public final OV merge(OK key, OV value, BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public final OV replace(OK key, OV value) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean replace(OK key, OV oldValue, OV newValue) { + throw new UnsupportedOperationException(); + } + + @Override + public final void replaceAll(BiFunction function) { + throw new UnsupportedOperationException(); + } + + @Override + public final OV remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean remove(Object key, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public final void clear() { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/AbstractReadOnlyTransformingSet.java b/src/main/java/org/codefx/libfx/collection/transform/AbstractReadOnlyTransformingSet.java new file mode 100644 index 0000000..3502b6e --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/AbstractReadOnlyTransformingSet.java @@ -0,0 +1,55 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Collection; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Abstract superclass to read-only {@link Set}s which transform another set. + * + * @param + * the inner type, i.e. the type of the elements contained in the wrapped/inner set + * @param + * the outer type, i.e. the type of elements appearing to be in this set + * @see AbstractTransformingSet + */ +abstract class AbstractReadOnlyTransformingSet extends AbstractTransformingSet { + + // prevent modification + + @Override + public final boolean add(O element) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean addAll(Collection otherCollection) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean remove(Object object) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean removeIf(Predicate filter) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean removeAll(Collection otherCollection) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean retainAll(Collection otherCollection) { + throw new UnsupportedOperationException(); + } + + @Override + public final void clear() { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingCollection.java b/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingCollection.java new file mode 100644 index 0000000..09ae676 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingCollection.java @@ -0,0 +1,792 @@ +package org.codefx.libfx.collection.transform; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Objects; +import java.util.Spliterator; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Abstract superclass to {@link Collection}s which transform another collection. + *

+ * This class allows null elements. Subclasses might override that by implementing aggressive null checks. + * + * @param + * the inner type, i.e. the type of the elements contained in the wrapped/inner collection + * @param + * the outer type, i.e. the type of elements appearing to be in this collection + */ +abstract class AbstractTransformingCollection implements Collection { + + // #begin CONSTANTS + + /** + * The largest possible (non-power of two) array size. + *

+ * Note that some collections can contain more entries than fit into {@code int} (see e.g. + * {@link java.util.concurrent.ConcurrentHashMap#mappingCount() mappingCount()}). In that case, the return value of + * {@link #size()} is capped at {@link Integer#MAX_VALUE}, which is ok because it is greater than + * {@code MAX_ARRAY_SIZE} and will throw an error anyways. + */ + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + + /** + * The message used for {@link OutOfMemoryError}s. + */ + private static final String COLLECTION_TOO_LARGE_ERROR_MESSAGE = "Required array size too large"; + + // #end CONSTANTS + + // #begin IMPLEMENTATION OF 'Collection' + + /** + * Indicates whether the specified collection is equivalent to this one. This is the case if it is also an + * {@link AbstractTransformingCollection} and wraps the same {@link #getInnerCollection() innerCollection}. + * + * @param otherCollection + * the {@link Collection} which is compared with this one + * @return true if this and the specified collection are equivalent + */ + protected final boolean isThisCollection(Collection otherCollection) { + if (otherCollection == this) + return true; + + if (otherCollection instanceof AbstractTransformingCollection) { + AbstractTransformingCollection otherTransformingCollection = + (AbstractTransformingCollection) otherCollection; + boolean sameInnerCollection = otherTransformingCollection.getInnerCollection() == getInnerCollection(); + return sameInnerCollection; + } + + return false; + } + + // size + + @Override + public int size() { + return getInnerCollection().size(); + } + + @Override + public boolean isEmpty() { + return getInnerCollection().isEmpty(); + } + + // contains + + @Override + public boolean contains(Object object) { + if (isOuterElement(object)) { + @SuppressWarnings("unchecked") + /* + * This cast can not fail due to erasure but the following call to 'transformToInner' might. In that case a + * 'ClassCastException' will be thrown which is in accordance with the contract of 'contains'. If + * 'isOuterElement' does its job well (which can be hard due to erasure) this will not happen. + */ + O outerElement = (O) object; + I innerElement = transformToInner(outerElement); + return getInnerCollection().contains(innerElement); + } else + return false; + } + + @Override + public boolean containsAll(Collection otherCollection) { + Objects.requireNonNull(otherCollection, "The argument 'otherCollection' must not be null."); + if (isThisCollection(otherCollection)) + return true; + + return callContainsAllOnInner(otherCollection); + } + + /** + * Wraps the specified collection into a transformation and calls {@link Collection#containsAll(Collection) + * containsAll} on the {@link #getInnerCollection() innerCollection}. + *

+ * Subclasses may chose to use this method if they override {@link #containsAll(Collection)}. + *

+ * Accessing the wrapped collection will lead to {@link ClassCastException}s when its elements are not of this + * collection's outer type {@code O}. Consider using {@link #callContainsOnThis(Collection)}. + * + * @param otherCollection + * the parameter to {@code containsAll} + * @return result of the call to {@code containsAll} + */ + protected final boolean callContainsAllOnInner(Collection otherCollection) { + Collection asInnerCollection = new TransformToReadOnlyInnerCollection<>(otherCollection); + return getInnerCollection().containsAll(asInnerCollection); + } + + /** + * Iterates over the specified collection and calls {@link #contains(Object)} (on this collection) for each element. + *

+ * Subclasses may chose to use this method if they override {@link #containsAll(Collection)}. + *

+ * Manually iterating over the specified collection and calling {@code this.}{@link #contains(Object)} individually + * might break guarantees or optimizations made by the inner collection. Consider using + * {@link #callContainsAllOnInner(Collection)}. + * + * @param otherCollection + * the collection whose elements are passed to {@code contains} + * @return false if at least one call to {@code contains} returns false; otherwise true + */ + protected final boolean callContainsOnThis(Collection otherCollection) { + for (Object item : otherCollection) + if (!contains(item)) + return false; + return true; + } + + // add + + @Override + public boolean add(O element) { + I innerElement = transformToInner(element); + return getInnerCollection().add(innerElement); + } + + @Override + public boolean addAll(Collection otherCollection) { + Objects.requireNonNull(otherCollection, "The argument 'otherCollection' must not be null."); + + return callAddAllOnInner(otherCollection); + } + + /** + * Wraps the specified collection into a transformation and calls {@link Collection#addAll(Collection) addAll} on + * the {@link #getInnerCollection() innerCollection}. + *

+ * Subclasses may chose to use this method if they override {@link #addAll(Collection)}. + *

+ * Accessing the wrapped collection will lead to {@link ClassCastException}s when its elements are not of this + * collection's outer type {@code O}. Consider using {@link #callAddOnThis(Collection)}. + * + * @param otherCollection + * the parameter to {@code addAll} + * @return result of the call to {@code addAll} + */ + protected final boolean callAddAllOnInner(Collection otherCollection) { + Collection asInnerCollection = new TransformToReadOnlyInnerCollection<>(otherCollection); + return getInnerCollection().addAll(asInnerCollection); + } + + /** + * Iterates over the specified collection and calls {@link #add(Object) add(O)} (on this collection) for each + * element. + *

+ * Subclasses may chose to use this method if they override {@link #addAll(Collection)}. + *

+ * Manually iterating over the specified collection and calling {@code this.}{@link #add(Object)} individually might + * break guarantees (e.g. regarding atomicity) or optimizations made by the inner collection. Consider using + * {@link #callAddAllOnInner(Collection)}. + * + * @param otherCollection + * the collection whose elements are passed to {@code add} + * @return true if at least one call to {@code add} returns true; otherwise false + */ + protected final boolean callAddOnThis(Collection otherCollection) { + boolean changed = false; + for (O entry : otherCollection) + changed |= add(entry); + return changed; + } + + // remove + + @Override + public boolean remove(Object object) { + if (isOuterElement(object)) { + @SuppressWarnings("unchecked") + /* + * This cast can not fail due to erasure but the following call to 'transformToInner' might. In that case a + * 'ClassCastException' will be thrown which is in accordance with the contract of 'remove'. If + * 'isOuterElement' does its job well (which can be hard due to erasure) this will not happen. + */ + O outerElement = (O) object; + I innerElement = transformToInner(outerElement); + return getInnerCollection().remove(innerElement); + } else + return false; + } + + @Override + public boolean removeIf(Predicate filter) { + Objects.requireNonNull(filter, "The argument 'filter' must not be null."); + + Predicate innerFilter = innerElement -> { + O outerElement = transformToOuter(innerElement); + return filter.test(outerElement); + }; + return getInnerCollection().removeIf(innerFilter); + } + + @Override + public boolean removeAll(Collection otherCollection) { + Objects.requireNonNull(otherCollection, "The argument 'otherCollection' must not be null."); + if (isThisCollection(otherCollection)) + return clearToRemoveAll(); + + return callRemoveAllOnInner(otherCollection); + } + + /** + * Calls {@link #clear()} to remove all instances from the {@link #getInnerCollection() innerCollection}. + * + * @return true if the call changed the innerCollection + */ + protected final boolean clearToRemoveAll() { + if (size() == 0) + return false; + + clear(); + return true; + } + + /** + * Wraps the specified collection into a transformation and calls {@link Collection#removeAll(Collection) removeAll} + * on the {@link #getInnerCollection() innerCollection}. + *

+ * Subclasses may chose to use this method if they override {@link #removeAll(Collection)}. + *

+ * Accessing the wrapped collection will lead to {@link ClassCastException}s when its elements are not of this + * collection's outer type {@code O}. Consider using {@link #callRemoveOnThis(Collection)}. + * + * @param otherCollection + * the parameter to {@code removeAll} + * @return result of the call to {@code removeAll} + */ + protected final boolean callRemoveAllOnInner(Collection otherCollection) { + Collection asInnerCollection = new TransformToReadOnlyInnerCollection<>(otherCollection); + return getInnerCollection().removeAll(asInnerCollection); + } + + /** + * Iterates over the specified collection and calls {@link #remove(Object)} (on this collection) for each element. + *

+ * Subclasses may chose to use this method if they override {@link #removeAll(Collection)}. + *

+ * Manually iterating over the specified collection and calling {@code this.}{@link #remove(Object)} individually + * might break guarantees (e.g. regarding atomicity) or optimizations made by the inner collection. Consider using + * {@link #callRemoveAllOnInner(Collection)}. + * + * @param otherCollection + * the collection whose elements are passed to {@code remove} + * @return true if at least one call to {@code remove} returns true; otherwise false + */ + protected final boolean callRemoveOnThis(Collection otherCollection) { + boolean changed = false; + for (Object entry : otherCollection) + changed |= remove(entry); + return changed; + } + + @Override + public boolean retainAll(Collection otherCollection) { + Objects.requireNonNull(otherCollection, "The argument 'otherCollection' must not be null."); + if (isThisCollection(otherCollection)) + return false; + + return callRetainAllOnInner(otherCollection); + } + + /** + * Wraps the specified collection into a transformation and calls {@link Collection#retainAll(Collection) retainAll} + * on the {@link #getInnerCollection() innerCollection}. + *

+ * Subclasses may choose to use this method if they override {@link #retainAll(Collection)}. + *

+ * Accessing the wrapped collection will lead to {@link ClassCastException}s when its elements are not of this + * collection's outer type {@code O}. Consider using {@link #retainByCallingRemoveOnThis(Collection)}. + * + * @param otherCollection + * the parameter to {@code retainAll} + * @return result of the call to {@code retainAll} + */ + protected final boolean callRetainAllOnInner(Collection otherCollection) { + Collection asInnerCollection = new TransformToReadOnlyInnerCollection<>(otherCollection); + return getInnerCollection().retainAll(asInnerCollection); + } + + /** + * Iterates over this collection (i.e. over the outer elements) and removes each element which is not contained in + * the specified collection. + *

+ * Subclasses may choose to use this method if they override {@link #retainAll(Collection)}. + *

+ * Manually iterating over this collection and calling {@code this.}{@link #remove(Object)} individually might break + * guarantees (e.g. regarding atomicity) or optimizations made by the inner collection. Consider using + * {@link #callRetainAllOnInner(Collection)}. + * + * @param otherCollection + * the collection whose elements are not removed from this collection + * @return true if at least one element was removed; otherwise false + */ + protected final boolean retainByCallingRemoveOnThis(Collection otherCollection) { + boolean changed = false; + for (Iterator iterator = iterator(); iterator.hasNext();) { + O element = iterator.next(); + boolean remove = !otherCollection.contains(element); + if (remove) { + iterator.remove(); + changed = true; + } + } + return changed; + } + + @Override + public void clear() { + getInnerCollection().clear(); + } + + // iteration + + @Override + public Iterator iterator() { + // use an iterator which immediately forwards all transformation calls to this collection; + // this excludes the 'TransformingIterator' which does some null handling on its own + return new ForwardingTransformingIterator(); + } + + @Override + public Spliterator spliterator() { + // use a spliterator which immediately forwards all transformation calls to this collection; + // this excludes the 'TransformingSpliterator' which does some null handling on its own + return new ForwardingTransformingSpliterator(); + } + + // #begin TOARRAY + + @Override + public Object[] toArray() { + /* + * Because this collection view might be used on a map which allows concurrent modifications, the method must be + * able to handle the situation where the number of elements changes throughout the its execution. For this + * reason the code is inspired by 'ConcurrentHashMap.CollectionView.toArray'. + */ + + Object[] array = createObjectArrayWithMapSize(); + + int currentElementIndex = 0; + for (O element : this) { + // the map might have grown, in which case a new array has to be allocated + array = provideArrayWithSufficientLength(array, currentElementIndex); + array[currentElementIndex] = element; + currentElementIndex++; + } + + // the map might have shrunk or a larger array might have been allocated; + // in both cases the array has to be truncated to the correct length + return truncateArrayToLength(array, currentElementIndex); + } + + /** + * Creates an object array with this collection's current {@link #size()}. + * + * @return an empty object array + */ + private Object[] createObjectArrayWithMapSize() { + int size = size(); + if (size > MAX_ARRAY_SIZE) + throw new OutOfMemoryError(COLLECTION_TOO_LARGE_ERROR_MESSAGE); + + return new Object[size]; + } + + /** + * Provides an array with at least the specified minimum length. If the specified array has that length, it is + * returned. Otherwise a new array with an unspecified length but sufficient is returned to which the input array's + * elements were copied. + * + * @param + * the component type of the array + * @param array + * the array whose length is tested + * @param minLength + * the minimum length of the required array + * @return an array with at least length {@code minLength} + */ + private static T[] provideArrayWithSufficientLength(T[] array, int minLength) { + boolean arrayHasSufficientLength = minLength < array.length; + if (arrayHasSufficientLength) + return array; + else + return copyToLargerArray(array); + } + + /** + * Creates a new array with a length greater than the specified one's and copies all elements to it. + * + * @param + * the component type of the array + * @param array + * the array whose elements are copied + * @return a new array + */ + private static T[] copyToLargerArray(T[] array) { + if (array.length == MAX_ARRAY_SIZE) + throw new OutOfMemoryError(COLLECTION_TOO_LARGE_ERROR_MESSAGE); + int newSize = getIncreasedSize(array.length); + return Arrays.copyOf(array, newSize); + } + + /** + * Returns the size for the new array, which is guaranteed to be greater than the specified size. + * + * @param size + * the current size + * @return the new size + */ + private static int getIncreasedSize(int size) { + // bit shifting is used to increase the size by ~ 50 % + boolean sizeWouldBeIncreasedAboveMaximum = size >= MAX_ARRAY_SIZE - (MAX_ARRAY_SIZE >>> 1) - 1; + if (sizeWouldBeIncreasedAboveMaximum) + return MAX_ARRAY_SIZE; + else + return size + (size >>> 1) + 1; + } + + /** + * Returns an array with the specified length.If the specified array has the correct length, it is returned, + * otherwise a new array is allocated and the values are copied into it. + * + * @param + * the component type of the array + * @param array + * the array to be truncated + * @param length + * the new array's length + * @return an array with the specified length + */ + private static T[] truncateArrayToLength(T[] array, int length) { + if (array.length == length) + return array; + else + return Arrays.copyOf(array, length); + } + + @Override + public T[] toArray(T[] inputArray) { + /* + * Because this collection view might be used on a map which allows concurrent modifications, the method must be + * able to handle the situation where the number of elements changes throughout its execution. For this reason + * the code is inspired by 'ConcurrentHashMap.CollectionView.toArray'. + */ + Objects.requireNonNull(inputArray, "The argument 'inputArray' must not be null."); + + T[] array = provideTypedArrayWithMapSize(inputArray); + + int currentElementIndex = 0; + for (O element : this) { + // the map might have grown, in which case a new array has to be allocated + array = provideArrayWithSufficientLength(array, currentElementIndex); + @SuppressWarnings("unchecked") + // due to erasure, this cast can never fail, but writing the reference to the array can; + // this would throw a ArrayStoreException which is in accordance with the contract of + // 'Collection.toArray(T[])' + T unsafelyTypedElement = (T) element; + array[currentElementIndex] = unsafelyTypedElement; + currentElementIndex++; + } + + // if the original array is still used, it must be terminated with null as per contract of this method; + // otherwise, the array created above might be too large so it has to be truncated + return markOrTruncateArray(inputArray, array, currentElementIndex); + } + + /** + * Provides an array of the same type as the specified array which has at least the length of this collection's + * current {@link #size() size}. If the input array is sufficiently long, it is returned; otherwise a new array is + * created. + * + * @param + * the component type of the array + * @param inputArray + * the input array + * @return an array {@code T[]} with length equal to or greater than {@link #size() size} + */ + private T[] provideTypedArrayWithMapSize(T[] inputArray) { + int size = size(); + boolean arrayHasSufficientLength = size <= inputArray.length; + if (arrayHasSufficientLength) + return inputArray; + else { + if (size > MAX_ARRAY_SIZE) + throw new OutOfMemoryError(COLLECTION_TOO_LARGE_ERROR_MESSAGE); + + @SuppressWarnings("unchecked") + // the array created by 'Array.newInstance' is of the correct type + T[] array = (T[]) Array.newInstance(inputArray.getClass().getComponentType(), size); + return array; + } + } + + /** + * The specified {@code array} is prepared to be returned by {@link #toArray(Object[])}. + * + * @param + * the component type of the array + * @param inputArray + * the array which was given to {@link #toArray(Object[])} + * @param array + * the array which contains this collection's elements (might be the same as {@code inputArray}) + * @param nrOfElements + * the number of elements in the {@code array} + * @return an array which fulfills the contract of {@link #toArray(Object[])} + */ + private static T[] markOrTruncateArray(T[] inputArray, T[] array, int nrOfElements) { + boolean usingInputArray = array == inputArray; + if (usingInputArray) + return markEndWithNull(array, nrOfElements); + else + return truncateArrayToLength(array, nrOfElements); + } + + /** + * Returns the specified array but with a null reference at the specified index if the array's length allows it. + * + * @param + * the component type of the array + * @param array + * the array which might be edited + * @param nullIndex + * the index where a null reference has to inserted + * @return the specified array + */ + private static T[] markEndWithNull(T[] array, int nullIndex) { + if (nullIndex < array.length) + array[nullIndex] = null; + + return array; + } + + // #end TOARRAY + + // #end IMPLEMENTATION OF 'Collection' + + // #begin OBJECT + + @Override + public abstract boolean equals(Object object); + + @Override + public abstract int hashCode(); + + @Override + public String toString() { + return stream() + .map(Objects::toString) + .collect(Collectors.joining(", ", "[", "]")); + } + + // #end OBJECT + + // #begin ABSTRACT METHODS + + /** + * @return the inner collection wrapped by this transforming collection + */ + protected abstract Collection getInnerCollection(); + + /** + * Checks whether the specified object might be an inner element. + *

+ * This method does not have to be exact (which might be impossible due to involved generic types) and might produce + * false positives (but no false negatives). + * + * @param object + * the object to check; may be null + * @return true if the object might be an inner element + */ + protected abstract boolean isInnerElement(Object object); + + /** + * Transforms the specified element to an instance of the outer type. + *

+ * It can not be guaranteed that the specified element is really of the inner type. If not, an exception can be + * thrown. + * + * @param innerElement + * the element to transform; may be null + * @return the transformed element + * @throws ClassCastException + * if the specified element is not of the correct type + */ + protected abstract O transformToOuter(I innerElement) throws ClassCastException; + + /** + * Checks whether the specified object might be an outer element. + *

+ * This method does not have to be exact (which might be impossible due to involved generic types) and might produce + * false positives (but no false negatives). + * + * @param object + * the object to check; may be null + * @return true if the object might be an outer element + */ + protected abstract boolean isOuterElement(Object object); + + /** + * Transforms the specified element to an instance of the inner type. + *

+ * It can not be guaranteed that the specified element is really of the outer type. If not, an exception can be + * thrown. + * + * @param outerElement + * the element to transform; may be null + * @return the transformed element + * @throws ClassCastException + * if the specified element is not of the correct type + */ + protected abstract I transformToInner(O outerElement) throws ClassCastException; + + // #end ABSTRACT METHODS + + // #begin INNER CLASSES + + /** + * A transforming iterator which directly forwards all transformation calls to the abstract methods in this + * collection. + */ + private class ForwardingTransformingIterator extends AbstractTransformingIterator { + + private final Iterator innerIterator = getInnerCollection().iterator(); + + @Override + protected Iterator getInnerIterator() { + return innerIterator; + } + + @Override + protected O transformToOuter(I innerElement) { + return AbstractTransformingCollection.this.transformToOuter(innerElement); + } + + } + + /** + * A transforming spliterator which directly forwards all transformation calls to the abstract methods in this + * collection. + */ + private class ForwardingTransformingSpliterator extends AbstractTransformingSpliterator { + + private final Spliterator innerSpliterator = getInnerCollection().spliterator(); + + @Override + protected Spliterator getInnerSpliterator() { + return innerSpliterator; + } + + @Override + protected O transformToOuter(I innerElement) { + return AbstractTransformingCollection.this.transformToOuter(innerElement); + } + + @Override + protected I transformToInner(O outerElement) { + return AbstractTransformingCollection.this.transformToInner(outerElement); + } + + @Override + protected Spliterator wrapNewSpliterator(Spliterator newSpliterator) { + return new ForwardingTransformingSpliterator(); + } + + } + + /** + * Wraps a collection with any element type {@code E} into a transforming collection with the inner type {@code I}. + *

+ * This works under the assumption that {@code E = O}. Of course, it is highly unsafe and can lead to + * {@link ClassCastException}s when this is not the case and the collection is accessed. + * + * @param + * the type of elements in the specified collection + */ + protected final class TransformToReadOnlyInnerCollection extends AbstractReadOnlyTransformingCollection { + + private final Collection transformedCollection; + + /** + * Creates a new read only collection which transforms the specified one to the inner type + * + * @param transformedCollection + * the collection to transform + */ + public TransformToReadOnlyInnerCollection(Collection transformedCollection) { + assert transformedCollection != null : "The argument 'innerCollection' must not be null."; + + this.transformedCollection = transformedCollection; + } + + @Override + protected Collection getInnerCollection() { + return transformedCollection; + } + + @Override + protected boolean isInnerElement(Object object) { + return AbstractTransformingCollection.this.isOuterElement(object); + } + + @Override + protected I transformToOuter(E innerElement) { + @SuppressWarnings("unchecked") + /* + * This cast can not fail due to erasure but the following call to 'transformToInner' might. In that case a + * 'ClassCastException' will be thrown which is in accordance with the contract of the method which created + * this wrapper. + */ + O asClientOuterElement = (O) innerElement; + return AbstractTransformingCollection.this.transformToInner(asClientOuterElement); + } + + @Override + protected boolean isOuterElement(Object object) { + return AbstractTransformingCollection.this.isInnerElement(object); + } + + @Override + protected E transformToInner(I outerElement) { + O transformedToClientOuterElement = AbstractTransformingCollection.this.transformToOuter(outerElement); + @SuppressWarnings("unchecked") + /* + * This cast can not fail due to erasure but whatever happens next might. In that case a + * 'ClassCastException' will be thrown which is in accordance with the contract of the method which created + * this wrapper. + */ + E asThisInnerElement = (E) transformedToClientOuterElement; + return asThisInnerElement; + } + + @Override + public boolean equals(Object object) { + if (object == this) + return true; + if (!(object instanceof Collection)) + return false; + + Collection other = (Collection) object; + if (isThisCollection(other)) + return true; + + return other.containsAll(this) && this.containsAll(other); + } + + @Override + public int hashCode() { + int hashCode = 1; + for (I clientInnerElement : this) + hashCode = 31 * hashCode + (clientInnerElement == null ? 0 : clientInnerElement.hashCode()); + return hashCode; + } + + } + + // #end INNER CLASSES + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingIterator.java b/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingIterator.java new file mode 100644 index 0000000..fef4b2f --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingIterator.java @@ -0,0 +1,64 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Iterator; +import java.util.function.Consumer; + +/** + * Abstract superclass to {@link Iterator}s which wrap another iterator and transform the returned elements from their + * inner type {@code I} to an outer type {@code O}. + * + * @param + * the inner type, i.e. the type of the elements returned by the wrapped/inner iterator + * @param + * the outer type, i.e. the type of elements returned by this iterator + */ +abstract class AbstractTransformingIterator implements Iterator { + + // #begin IMPLEMENTATION OF 'Iterator' + + @Override + public boolean hasNext() { + return getInnerIterator().hasNext(); + } + + @Override + public O next() { + I nextElement = getInnerIterator().next(); + return transformToOuter(nextElement); + } + + @Override + public void remove() { + getInnerIterator().remove(); + } + + @Override + public void forEachRemaining(Consumer action) { + Consumer transformThenAction = innerElement -> { + O asOuterElement = transformToOuter(innerElement); + action.accept(asOuterElement); + }; + getInnerIterator().forEachRemaining(transformThenAction); + } + + // #end IMPLEMENTATION OF 'Iterator' + + // #begin ABSTRACT METHODS + + /** + * @return the wrapped/inner iterator + */ + protected abstract Iterator getInnerIterator(); + + /** + * Transforms an element from the inner type {@code I} to the outer type {@code O}. + * + * @param innerElement + * an element returned by the {@link #getInnerIterator() innerIterator} + * @return an equivalent element of type {@code O} + */ + protected abstract O transformToOuter(I innerElement); + + // #end ABSTRACT METHODS + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingList.java b/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingList.java new file mode 100644 index 0000000..ba6e0d0 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingList.java @@ -0,0 +1,292 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.ListIterator; +import java.util.Objects; +import java.util.function.UnaryOperator; + +/** + * Abstract superclass to {@link List}s which transform another collection. + *

+ * This class allows null elements. Subclasses might override that by implementing aggressive null checks. + * + * @param + * the inner type, i.e. the type of the elements contained in the wrapped/inner list + * @param + * the outer type, i.e. the type of elements appearing to be in this list + */ +abstract class AbstractTransformingList extends AbstractTransformingCollection implements List { + + @Override + protected final Collection getInnerCollection() { + return getInnerList(); + } + + /** + * @return the inner list wrapped by this transforming list + */ + protected abstract List getInnerList(); + + // get & index + + @Override + public O get(int index) { + I innerElement = getInnerList().get(index); + return transformToOuter(innerElement); + } + + @Override + public int indexOf(Object object) { + if (isOuterElement(object)) { + @SuppressWarnings("unchecked") + /* + * This cast can not fail due to erasure but the following call to 'transformToInner' might. In that case a + * 'ClassCastException' will be thrown which is in accordance with the contract of 'indexOf'. If + * 'isOuterElement' does its job well (which can be hard due to erasure) this will not happen. + */ + O outerElement = (O) object; + I innerElement = transformToInner(outerElement); + return getInnerList().indexOf(innerElement); + } else + return -1; + } + + @Override + public int lastIndexOf(Object object) { + if (isOuterElement(object)) { + @SuppressWarnings("unchecked") + /* + * This cast can not fail due to erasure but the following call to 'transformToInner' might. In that case a + * 'ClassCastException' will be thrown which is in accordance with the contract of 'lastIndexOf'. If + * 'isOuterElement' does its job well (which can be hard due to erasure) this will not happen. + */ + O outerElement = (O) object; + I innerElement = transformToInner(outerElement); + return getInnerList().lastIndexOf(innerElement); + } else + return -1; + } + + // mutate + + @Override + public void add(int index, O element) { + I innerElement = transformToInner(element); + getInnerList().add(index, innerElement); + } + + @Override + public boolean addAll(int index, Collection otherCollection) { + Objects.requireNonNull(otherCollection, "The argument 'otherCollection' must not be null."); + + return callAddAllOnInner(index, otherCollection); + } + + /** + * Wraps the specified collection into a transformation and calls {@link List#addAll(int, Collection) addAll} on the + * {@link #getInnerList() innerList}. + *

+ * Subclasses may choose to use this method if they override {@link #addAll(int, Collection)}. + *

+ * Accessing the wrapped collection will lead to {@link ClassCastException}s when its elements are not of this + * list's outer type {@code O}. Consider using {@link #callAddOnThis(int, Collection)}. + * + * @param startIndex + * index at which to insert the first element from the specified collection + * @param otherCollection + * the parameter to {@code addAll} + * @return result of the call to {@code addAll} + */ + protected final boolean callAddAllOnInner(int startIndex, Collection otherCollection) { + Collection asInnerCollection = new TransformToReadOnlyInnerCollection<>(otherCollection); + return getInnerList().addAll(startIndex, asInnerCollection); + } + + /** + * Iterates over the specified collection and calls {@link #add(int, Object) add()} (on this list) for each element. + *

+ * Subclasses may choose to use this method if they override {@link #addAll(int, Collection)}. + *

+ * Manually iterating over the specified collection and calling {@code this.}{@link #add(int, Object)} individually + * might break guarantees (e.g. regarding atomicity) or optimizations made by the inner collection. Consider using + * {@link #callAddAllOnInner(int, Collection)}. + * + * @param startIndex + * index at which to insert the first element from the specified collection + * @param otherCollection + * the collection whose elements are passed to {@code add} + * @return true if at least one call to {@code add} returns true; otherwise false + */ + protected final boolean callAddOnThis(int startIndex, Collection otherCollection) { + boolean changed = false; + int currentIndex = startIndex; + for (O entry : otherCollection) { + add(currentIndex, entry); + currentIndex++; + changed = true; + } + return changed; + } + + @Override + public O set(int index, O element) { + I innerElement = transformToInner(element); + I formerInnerElement = getInnerList().set(index, innerElement); + return transformToOuter(formerInnerElement); + } + + @Override + public void replaceAll(UnaryOperator operator) { + Objects.requireNonNull(operator, "The argument 'operator' must not be null."); + + UnaryOperator operatorOnInner = inner -> transformToInner(operator.apply(transformToOuter(inner))); + getInnerList().replaceAll(operatorOnInner); + } + + @Override + public O remove(int index) { + I removedInnerElement = getInnerList().remove(index); + return transformToOuter(removedInnerElement); + } + + // sort + + @Override + public void sort(Comparator comparator) { + Objects.requireNonNull(comparator, "The argument 'comparator' must not be null."); + + Comparator comparatorOfInner = (leftInner, rightInner) -> + comparator.compare( + transformToOuter(leftInner), + transformToOuter(rightInner)); + getInnerList().sort(comparatorOfInner); + } + + // iteration & sublist + + @Override + public ListIterator listIterator() { + return new ForwardingTransformingIterator(); + } + + @Override + public ListIterator listIterator(int startIndex) { + return new ForwardingTransformingIterator(startIndex); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return new ForwardingSubList(fromIndex, toIndex); + } + + // #begin OBJECT + + @Override + public final boolean equals(Object object) { + if (object == this) + return true; + if (!(object instanceof List)) + return false; + + List other = (List) object; + + // check all elements + ListIterator thisIterator = listIterator(); + ListIterator otherIterator = other.listIterator(); + while (thisIterator.hasNext() && otherIterator.hasNext()) + if (!(Objects.equals(thisIterator.next(), otherIterator.next()))) + return false; + + // make sure both have the same length + return !thisIterator.hasNext() && !otherIterator.hasNext(); + } + + @Override + public final int hashCode() { + int hashCode = 1; + for (O element : this) + hashCode = 31 * hashCode + (element == null ? 0 : element.hashCode()); + return hashCode; + } + + // #end OBJECT + + // #begin INNER CLASSES + + /** + * A transforming list iterator which directly forwards all transformation calls to the abstract methods in this + * list. + */ + private class ForwardingTransformingIterator extends AbstractTransformingListIterator { + + private final ListIterator innerIterator; + + public ForwardingTransformingIterator() { + innerIterator = getInnerList().listIterator(); + } + + public ForwardingTransformingIterator(int startIndex) { + innerIterator = getInnerList().listIterator(startIndex); + } + + @Override + protected ListIterator getInnerIterator() { + return innerIterator; + } + + @Override + protected O transformToOuter(I innerElement) { + return AbstractTransformingList.this.transformToOuter(innerElement); + } + + @Override + protected I transformToInner(O outerElement) { + return AbstractTransformingList.this.transformToInner(outerElement); + } + + } + + /** + * A transforming list which is the sub list of this list and directly forwards all transformation calls to the + * abstract methods in this list. + */ + private class ForwardingSubList extends AbstractTransformingList { + + private final List innerSubList; + + public ForwardingSubList(int fromIndex, int toIndex) { + innerSubList = AbstractTransformingList.this.getInnerList().subList(fromIndex, toIndex); + } + + @Override + protected List getInnerList() { + return innerSubList; + } + + @Override + protected boolean isInnerElement(Object object) { + return AbstractTransformingList.this.isInnerElement(object); + } + + @Override + protected O transformToOuter(I innerElement) throws ClassCastException { + return AbstractTransformingList.this.transformToOuter(innerElement); + } + + @Override + protected boolean isOuterElement(Object object) { + return AbstractTransformingList.this.isOuterElement(object); + } + + @Override + protected I transformToInner(O outerElement) throws ClassCastException { + return AbstractTransformingList.this.transformToInner(outerElement); + } + + } + + // #end INNER CLASSES + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingListIterator.java b/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingListIterator.java new file mode 100644 index 0000000..5d29952 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingListIterator.java @@ -0,0 +1,70 @@ +package org.codefx.libfx.collection.transform; + +import java.util.ListIterator; + +/** + * Abstract superclass to {@link ListIterator}s which wrap another list iterator and transform elements from an inner + * type {@code I} to an outer type {@code O} and vice versa. + * + * @param + * the inner type, i.e. the type of the elements returned by the wrapped/inner iterator + * @param + * the outer type, i.e. the type of elements returned by this iterator + */ +abstract class AbstractTransformingListIterator extends AbstractTransformingIterator implements + ListIterator { + + // #begin IMPLEMENTATION OF 'ListIterator' + + @Override + public boolean hasPrevious() { + return getInnerIterator().hasPrevious(); + } + + @Override + public int nextIndex() { + return getInnerIterator().nextIndex(); + } + + @Override + public int previousIndex() { + return getInnerIterator().previousIndex(); + } + + @Override + public O previous() { + I previousElement = getInnerIterator().previous(); + return transformToOuter(previousElement); + } + + @Override + public void add(O element) { + I innerElement = transformToInner(element); + getInnerIterator().add(innerElement); + } + + @Override + public void set(O element) { + I innerElement = transformToInner(element); + getInnerIterator().set(innerElement); + } + + // #end IMPLEMENTATION OF 'ListIterator' + + // #begin ABSTRACT METHODS + + @Override + protected abstract ListIterator getInnerIterator(); + + /** + * Transforms an element from the outer type {@code O} to the inner type {@code I}. + * + * @param outerElement + * an element specified as an argument to a method call + * @return an equivalent element of type {@code I} + */ + protected abstract I transformToInner(O outerElement); + + // #end ABSTRACT METHODS + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingMap.java b/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingMap.java new file mode 100644 index 0000000..cf84b88 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingMap.java @@ -0,0 +1,746 @@ +package org.codefx.libfx.collection.transform; + +import java.util.AbstractMap.SimpleEntry; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Abstract superclass to {@link Map}s which transform another map. + *

+ * This class allows null keys and values. Subclasses might override that by implementing aggressive null checks. + * + * @param + * the inner key type, i.e. the type of the keys contained in the wrapped/inner map + * @param + * the outer key type, i.e. the type of keys appearing to be in this map + * @param + * the inner value type, i.e. the type of the values contained in the wrapped/inner map + * @param + * the outer value type, i.e. the type of values appearing to be in this map + */ +abstract class AbstractTransformingMap implements Map { + + // #begin FIELDS + + private final Set outerKeys; + + private final Collection outerValues; + + private final Set> outerEntries; + + // #end FIELDS + + // #begin CONSTRUCTION + + /** + * Creates a new abstract transforming map. + */ + protected AbstractTransformingMap() { + outerKeys = new KeySetView(); + outerValues = new ValueCollectionView(); + outerEntries = new EntrySetView(); + } + + // #end CONSTRUCTION + + // #begin IMPLEMENTATION OF 'Map' + + /** + * Indicates whether the specified collection is equivalent to this one. This is the case if it is also an + * {@link AbstractTransformingMap} and wraps the same {@link #getInnerMap() innerMap}. + * + * @param otherMap + * the {@link Collection} which is compared with this one + * @return true if this and the specified collection are equivalent + */ + protected final boolean isThisMap(Map otherMap) { + if (otherMap == this) + return true; + + if (otherMap instanceof AbstractTransformingMap) { + AbstractTransformingMap otherTransformingMap = + (AbstractTransformingMap) otherMap; + boolean sameInnerMap = otherTransformingMap.getInnerMap() == getInnerMap(); + return sameInnerMap; + } + + return false; + } + + // size + + @Override + public int size() { + return getInnerMap().size(); + } + + @Override + public boolean isEmpty() { + return getInnerMap().isEmpty(); + } + + // contains + + @Override + public boolean containsKey(Object key) { + if (isOuterKey(key)) { + @SuppressWarnings("unchecked") + /* + * This cast can not fail due to erasure but the following call to 'transformToInnerKey' might. In that case + * a 'ClassCastException' will be thrown which is in accordance with the contract of 'containsKey'. If + * 'isOuterKey' does its job well (which can be hard due to erasure) this will not happen. + */ + OK outerKey = (OK) key; + return getInnerMap().containsKey( + transformToInnerKey(outerKey)); + } else + return false; + } + + @Override + public boolean containsValue(Object value) { + if (isOuterValue(value)) { + @SuppressWarnings("unchecked") + /* + * This cast can not fail due to erasure but the following call to 'transformToInnerValue' might. In that + * case a 'ClassCastException' will be thrown which is in accordance with the contract of 'containsValue'. + * If 'isOuterValue' does its job well (which can be hard due to erasure) this will not happen. + */ + OV outerValue = (OV) value; + return getInnerMap().containsValue( + transformToInnerValue(outerValue)); + } else + return false; + } + + // get + + @Override + public OV get(Object key) { + if (isOuterKey(key)) { + @SuppressWarnings("unchecked") + /* + * This cast can not fail due to erasure but the following call to 'transformToInnerKey' might. In that case + * a 'ClassCastException' will be thrown which is in accordance with the contract of 'get'. If 'isOuterKey' + * does its job well (which can be hard due to erasure) this will not happen. + */ + OK outerKey = (OK) key; + return transformToOuterValue(getInnerMap().get( + transformToInnerKey(outerKey))); + } else + return null; + } + + @Override + public OV getOrDefault(Object key, OV defaultValue) { + if (isOuterKey(key)) { + @SuppressWarnings("unchecked") + /* + * This cast can not fail due to erasure but the following call to 'transformToInnerKey' might. In that case + * a 'ClassCastException' will be thrown which is in accordance with the contract of 'get'. If 'isOuterKey' + * does its job well (which can be hard due to erasure) this will not happen. + */ + OK outerKey = (OK) key; + return transformToOuterValue(getInnerMap().getOrDefault( + transformToInnerKey(outerKey), + transformToInnerValue(defaultValue) + )); + } else + return defaultValue; + } + + // put / compute / merge / replace + + @Override + public OV put(OK key, OV value) { + return transformToOuterValue(getInnerMap().put( + transformToInnerKey(key), + transformToInnerValue(value))); + } + + @Override + public OV putIfAbsent(OK key, OV value) { + return transformToOuterValue(getInnerMap().putIfAbsent( + transformToInnerKey(key), + transformToInnerValue(value))); + } + + @Override + public void putAll(Map outerMap) { + Objects.requireNonNull(outerMap, "The argument 'outerMap' must not be null."); + + Map asInner = new TransformToReadOnlyInnerMap(outerMap); + getInnerMap().putAll(asInner); + } + + @Override + public OV compute(OK key, BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction, "The argument 'remappingFunction' must not be null."); + + return transformToOuterValue(getInnerMap().compute( + transformToInnerKey(key), + transformToInnerKeyValueToValueFunction(remappingFunction) + )); + } + + @Override + public OV computeIfAbsent(OK key, Function mappingFunction) { + Objects.requireNonNull(mappingFunction, "The argument 'mappingFunction' must not be null."); + + return transformToOuterValue(getInnerMap().computeIfAbsent( + transformToInnerKey(key), + transformToInnerToKeyValueFunction(mappingFunction) + )); + } + + @Override + public OV computeIfPresent(OK key, BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction, "The argument 'remappingFunction' must not be null."); + + return transformToOuterValue(getInnerMap().computeIfPresent( + transformToInnerKey(key), + transformToInnerKeyValueToValueFunction(remappingFunction) + )); + } + + @Override + public OV merge(OK key, OV value, BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction, "The argument 'remappingFunction' must not be null."); + + return transformToOuterValue(getInnerMap().merge( + transformToInnerKey(key), + transformToInnerValue(value), + transformToInnerValueValueToValueFunction(remappingFunction) + )); + } + + @Override + public OV replace(OK key, OV value) { + return transformToOuterValue(getInnerMap().replace( + transformToInnerKey(key), + transformToInnerValue(value))); + } + + @Override + public boolean replace(OK key, OV oldValue, OV newValue) { + return getInnerMap().replace( + transformToInnerKey(key), + transformToInnerValue(oldValue), + transformToInnerValue(newValue) + ); + } + + @Override + public void replaceAll(BiFunction function) { + Objects.requireNonNull(function, "The argument 'function' must not be null."); + + getInnerMap().replaceAll(transformToInnerKeyValueToValueFunction(function)); + } + + // remove + + @Override + public OV remove(Object key) { + if (isOuterKey(key)) { + @SuppressWarnings("unchecked") + /* + * This cast can not fail due to erasure but the following call to 'transformToInnerKey' might. In that case + * a 'ClassCastException' will be thrown which is in accordance with the contract of 'remove'. If + * 'isOuterKey' does its job well (which can be hard due to erasure) this will not happen. + */ + OK outerKey = (OK) key; + return transformToOuterValue(getInnerMap().remove( + transformToInnerKey(outerKey))); + } else + return null; + } + + @Override + public boolean remove(Object key, Object value) { + if (isOuterKey(key) && isOuterValue(value)) { + /* + * These casts can not fail due to erasure but the following calls to 'transformToInner...' might. In that + * case a 'ClassCastException' will be thrown which is in accordance with the contract of 'remove'. If + * 'isOuter...' does its job well (which can be hard due to erasure) this will not happen. + */ + @SuppressWarnings("unchecked") + OK outerKey = (OK) key; + @SuppressWarnings("unchecked") + OV outerValue = (OV) value; + return getInnerMap().remove( + transformToInnerKey(outerKey), + transformToInnerValue(outerValue) + ); + } else + return false; + } + + @Override + public void clear() { + getInnerMap().clear(); + } + + // process + + @Override + public void forEach(BiConsumer action) { + Objects.requireNonNull(action, "The argument 'action' must not be null."); + + getInnerMap().forEach(transformToInnerKeyValueConsumer(action)); + } + + // views + + @Override + public Set keySet() { + return outerKeys; + } + + @Override + public Collection values() { + return outerValues; + } + + @Override + public Set> entrySet() { + return outerEntries; + } + + // function transformation + + private Function transformToInnerToKeyValueFunction( + Function function) { + + return innerKey -> transformToInnerValue(function.apply(transformToOuterKey(innerKey))); + } + + private BiFunction transformToInnerKeyValueToValueFunction( + BiFunction function) { + + return (innerKey, innerValue) -> transformToInnerValue(function.apply( + transformToOuterKey(innerKey), + transformToOuterValue(innerValue))); + } + + private BiFunction transformToInnerValueValueToValueFunction( + BiFunction function) { + + return (innerValue1, innerValue2) -> transformToInnerValue(function.apply( + transformToOuterValue(innerValue1), + transformToOuterValue(innerValue2))); + } + + private BiConsumer transformToInnerKeyValueConsumer( + BiConsumer consumer) { + + return (innerKey, innerValue) -> consumer.accept( + transformToOuterKey(innerKey), + transformToOuterValue(innerValue)); + } + + // #end IMPLEMENTATION OF 'Map' + + // #begin OBJECT + + @Override + public final boolean equals(Object object) { + if (object == this) + return true; + if (!(object instanceof Map)) + return false; + + Map other = (Map) object; + if (isThisMap(other)) + return true; + + return outerEntries.equals(other.entrySet()); + } + + @Override + public final int hashCode() { + return outerEntries.hashCode(); + } + + @Override + public String toString() { + return outerEntries + .stream() + .map(Objects::toString) + .collect(Collectors.joining(", ", "{", "}")); + } + + // #end OBJECT + + // #begin ABSTRACT METHODS + + /** + * @return the inner map wrapped by this transforming map + */ + protected abstract Map getInnerMap(); + + /** + * Checks whether the specified object might be an inner key. + *

+ * This method does not have to be exact (which might be impossible due to involved generic types) and might produce + * false positives (but no false negatives). + * + * @param object + * the object to check; may be null + * @return true if the object might be an inner key + */ + protected abstract boolean isInnerKey(Object object); + + /** + * Transforms the specified key to an instance of the outer key type. + *

+ * It can not be guaranteed that the specified key is really of the inner key type. If not, an exception can be + * thrown. + * + * @param innerKey + * the key to transform; may be null + * @return the transformed key + * @throws ClassCastException + * if the specified key is not of the correct type + */ + protected abstract OK transformToOuterKey(IK innerKey) throws ClassCastException; + + /** + * Checks whether the specified object might be an outer key. + *

+ * This method does not have to be exact (which might be impossible due to involved generic types) and might produce + * false positives (but no false negatives). + * + * @param object + * the object to check; may be null + * @return true if the object might be an outer key + */ + protected abstract boolean isOuterKey(Object object); + + /** + * Transforms the specified key to an instance of the inner key type. + *

+ * It can not be guaranteed that the specified key is really of the outer key type. If not, an exception can be + * thrown. + * + * @param outerKey + * the key to transform; may be null + * @return the transformed key + * @throws ClassCastException + * if the specified key is not of the correct type + */ + protected abstract IK transformToInnerKey(OK outerKey) throws ClassCastException; + + /** + * Checks whether the specified object might be an inner value. + *

+ * This method does not have to be exact (which might be impossible due to involved generic types) and might produce + * false positives (but no false negatives). + * + * @param object + * the object to check; may be null + * @return true if the object might be an inner value + */ + protected abstract boolean isInnerValue(Object object); + + /** + * Transforms the specified value to an instance of the outer value type. + *

+ * It can not be guaranteed that the specified value is really of the inner value type. If not, an exception can be + * thrown. + * + * @param innerValue + * the value to transform; may be null + * @return the transformed value + * @throws ClassCastException + * if the specified value is not of the correct type + */ + protected abstract OV transformToOuterValue(IV innerValue) throws ClassCastException; + + /** + * Checks whether the specified object might be an outer value. + *

+ * This method does not have to be exact (which might be impossible due to involved generic types) and might produce + * false positives (but no false negatives). + * + * @param object + * the object to check; may be null + * @return true if the object might be an outer value + */ + protected abstract boolean isOuterValue(Object object); + + /** + * Transforms the specified value to an instance of the inner value type. + *

+ * It can not be guaranteed that the specified value is really of the outer value type. If not, an exception can be + * thrown. + * + * @param outerValue + * the value to transform; may be null + * @return the transformed value + * @throws ClassCastException + * if the specified value is not of the correct type + */ + protected abstract IV transformToInnerValue(OV outerValue) throws ClassCastException; + + // #end ABSTRACT METHODS + + // #begin INNER CLASSES + + /** + * The view on this map's key set. + *

+ * This view is a {@link TransformingSet} on the inner map's key set. + */ + private class KeySetView extends AbstractTransformingSet { + + @Override + protected Set getInnerSet() { + return getInnerMap().keySet(); + } + + @Override + protected boolean isInnerElement(Object object) { + return isInnerKey(object); + } + + @Override + protected OK transformToOuter(IK innerElement) { + return transformToOuterKey(innerElement); + } + + @Override + protected boolean isOuterElement(Object object) { + return isOuterKey(object); + } + + @Override + protected IK transformToInner(OK outerElement) { + return transformToInnerKey(outerElement); + } + + // prevent adding elements according to the contract of 'Map.keySet()' + + @Override + public boolean add(OK element) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection otherCollection) { + throw new UnsupportedOperationException(); + } + + } + + /** + * The view on this map's values. + *

+ * This view is a {@link TransformingCollection} on the inner map's values. + */ + private class ValueCollectionView extends AbstractTransformingCollection { + + @Override + protected Collection getInnerCollection() { + return getInnerMap().values(); + } + + @Override + protected boolean isInnerElement(Object object) { + return isInnerValue(object); + } + + @Override + protected OV transformToOuter(IV innerElement) { + return transformToOuterValue(innerElement); + } + + @Override + protected boolean isOuterElement(Object object) { + return isOuterValue(object); + } + + @Override + protected IV transformToInner(OV outerElement) { + return transformToInnerValue(outerElement); + } + + // prevent adding elements according to the contract of 'Map.values()' + + @Override + public boolean add(OV element) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection otherCollection) { + throw new UnsupportedOperationException(); + } + + // object + + @Override + public boolean equals(Object object) { + if (object == this) + return true; + if (!(object instanceof Collection)) + return false; + + Collection other = (Collection) object; + if (isThisCollection(other)) + return true; + + return other.containsAll(this) && this.containsAll(other); + } + + @Override + public int hashCode() { + int hashCode = 1; + for (OV outerElement : this) + hashCode = 31 * hashCode + (outerElement == null ? 0 : outerElement.hashCode()); + return hashCode; + } + + } + + /** + * The view on this map's entry set. + *

+ * This view is a {@link TransformingSet} on the inner map's entry set. + */ + private class EntrySetView extends AbstractTransformingSet, Entry> { + + @Override + protected Set> getInnerSet() { + return getInnerMap().entrySet(); + } + + @Override + protected boolean isInnerElement(Object object) { + if (!(object instanceof Entry)) + // this also returns 'false' if object is 'null'; + // that is correct because an entrySet can not contain null values + return false; + + Entry entry = (Entry) object; + return isInnerKey(entry.getKey()) && isInnerValue(entry.getValue()); + } + + @Override + protected Entry transformToOuter(Entry innerElement) { + // the entry view is based on an inner view, which should never contain null + Objects.requireNonNull(innerElement, "The argument 'innerElement' must not be null."); + + OK outerKey = transformToOuterKey(innerElement.getKey()); + OV outerValue = transformToOuterValue(innerElement.getValue()); + return new SimpleEntry<>(outerKey, outerValue); + } + + @Override + protected boolean isOuterElement(Object object) { + if (!(object instanceof Entry)) + // this also returns 'false' if object is 'null'; + // that is correct because an entrySet can not contain null values + return false; + + Entry entry = (Entry) object; + return isOuterKey(entry.getKey()) && isOuterValue(entry.getValue()); + } + + @Override + protected Entry transformToInner(Map.Entry outerElement) { + // someone might hand null to a method of this view (e.g. 'contains'); + // since there can never be null values in an entry view of a map, this mapping can be fixed to null -> null; + // the inner map's entry view will handle this case correctly + if (outerElement == null) + return null; + + IK innerKey = transformToInnerKey(outerElement.getKey()); + IV innerValue = transformToInnerValue(outerElement.getValue()); + return new SimpleEntry<>(innerKey, innerValue); + } + + // prevent adding elements according to the contract of 'Map.entrySet()' + + @Override + public boolean add(Entry element) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection> otherCollection) { + throw new UnsupportedOperationException(); + } + + } + + private class TransformToReadOnlyInnerMap extends AbstractReadOnlyTransformingMap { + + private final Map transformedMap; + + public TransformToReadOnlyInnerMap(Map transformedMap) { + this.transformedMap = transformedMap; + } + + // abstract methods + + @Override + protected Map getInnerMap() { + @SuppressWarnings("unchecked") + /* + * This cast is not safe! But since this class only allows reading operations, it can not cause trouble. + */ + Map unsafelyTypedMap = (Map) transformedMap; + return unsafelyTypedMap; + } + + @Override + protected boolean isInnerKey(Object object) { + return AbstractTransformingMap.this.isOuterKey(object); + } + + @Override + protected IK transformToOuterKey(OK innerKey) { + return AbstractTransformingMap.this.transformToInnerKey(innerKey); + } + + @Override + protected boolean isOuterKey(Object object) { + return AbstractTransformingMap.this.isInnerKey(object); + } + + @Override + protected OK transformToInnerKey(IK outerKey) { + return AbstractTransformingMap.this.transformToOuterKey(outerKey); + } + + @Override + protected boolean isInnerValue(Object object) { + return AbstractTransformingMap.this.isOuterValue(object); + } + + @Override + protected IV transformToOuterValue(OV innerValue) { + return AbstractTransformingMap.this.transformToInnerValue(innerValue); + } + + @Override + protected boolean isOuterValue(Object object) { + return AbstractTransformingMap.this.isInnerValue(object); + } + + @Override + protected OV transformToInnerValue(IV outerValue) { + return AbstractTransformingMap.this.transformToOuterValue(outerValue); + } + + } + + // #end INNER CLASSES + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingSet.java b/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingSet.java new file mode 100644 index 0000000..3841e5c --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingSet.java @@ -0,0 +1,64 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Collection; +import java.util.Set; + +/** + * Abstract superclass to {@link Set}s which transform wrap another collection. + *

+ * This class allows null elements. Subclasses might override that by implementing aggressive null checks. + * + * @param + * the inner type, i.e. the type of the elements contained in the wrapped/inner set + * @param + * the outer type, i.e. the type of elements appearing to be in this set + * @see AbstractTransformingCollection + */ +abstract class AbstractTransformingSet extends AbstractTransformingCollection implements Set { + + @Override + protected final Collection getInnerCollection() { + return getInnerSet(); + } + + /** + * @return the inner set wrapped by this transforming set + */ + protected abstract Set getInnerSet(); + + // #begin OBJECT + + @Override + public final boolean equals(Object object) { + if (object == this) + return true; + if (!(object instanceof Set)) + return false; + + Set other = (Set) object; + if (isThisCollection(other)) + return true; + + if (other.size() != size()) + return false; + try { + return containsAll(other); + } catch (ClassCastException unused) { + return false; + } catch (NullPointerException unused) { + return false; + } + } + + @Override + public final int hashCode() { + int hashCode = 0; + for (O outerElement : this) + if (outerElement != null) + hashCode += outerElement.hashCode(); + return hashCode; + } + + // #end OBJECT + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingSpliterator.java b/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingSpliterator.java new file mode 100644 index 0000000..ab38080 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/AbstractTransformingSpliterator.java @@ -0,0 +1,134 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Comparator; +import java.util.Spliterator; +import java.util.function.Consumer; + +/** + * Abstract superclass to {@link Spliterator}s which wrap another spliterator and transform the returned elements from + * their inner type {@code I} to an outer type {@code O}. + *

+ * Note that this spliterator reports the exact same {@link Spliterator#SORTED SORTED} {@link #characteristics() + * characteristic} as the inner one. It's {@link #getComparator()} transforms the elements it should compare from the + * outer to the inner type and calls the inner spliterator's {@link Spliterator#getComparator() comparator} with it. + * This means that sorting streams is always done by the inner spliterator's logic. + * + * @param + * the inner type, i.e. the type of the elements returned by the wrapped/inner spliterator + * @param + * the outer type, i.e. the type of elements returned by this spliterator + */ +abstract class AbstractTransformingSpliterator implements Spliterator { + + // #begin IMPLEMENTATION OF 'Spliterator' + + @Override + public boolean tryAdvance(Consumer action) { + Consumer transformThenAction = transformThen(action); + return getInnerSpliterator().tryAdvance(transformThenAction); + } + + @Override + public void forEachRemaining(Consumer action) { + Consumer transformThenAction = transformThen(action); + getInnerSpliterator().forEachRemaining(transformThenAction); + } + + @Override + public Spliterator trySplit() { + Spliterator newSpliterator = getInnerSpliterator().trySplit(); + if (newSpliterator == null) + return null; + else + return wrapNewSpliterator(newSpliterator); + } + + @Override + public long estimateSize() { + return getInnerSpliterator().estimateSize(); + } + + @Override + public long getExactSizeIfKnown() { + return getInnerSpliterator().getExactSizeIfKnown(); + } + + @Override + public int characteristics() { + return getInnerSpliterator().characteristics(); + } + + @Override + public boolean hasCharacteristics(int characteristics) { + return getInnerSpliterator().hasCharacteristics(characteristics); + } + + @Override + public Comparator getComparator() { + Comparator innerComparator = getInnerSpliterator().getComparator(); + if (innerComparator == null) + return null; + + return (leftOuter, rightOuter) -> { + I leftInner = transformToInner(leftOuter); + I rightInner = transformToInner(rightOuter); + return innerComparator.compare(leftInner, rightInner); + }; + } + + // #end IMPLEMENTATION OF 'Spliterator' + + // #begin ABSTRACT METHODS + + /** + * @return the wrapped/inner spliterator + */ + protected abstract Spliterator getInnerSpliterator(); + + /** + * Transforms an element from the inner type {@code I} to the outer type {@code O}. + * + * @param innerElement + * an element returned by the {@link #getInnerSpliterator() innerSpliterator} + * @return an equivalent element of type {@code O} + */ + protected abstract O transformToOuter(I innerElement); + + /** + * Transforms an element from the outer type {@code O} to the inner type {@code I}. + * + * @param outerElement + * an element of type {@code O} + * @return an equivalent element of type {@code I} + */ + protected abstract I transformToInner(O outerElement); + + /** + * Transforms the specified element of type {@code I} with {@link #transformToOuter(Object) transformToOuter} before + * passing it to the specified consumer. + * + * @param action + * the {@link Consumer} of outer elements to which the transformed element will be passed + * @return a {@link Consumer} of inner elements + */ + private Consumer transformThen(Consumer action) { + return innerElement -> { + O asOuterElement = transformToOuter(innerElement); + action.accept(asOuterElement); + }; + } + + /** + * Wraps the specified spliterator over {@code I} into a spliterator over {@code O}. + *

+ * This method is called inside {@link #trySplit()}. It is not called with null. + * + * @param newSpliterator + * the newly created inner {@link Spliterator Spliterator<I>} + * @return a {@link Spliterator Spliterator<O>} + */ + protected abstract Spliterator wrapNewSpliterator(Spliterator newSpliterator); + + // #end ABSTRACT METHODS + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/EqHash.java b/src/main/java/org/codefx/libfx/collection/transform/EqHash.java new file mode 100644 index 0000000..c9b3113 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/EqHash.java @@ -0,0 +1,86 @@ +package org.codefx.libfx.collection.transform; + +import java.util.function.BiPredicate; +import java.util.function.ToIntFunction; + +/** + * Wraps elements which are pout into an inner hashing data structure and delegates {@link #equals(Object)} and + * {@link #hashCode()} to functions specified during construction. + * + * @param + * the type of the wrapped elements + */ +class EqHash { + + /** + * The default hash code used for null keys. + *

+ * This value is mentioned in the comments of {@link EqualityTransformingMap} and {@link EqualityTransformingSet}. + * Update on change. + */ + public static final int NULL_KEY_HASH_CODE = 0; + + private final E element; + private final BiPredicate equals; + private final ToIntFunction hash; + + private EqHash(E element, BiPredicate equals, ToIntFunction hash) { + // null is allowed as an element + assert equals != null : "The argument 'equals' must not be null."; + assert hash != null : "The argument 'hash' must not be null."; + + this.element = element; + this.equals = equals; + this.hash = hash; + } + + /** + * @param + * the type of the wrapped elements + * @param element + * the wrapped element; may be null + * @param equals + * the function computing equality of two elements + * @param hash + * the function computing the hash code of the element + * @return an instance of {@link EqHash} + */ + public static EqHash create( + E element, BiPredicate equals, ToIntFunction hash) { + return new EqHash(element, equals, hash); + } + + /** + * @return the wrapped element + */ + public E getElement() { + return element; + } + + @Override + public int hashCode() { + return hash.applyAsInt(element); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof EqHash)) + return false; + + @SuppressWarnings("unchecked") + // This cast is ok because no instance of EqHash can ever leave the inner map (without being transformed + // by the equality transforming map). + // If it can not leave it can not end up in an equality test in another map. + EqHash other = (EqHash) obj; + return equals.test(this.element, other.element); + } + + @Override + public String toString() { + return "EqHash [" + element + "]"; + } +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/EqualityTransformingCollectionBuilder.java b/src/main/java/org/codefx/libfx/collection/transform/EqualityTransformingCollectionBuilder.java new file mode 100644 index 0000000..4da70a2 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/EqualityTransformingCollectionBuilder.java @@ -0,0 +1,195 @@ +package org.codefx.libfx.collection.transform; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.ToIntFunction; + +/** + * Builds {@link EqualityTransformingSet}s and {@link EqualityTransformingMap}s. + *

+ * (For simplification the comments only talk about sets but unless otherwise noted the same applies to maps.) + *

+ * The implementations of {@code equals} and {@code hashCode} are provided as functions to the builder. They must of + * course fulfill the general contract between those two methods (see {@link Object#hashCode() here}). The functions can + * be provided on two ways: + *

    + *
  1. Via the {@code with[Equals|Hash]}-methods. In this case, the functions will never be called with null instances, + * which are handled by this map as follows: + *
      + *
    • {@code hashCode(null) == 0} + *
    • {@code equals(null, null) == true}; + *
    • {@code equals(not-null, null) == false} + *
    • {@code equals(null, not-null) == false} + *
    + *
  2. If those defaults are not sufficient, the functions can handle null themselves. Those variants can be provided + * via the {@code with[Equals|Hash]HandlingNull}-methods. + *
+ *

+ * The same builder instance can be reused to create multiple instances. The builder is not thread-safe and should not + * be used concurrently. + * + * @param + * the type of elements maintained by the created set or keys by the created map. + */ +public class EqualityTransformingCollectionBuilder { + + private final Class outerKeyTypeToken; + private BiPredicate equals; + private ToIntFunction hash; + + // #begin CONSTRUCTION + + private EqualityTransformingCollectionBuilder(Class outerKeyTypeToken) { + this.outerKeyTypeToken = outerKeyTypeToken; + // note that the methods from 'Objects' already implement the contract for null-safety + // imposed by the transforming set and map + this.equals = Objects::equals; + this.hash = Objects::hashCode; + } + + /** + * Returns a new builder for the specified element type. + *

+ * If a type token for the elements can not be provided, call {@link #forTypeUnknown()} instead. + * + * @param + * the type of elements contained in the created set + * @param keyTypeToken + * a type token for the elements contained in the created set + * @return a new builder + */ + public static EqualityTransformingCollectionBuilder forType(Class keyTypeToken) { + Objects.requireNonNull(keyTypeToken, "The argument 'keyTypeToken' must not be null."); + return new EqualityTransformingCollectionBuilder<>(keyTypeToken); + } + + /** + * Returns a new builder for an unknown key type. + *

+ * This is equivalent to calling {@link #forType(Class) forKeyType(Object.class)}. To obtain a builder for + * {@code } you will have to call {@code EqualityTransformingCollectionBuilder. forTypeUnknown()}. + * + * @param + * the type of elements contained in the set created by the builder + * @return a new builder + */ + public static EqualityTransformingCollectionBuilder forTypeUnknown() { + return new EqualityTransformingCollectionBuilder<>(Object.class); + } + + // #end CONSTRUCTION + + // #begin SET PROPERTIES + + /** + * @param equals + * a function determining equality of elements; might be called with null elements + * @return this builder + */ + public EqualityTransformingCollectionBuilder withEqualsHandlingNull(BiPredicate equals) { + Objects.requireNonNull(equals, "The argument 'equals' must not be null."); + this.equals = equals; + return this; + } + + /** + * @param equals + * a function determining equality of elements; will not be called with null elements + * @return this builder + */ + public EqualityTransformingCollectionBuilder withEquals(BiPredicate equals) { + Objects.requireNonNull(equals, "The argument 'equals' must not be null."); + return withEqualsHandlingNull(makeNullSafe(equals)); + } + + private static BiPredicate makeNullSafe(BiPredicate equals) { + return (outerKey1, outerKey2) -> { + if (outerKey1 == null && outerKey2 == null) + return true; + if (outerKey1 == null || outerKey2 == null) + return false; + + return equals.test(outerKey1, outerKey2); + }; + } + + /** + * @param hash + * a function computing the hash code of an element; might be called with null elements + * @return this builder + */ + public EqualityTransformingCollectionBuilder withHashHandlingNull(ToIntFunction hash) { + Objects.requireNonNull(hash, "The argument 'hash' must not be null."); + this.hash = hash; + return this; + } + + /** + * @param hash + * a function computing the hash code of an element; will not be called with null elements + * @return this builder + */ + public EqualityTransformingCollectionBuilder withHash(ToIntFunction hash) { + Objects.requireNonNull(hash, "The argument 'hash' must not be null."); + return withHashHandlingNull(makeNullSafe(hash)); + } + + private static ToIntFunction makeNullSafe(ToIntFunction hash) { + return outerKey -> outerKey == null ? EqHash.NULL_KEY_HASH_CODE : hash.applyAsInt(outerKey); + } + + // #end SET PROPERTIES + + // #begin BUILD + + /** + * Creates a new {@link EqualityTransformingSet} by decorating a {@link HashSet}. + * + * @return a new instance of {@link EqualityTransformingSet} + */ + public EqualityTransformingSet buildSet() { + return new EqualityTransformingSet<>(new HashSet<>(), outerKeyTypeToken, equals, hash); + } + + /** + * Creates a new {@link EqualityTransformingSet} by decorating the specified set. + * + * @param emptySet + * an empty set which is not otherwise referenced + * @return a new instance of {@link EqualityTransformingSet} + */ + public EqualityTransformingSet buildSet(Set emptySet) { + return new EqualityTransformingSet<>(emptySet, outerKeyTypeToken, equals, hash); + } + + /** + * Creates a new {@link EqualityTransformingMap} by decorating a {@link HashMap}. + * + * @param + * the type of values mapped by the new map + * @return a new instance of {@link EqualityTransformingMap} + */ + public EqualityTransformingMap buildMap() { + return new EqualityTransformingMap<>(new HashMap<>(), outerKeyTypeToken, equals, hash); + } + + /** + * Creates a new {@link EqualityTransformingMap} by decorating the specified map. + * + * @param + * the type of values mapped by the new map + * @param emptyMap + * an empty map which is not otherwise referenced + * @return a new instance of {@link EqualityTransformingMap} + */ + public EqualityTransformingMap buildMap(Map emptyMap) { + return new EqualityTransformingMap<>(emptyMap, outerKeyTypeToken, equals, hash); + } + + // #end BUILD + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/EqualityTransformingMap.java b/src/main/java/org/codefx/libfx/collection/transform/EqualityTransformingMap.java new file mode 100644 index 0000000..b222888 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/EqualityTransformingMap.java @@ -0,0 +1,146 @@ +package org.codefx.libfx.collection.transform; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiPredicate; +import java.util.function.ToIntFunction; + +/** + * An equality transforming map allows to define the implementations of {@link Object#equals(Object) equals} and + * {@link Object#hashCode() hashCode} which are used for the map's keys. + *

+ * It does so by storing the entries in an inner map and providing a transforming view on them. See the + * {@link org.codefx.libfx.collection.transform package} documentation for general comments on that. Note that instances + * of {@code EqualityTransformingMap}s are created with a {@link EqualityTransformingCollectionBuilder builder}. + *

+ * This implementation mitigates the type safety problems by optionally using a token of the (outer) key type to check + * instances against them. This solves some of the critical situations but not all of them. In those other cases + * {@link ClassCastException}s might still occur. + *

+ * By default the inner map will be a new {@link HashMap} but the another map can be provided to the builder. Such + * instances must be empty and not be referenced anywhere else. The implementations of {@code equals} and + * {@code hashCode} are provided as functions to the builder - see there for details. + *

+ * The transformations used by this map preserve object identity of outer keys and values. This means if keys and values + * are added to this map, an iteration over it will return the same instances. + *

+ * {@code EqualityTransformingMap}s are created with a {@link EqualityTransformingCollectionBuilder}. + * + * @param + * the type of keys maintained by this map + * @param + * the type of mapped values + */ +public final class EqualityTransformingMap extends AbstractTransformingMap, K, V, V> { + + // #begin FIELDS + + private final Map, V> innerMap; + + private final Class outerKeyTypeToken; + + /** + * Compares two outer keys for equality. + */ + private final BiPredicate equals; + + /** + * Computes a hashCode for an outer key. + */ + private final ToIntFunction hash; + + // #end FIELDS + + // #begin CONSTRUCTION + + /** + * Creates a new transforming map. + * + * @param innerMap + * the decorated map; must be empty + * @param outerKeyTypeToken + * the token used to verify outer keys + * @param equals + * the function computing equality of keys + * @param hash + * the function computing the hash code of keys + */ + EqualityTransformingMap( + Map innerMap, + Class outerKeyTypeToken, + BiPredicate equals, + ToIntFunction hash) { + + assert innerMap != null : "The argument 'innerMap' must not be null."; + assert outerKeyTypeToken != null : "The argument 'outerKeyTypeToken' must not be null."; + assert equals != null : "The argument 'equals' must not be null."; + assert hash != null : "The argument 'hash' must not be null."; + + this.innerMap = castInnerMap(innerMap); + this.outerKeyTypeToken = outerKeyTypeToken; + this.equals = equals; + this.hash = hash; + } + + private static Map, V> castInnerMap(Map untypedInnerMap) { + @SuppressWarnings("unchecked") + // This class' contract states that the 'innerMap' must be empty and that no other + // references to it must exist. This implies that only this class can ever access or mutate it. + // Thanks to erasure its generic key and value types can hence be cast to any other type. + Map, V> innerMap = (Map, V>) untypedInnerMap; + return innerMap; + } + + // #end CONSTRUCTION + + // #begin IMPLEMENTATION OF 'AbstractTransformingMap' + + @Override + protected Map, V> getInnerMap() { + return innerMap; + } + + @Override + protected boolean isInnerKey(Object object) { + // this excludes null objects from being inner keys which is correct because even null will be wrapped in EqHash + return object instanceof EqHash; + } + + @Override + protected K transformToOuterKey(EqHash innerKey) throws ClassCastException { + return innerKey.getElement(); + } + + @Override + protected boolean isOuterKey(Object object) { + return object == null || outerKeyTypeToken.isInstance(object); + } + + @Override + protected EqHash transformToInnerKey(K outerKey) throws ClassCastException { + return EqHash.create(outerKey, equals, hash); + } + + @Override + protected boolean isInnerValue(Object object) { + return true; + } + + @Override + protected V transformToOuterValue(V innerValue) throws ClassCastException { + return innerValue; + } + + @Override + protected boolean isOuterValue(Object object) { + return true; + } + + @Override + protected V transformToInnerValue(V outerValue) throws ClassCastException { + return outerValue; + } + + // #end IMPLEMENTATION OF 'AbstractTransformingMap' + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/EqualityTransformingSet.java b/src/main/java/org/codefx/libfx/collection/transform/EqualityTransformingSet.java new file mode 100644 index 0000000..9b45c0a --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/EqualityTransformingSet.java @@ -0,0 +1,111 @@ +package org.codefx.libfx.collection.transform; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.ToIntFunction; + +/** + * An equality transforming set allows to define the implementations of {@link Object#equals(Object) equals} and + * {@link Object#hashCode() hashCode} which are used by the set. + *

+ * It does so by storing the entries in an inner set and providing a transforming view on them. See the + * {@link org.codefx.libfx.collection.transform package} documentation for general comments on that. + *

+ * This implementation mitigates the type safety problems by optionally using a token of the outer type to check + * instances against them. This solves some of the critical situations but not all of them. In those other cases + * {@link ClassCastException}s might still occur. + *

+ * By default the inner set will be a {@link HashSet} but another map can be provided to the builder. Such instances + * must be empty and not be referenced anywhere else. The implementations of {@code equals} and {@code hashCode} are + * provided as functions to the builder - see there for details. + *

+ * The transformations used by this set preserve object identity of outer values. This means if values are added to this + * set, an iteration over it will return the same instances. + *

+ * {@code EqualityTransformingSet}s are created with a {@link EqualityTransformingCollectionBuilder}. + * + * @param + * the type of elements in this set + */ +public class EqualityTransformingSet extends AbstractTransformingSet, E> { + + // #begin FIELDS + + private final Set> innerSet; + + private final Class outerTypeToken; + + /** + * Compares two outer elements for equality. + */ + private final BiPredicate equals; + + /** + * Computes a hashCode for an outer element. + */ + private final ToIntFunction hash; + + // #end FIELDS + + // #begin CONSTRUCTION + + /** + * Creates a new transforming set. + * + * @param innerSet + * the decorated set; must be empty + * @param outerTypeToken + * the token used to verify outer elements + * @param equals + * the function computing equality of elements + * @param hash + * the function computing the hash code of elements + */ + EqualityTransformingSet( + Set innerSet, Class outerTypeToken, + BiPredicate equals, ToIntFunction hash) { + this.innerSet = castInnerSet(innerSet); + this.outerTypeToken = outerTypeToken; + this.equals = equals; + this.hash = hash; + } + + private static Set> castInnerSet(Set untypedInnerSet) { + @SuppressWarnings("unchecked") + // This class' contract states that the 'innerSet' must be empty and that no other + // references to it must exist. This implies that only this class can ever access or mutate it. + // Thanks to erasure its generic element type can hence be cast to any other type. + Set> innerMap = (Set>) untypedInnerSet; + return innerMap; + } + + // #end CONSTRUCTION + + @Override + protected Set> getInnerSet() { + return innerSet; + } + + @Override + protected boolean isInnerElement(Object object) { + // this excludes null objects from being inner element which is correct because even null will be wrapped in EqHash + return object instanceof EqHash; + } + + @Override + protected E transformToOuter(EqHash innerElement) throws ClassCastException { + return innerElement.getElement(); + } + + @Override + protected boolean isOuterElement(Object object) { + return object == null || outerTypeToken.isInstance(object); + } + + @Override + protected EqHash transformToInner(E outerElement) throws ClassCastException { + return EqHash.create(outerElement, equals, hash); + } + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/OptionalTransformingCollection.java b/src/main/java/org/codefx/libfx/collection/transform/OptionalTransformingCollection.java new file mode 100644 index 0000000..696b069 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/OptionalTransformingCollection.java @@ -0,0 +1,191 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Collection; +import java.util.Objects; +import java.util.Optional; + +/** + * A transforming {@link Collection} which is a flattened view on a collection of {@link Optional}s, i.e. it only + * presents the contained values. + *

+ * See the {@link org.codefx.libfx.collection.transform package} documentation for general comments on transformation. + *

+ * The inner collection must not contain null elements. The empty {@code Optional} is mapped to an outer element + * specified during construction. If null is chosen for this, this collection will accept null elements. Otherwise it + * will reject them with a {@link NullPointerException}. + *

+ * The transformations used by this collection preserve object identity of outer elements with the exception of the + * default element. This means if (non-default) elements are added to this collection, an iteration over it will return + * the same instances. The default value instance will be used to represent the empty {@code Optional}s, so when + * elements equal to it are added, they will be retrieved as that instance (thus loosing their identity). + *

+ * This implementation mitigates the type safety problems by using type tokens. {@code Optional.class} is used as the + * inner type token. The outer type token can be specified during construction. This solves some of the critical + * situations but not all of them. In those other cases (e.g. if {@link #containsAll(Collection) containsAll} is called + * with a {@code Collection>}) {@link ClassCastException}s might occur. + *

+ * All method calls (of abstract and default methods existing in JDK 8) are forwarded to the same method on the + * wrapped collection. This implies that all guarantees made by such methods (e.g. regarding atomicity) are upheld by + * the transformation. + * + * @param + * the type of elements contained in the {@code Optional}s + */ +public final class OptionalTransformingCollection extends AbstractTransformingCollection, E> { + + // #begin FIELDS + + private final Collection> innerCollection; + private final Class outerTypeToken; + + /** + * The outer element used to represent {@link Optional#empty()}. + */ + private final E outerDefaultElement; + + // #end FIELDS + + // #begin CONSTRUCTION + + /** + * Creates a new transforming collection which uses a type token to identify the outer elements. + * + * @param innerCollection + * the wrapped collection + * @param outerTypeToken + * the token for the outer type + * @param outerDefaultElement + * the element used to represent {@link Optional#empty()}; can be null; it is of crucial importance that + * this element does not occur inside a non-empty optional because then the transformations from that + * optional to an element and back are not inverse, which will cause unexpected behavior + */ + public OptionalTransformingCollection( + Collection> innerCollection, + Class outerTypeToken, + E outerDefaultElement) { + Objects.requireNonNull(innerCollection, "The argument 'innerCollection' must not be null."); + Objects.requireNonNull(outerTypeToken, "The argument 'outerTypeToken' must not be null."); + // 'outerDefaultElement' can be null + + this.innerCollection = innerCollection; + this.outerTypeToken = outerTypeToken; + this.outerDefaultElement = outerDefaultElement; + } + + /** + * Creates a new transforming collection. + * + * @param innerCollection + * the wrapped collection + */ + public OptionalTransformingCollection(Collection> innerCollection) { + this(innerCollection, Object.class, null); + } + + /** + * Creates a new transforming collection which uses a type token to identify the outer elements. + * + * @param innerCollection + * the wrapped collection + * @param outerTypeToken + * the token for the outer type + */ + public OptionalTransformingCollection(Collection> innerCollection, Class outerTypeToken) { + this(innerCollection, outerTypeToken, null); + } + + /** + * Creates a new transforming collection which uses the type of the specified default element as a token to identify + * the outer elements. + * + * @param innerCollection + * the wrapped collection + * @param outerDefaultElement + * the non-null element used to represent {@link Optional#empty()}; it is of crucial importance that this + * element does not occur inside an optional because then the transformations from that optional to an + * element and back are not inverse, which will cause unexpected behavior; the instance's class will be + * used as the outer type token + */ + public OptionalTransformingCollection( + Collection> innerCollection, + E outerDefaultElement) { + this(innerCollection, getClassOfDefaultElement(outerDefaultElement), outerDefaultElement); + } + + @SuppressWarnings("unchecked") + private static Class getClassOfDefaultElement(T outerDefaultElement) { + Objects.requireNonNull(outerDefaultElement, "The argument 'outerDefaultElement' must not be null."); + return (Class) outerDefaultElement.getClass(); + } + + // #end CONSTRUCTION + + // #begin IMPLEMENTATION OF 'AbstractTransformingCollection' + + @Override + protected Collection> getInnerCollection() { + return innerCollection; + } + + @Override + protected boolean isInnerElement(Object object) { + // reject nulls unless it is the outer default element + if (outerDefaultElement != null) + Objects.requireNonNull(object, "When the outer default element is not null, this collection rejects nulls."); + + return Optional.class.isInstance(object); + } + + @Override + protected E transformToOuter(Optional innerElement) { + Objects.requireNonNull(innerElement, "No element of the inner collection can be null."); + return innerElement.orElse(outerDefaultElement); + } + + @Override + protected boolean isOuterElement(Object object) { + // the second part of the check ensures + // that 'null' is only considered an outer element if the default element is also null + return outerTypeToken.isInstance(object) || Objects.equals(object, outerDefaultElement); + } + + @Override + protected Optional transformToInner(E outerElement) { + // reject nulls unless it is the outer default element + if (outerDefaultElement != null) + Objects.requireNonNull(outerElement, "The argument 'outerElement' must not be null."); + + return Objects.equals(outerElement, outerDefaultElement) + ? Optional.empty() + : Optional.of(outerElement); + } + + // #end IMPLEMENTATION OF 'AbstractTransformingCollection' + + // #begin OBJECT + + @Override + public boolean equals(Object object) { + if (object == this) + return true; + if (!(object instanceof Collection)) + return false; + + Collection other = (Collection) object; + if (isThisCollection(other)) + return true; + + return other.containsAll(this) && this.containsAll(other); + } + + @Override + public int hashCode() { + int hashCode = 1; + for (E outerElement : this) + hashCode = 31 * hashCode + (outerElement == null ? 0 : outerElement.hashCode()); + return hashCode; + } + + // #end OBJECT + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/OptionalTransformingList.java b/src/main/java/org/codefx/libfx/collection/transform/OptionalTransformingList.java new file mode 100644 index 0000000..39fccfd --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/OptionalTransformingList.java @@ -0,0 +1,166 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * A transforming {@link List} which is a flattened view on a list of {@link Optional}s, i.e. it only presents the + * contained values. + *

+ * See the {@link org.codefx.libfx.collection.transform package} documentation for general comments on transformation. + *

+ * The inner list must not contain null elements. The empty {@code Optional} is mapped to an outer element specified + * during construction. If null is chosen for this, this list will accept null elements. Otherwise it will reject them + * with a {@link NullPointerException}. + *

+ * The transformations used by this list preserve object identity of outer elements with the exception of the default + * element. This means if (non-default) elements are added to this list, an iteration over it will return the same + * instances. The default value instance will be used to represent the empty {@code Optional}, so when elements equal to + * it are added, they will be retrieved as that instance (thus loosing their identity). + *

+ * This implementation mitigates the type safety problems by using type tokens. {@code Optional.class} is used as the + * inner type token. The outer type token can be specified during construction. This solves some of the critical + * situations but not all of them. In those other cases (e.g. if {@link #containsAll(Collection) containsAll} is called + * with a {@code Collection>}) {@link ClassCastException}s might occur. + *

+ * All method calls (of abstract and default methods existing in JDK 8) are forwarded to the same method on the + * wrapped set. This implies that all guarantees made by such methods (e.g. regarding atomicity) are upheld by the + * transformation. + * + * @param + * the type of elements contained in the {@code Optional}s + */ +public final class OptionalTransformingList extends AbstractTransformingList, E> { + + // #begin FIELDS + + private final List> innerList; + private final Class outerTypeToken; + + /** + * The outer element used to represent {@link Optional#empty()}. + */ + private final E outerDefaultElement; + + // #end FIELDS + + // #begin CONSTRUCTION + + /** + * Creates a new transforming list which uses a type token to identify the outer elements. + * + * @param innerList + * the wrapped list + * @param outerTypeToken + * the token for the outer type + * @param outerDefaultElement + * the element used to represent {@link Optional#empty()}; can be null; it is of crucial importance that + * this element does not occur inside a non-empty optional because then the transformations from that + * optional to an element and back are not inverse, which will cause unexpected behavior + */ + public OptionalTransformingList( + List> innerList, + Class outerTypeToken, + E outerDefaultElement) { + Objects.requireNonNull(innerList, "The argument 'innerList' must not be null."); + Objects.requireNonNull(outerTypeToken, "The argument 'outerTypeToken' must not be null."); + // 'outerDefaultElement' can be null + + this.innerList = innerList; + this.outerTypeToken = outerTypeToken; + this.outerDefaultElement = outerDefaultElement; + } + + /** + * Creates a new transforming list. + * + * @param innerList + * the wrapped list + */ + public OptionalTransformingList(List> innerList) { + this(innerList, Object.class, null); + } + + /** + * Creates a new transforming list which uses a type token to identify the outer elements. + * + * @param innerList + * the wrapped set + * @param outerTypeToken + * the token for the outer type + */ + public OptionalTransformingList(List> innerList, Class outerTypeToken) { + this(innerList, outerTypeToken, null); + } + + /** + * Creates a new transforming list which uses the type of the specified default element as a token to identify the + * outer elements. + * + * @param innerList + * the wrapped list + * @param outerDefaultElement + * the non-null element used to represent {@link Optional#empty()}; it is of crucial importance that this + * element does not occur inside an optional because then the transformations from that optional to an + * element and back are not inverse, which will cause unexpected behavior; the instance's class will be + * used as the outer type token + */ + public OptionalTransformingList( + List> innerList, + E outerDefaultElement) { + this(innerList, getClassOfDefaultElement(outerDefaultElement), outerDefaultElement); + } + + @SuppressWarnings("unchecked") + private static Class getClassOfDefaultElement(T outerDefaultElement) { + Objects.requireNonNull(outerDefaultElement, "The argument 'outerDefaultElement' must not be null."); + return (Class) outerDefaultElement.getClass(); + } + + // #end CONSTRUCTION + + // #begin IMPLEMENTATION OF 'AbstractTransformingList' + + @Override + protected List> getInnerList() { + return innerList; + } + + @Override + protected boolean isInnerElement(Object object) { + // reject nulls unless it is the outer default element + if (outerDefaultElement != null) + Objects.requireNonNull(object, "When the outer default element is not null, this collection rejects nulls."); + + return Optional.class.isInstance(object); + } + + @Override + protected E transformToOuter(Optional innerElement) { + Objects.requireNonNull(innerElement, "No element of the inner collection can be null."); + return innerElement.orElse(outerDefaultElement); + } + + @Override + protected boolean isOuterElement(Object object) { + // the second part of the check ensures + // that 'null' is only considered an outer element if the default element is also null + return outerTypeToken.isInstance(object) || Objects.equals(object, outerDefaultElement); + } + + @Override + protected Optional transformToInner(E outerElement) { + // reject nulls unless it is the outer default element + if (outerDefaultElement != null) + Objects.requireNonNull(outerElement, "The argument 'outerElement' must not be null."); + + return Objects.equals(outerElement, outerDefaultElement) + ? Optional.empty() + : Optional.of(outerElement); + } + + // #end IMPLEMENTATION OF 'AbstractTransformingList' + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/OptionalTransformingSet.java b/src/main/java/org/codefx/libfx/collection/transform/OptionalTransformingSet.java new file mode 100644 index 0000000..b07ffe4 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/OptionalTransformingSet.java @@ -0,0 +1,166 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Collection; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * A transforming {@link Set} which is a flattened view on a set of {@link Optional}s, i.e. it only presents the + * contained values. + *

+ * See the {@link org.codefx.libfx.collection.transform package} documentation for general comments on transformation. + *

+ * The inner set must not contain null elements. The empty {@code Optional} is mapped to an outer element specified + * during construction. If null is chosen for this, this set will accept null elements. Otherwise it will reject them + * with a {@link NullPointerException}. + *

+ * The transformations used by this set preserve object identity of outer elements with the exception of the default + * element. This means if (non-default) elements are added to this set, an iteration over it will return the same + * instances. The default value instance will be used to represent the empty {@code Optional}, so when elements equal to + * it are added, they will be retrieved as that instance (thus loosing their identity). + *

+ * This implementation mitigates the type safety problems by using type tokens. {@code Optional.class} is used as the + * inner type token. The outer type token can be specified during construction. This solves some of the critical + * situations but not all of them. In those other cases (e.g. if {@link #containsAll(Collection) containsAll} is called + * with a {@code Collection>}) {@link ClassCastException}s might occur. + *

+ * All method calls (of abstract and default methods existing in JDK 8) are forwarded to the same method on the + * wrapped set. This implies that all guarantees made by such methods (e.g. regarding atomicity) are upheld by the + * transformation. + * + * @param + * the type of elements contained in the {@code Optional}s + */ +public final class OptionalTransformingSet extends AbstractTransformingSet, E> { + + // #begin FIELDS + + private final Set> innerSet; + private final Class outerTypeToken; + + /** + * The outer element used to represent {@link Optional#empty()}. + */ + private final E outerDefaultElement; + + // #end FIELDS + + // #begin CONSTRUCTION + + /** + * Creates a new transforming set which uses a type token to identify the outer elements. + * + * @param innerSet + * the wrapped set + * @param outerTypeToken + * the token for the outer type + * @param outerDefaultElement + * the element used to represent {@link Optional#empty()}; can be null; it is of crucial importance that + * this element does not occur inside a non-empty optional because then the transformations from that + * optional to an element and back are not inverse, which will cause unexpected behavior + */ + public OptionalTransformingSet( + Set> innerSet, + Class outerTypeToken, + E outerDefaultElement) { + Objects.requireNonNull(innerSet, "The argument 'innerSet' must not be null."); + Objects.requireNonNull(outerTypeToken, "The argument 'outerTypeToken' must not be null."); + // 'outerDefaultElement' can be null + + this.innerSet = innerSet; + this.outerTypeToken = outerTypeToken; + this.outerDefaultElement = outerDefaultElement; + } + + /** + * Creates a new transforming set. + * + * @param innerSet + * the wrapped set + */ + public OptionalTransformingSet(Set> innerSet) { + this(innerSet, Object.class, null); + } + + /** + * Creates a new transforming set which uses a type token to identify the outer elements. + * + * @param innerSet + * the wrapped set + * @param outerTypeToken + * the token for the outer type + */ + public OptionalTransformingSet(Set> innerSet, Class outerTypeToken) { + this(innerSet, outerTypeToken, null); + } + + /** + * Creates a new transforming set which uses the type of the specified default element as a token to identify the + * outer elements. + * + * @param innerSet + * the wrapped set + * @param outerDefaultElement + * the non-null element used to represent {@link Optional#empty()}; it is of crucial importance that this + * element does not occur inside an optional because then the transformations from that optional to an + * element and back are not inverse, which will cause unexpected behavior; the instance's class will be + * used as the outer type token + */ + public OptionalTransformingSet( + Set> innerSet, + E outerDefaultElement) { + this(innerSet, getClassOfDefaultElement(outerDefaultElement), outerDefaultElement); + } + + @SuppressWarnings("unchecked") + private static Class getClassOfDefaultElement(T outerDefaultElement) { + Objects.requireNonNull(outerDefaultElement, "The argument 'outerDefaultElement' must not be null."); + return (Class) outerDefaultElement.getClass(); + } + + // #end CONSTRUCTION + + // #begin IMPLEMENTATION OF 'AbstractTransformingSet' + + @Override + protected Set> getInnerSet() { + return innerSet; + } + + @Override + protected boolean isInnerElement(Object object) { + // reject nulls unless it is the outer default element + if (outerDefaultElement != null) + Objects.requireNonNull(object, "When the outer default element is not null, this collection rejects nulls."); + + return Optional.class.isInstance(object); + } + + @Override + protected E transformToOuter(Optional innerElement) { + Objects.requireNonNull(innerElement, "No element of the inner collection can be null."); + return innerElement.orElse(outerDefaultElement); + } + + @Override + protected boolean isOuterElement(Object object) { + // the second part of the check ensures + // that 'null' is only considered an outer element if the default element is also null + return outerTypeToken.isInstance(object) || Objects.equals(object, outerDefaultElement); + } + + @Override + protected Optional transformToInner(E outerElement) { + // reject nulls unless it is the outer default element + if (outerDefaultElement != null) + Objects.requireNonNull(outerElement, "The argument 'outerElement' must not be null."); + + return Objects.equals(outerElement, outerDefaultElement) + ? Optional.empty() + : Optional.of(outerElement); + } + + // #end IMPLEMENTATION OF 'AbstractTransformingSet' + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/TransformingCollection.java b/src/main/java/org/codefx/libfx/collection/transform/TransformingCollection.java new file mode 100644 index 0000000..c6b088c --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/TransformingCollection.java @@ -0,0 +1,142 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Collection; +import java.util.Objects; +import java.util.function.Function; + +/** + * A {@link Collection} which decorates another collection and transforms the element type from the inner type {@code I} + * to an outer type {@code O}. + *

+ * See the {@link org.codefx.libfx.collection.transform package} documentation for general comments on transformation. + *

+ * This implementation mitigates the type safety problems by using a token of the inner and the outer type to check + * instances against them. This solves some of the critical situations but not all of them. In those other cases + * {@link ClassCastException}s might occur when an element can not be transformed by the transformation functions. + *

+ * Null elements are allowed unless the inner collection does not accept them. These are handled explicitly and fixed to + * the transformation {@code null -> null}. The transforming functions specified during construction neither have to + * handle that case nor are they allowed to produce null elements. + *

+ * If the {@link #stream() stream} returned by this collection is told to {@link java.util.stream.Stream#sorted() sort} + * itself, it will do so on the base of the comparator returned by the inner collection's spliterator (e.g. based on the + * natural order of {@code I} if it has one). + *

+ * {@code TransformingCollection}s are created with a {@link TransformingCollectionBuilder}. + * + * @param + * the inner type, i.e. the type of the elements contained in the wrapped/inner collection + * @param + * the outer type, i.e. the type of elements appearing to be in this collection + */ +public final class TransformingCollection extends AbstractTransformingCollection { + + // #begin FIELDS + + private final Collection innerCollection; + private final Class innerTypeToken; + private final Class outerTypeToken; + private final Function transformToOuter; + private final Function transformToInner; + + // #end FIELDS + + /** + * Creates a new transforming collection. + * + * @param innerCollection + * the wrapped collection + * @param innerTypeToken + * the token for the inner type + * @param outerTypeToken + * the token for the outer type + * @param transformToOuter + * transforms an element from an inner to an outer type; will never be called with null argument and must + * not produce null + * @param transformToInner + * transforms an element from an outer to an inner type; will never be called with null argument and must + * not produce null + */ + TransformingCollection( + Collection innerCollection, + Class innerTypeToken, Class outerTypeToken, + Function transformToOuter, Function transformToInner) { + + Objects.requireNonNull(innerCollection, "The argument 'innerCollection' must not be null."); + Objects.requireNonNull(innerTypeToken, "The argument 'innerTypeToken' must not be null."); + Objects.requireNonNull(outerTypeToken, "The argument 'outerTypeToken' must not be null."); + Objects.requireNonNull(transformToOuter, "The argument 'transformToOuter' must not be null."); + Objects.requireNonNull(transformToInner, "The argument 'transformToInner' must not be null."); + + this.innerCollection = innerCollection; + this.innerTypeToken = innerTypeToken; + this.outerTypeToken = outerTypeToken; + this.transformToOuter = transformToOuter; + this.transformToInner = transformToInner; + } + + // #begin IMPLEMENTATION OF 'AbstractTransformingCollection' + + @Override + protected Collection getInnerCollection() { + return innerCollection; + } + + @Override + protected boolean isInnerElement(Object object) { + return object == null || innerTypeToken.isInstance(object); + } + + @Override + protected O transformToOuter(I innerElement) { + if (innerElement == null) + return null; + + O outerElement = transformToOuter.apply(innerElement); + Objects.requireNonNull(outerElement, "The transformation must not create null instances."); + return outerElement; + } + + @Override + protected boolean isOuterElement(Object object) { + return object == null || outerTypeToken.isInstance(object); + } + + @Override + protected I transformToInner(O outerElement) { + if (outerElement == null) + return null; + + I innerElement = transformToInner.apply(outerElement); + Objects.requireNonNull(innerElement, "The transformation must not create null instances."); + return innerElement; + } + + // #end IMPLEMENTATION OF 'AbstractTransformingCollection' + + // #begin OBJECT + + @Override + public boolean equals(Object object) { + if (object == this) + return true; + if (!(object instanceof Collection)) + return false; + + Collection other = (Collection) object; + if (isThisCollection(other)) + return true; + + return other.containsAll(this) && this.containsAll(other); + } + + @Override + public int hashCode() { + int hashCode = 1; + for (O outerElement : this) + hashCode = 31 * hashCode + (outerElement == null ? 0 : outerElement.hashCode()); + return hashCode; + } + + // #end OBJECT +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/TransformingCollectionBuilder.java b/src/main/java/org/codefx/libfx/collection/transform/TransformingCollectionBuilder.java new file mode 100644 index 0000000..32b9052 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/TransformingCollectionBuilder.java @@ -0,0 +1,158 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +/** + * Builder for {@link TransformingCollection}s, {@link TransformingSet}s and {@link TransformingList}s. + *

+ * A builder can be obtained by calling {@link #forInnerAndOuterType(Class, Class) forInnerAndOuterType} or + * {@link #forInnerAndOuterTypeUnknown()}. The building methods {@code transform...} can only be called after + * transformations from inner to outer elements and vice versa have been set. + * + * @param + * the inner type of the created transforming collection, i.e. the type of the elements contained in the + * wrapped/inner collection + * @param + * the outer type of the created transforming collection, i.e. the type of elements appearing to be in the + * created collection + */ +public class TransformingCollectionBuilder { + + // #begin FIELDS + + private final Class outerTypeToken; + private final Class innerTypeToken; + + private Function transformToOuter; + private Function transformToInner; + + // #end FIELDS + + // #begin CONSTRUCTION + + private TransformingCollectionBuilder(Class innerTypeToken, Class outerTypeToken) { + Objects.requireNonNull(innerTypeToken, "The argument 'innerTypeToken' must not be null."); + Objects.requireNonNull(outerTypeToken, "The argument 'outerTypeToken' must not be null."); + + this.outerTypeToken = outerTypeToken; + this.innerTypeToken = innerTypeToken; + } + + /** + * Returns a new builder for the specified inner and outer type. + * + * @param + * the inner type of the created transforming collection, i.e. the type of the elements contained in the + * wrapped/inner collection + * @param + * the outer type of the created transforming collection, i.e. the type of elements appearing to be in + * the created collection + * @param innerTypeToken + * the token for the inner type + * @param outerTypeToken + * the token for the outer type + * @return a new builder + */ + public static TransformingCollectionBuilder forInnerAndOuterType( + Class innerTypeToken, Class outerTypeToken) { + return new TransformingCollectionBuilder<>(innerTypeToken, outerTypeToken); + } + + /** + * Returns a new builder for unknown inner and outer types. + *

+ * This is equivalent to calling {@link #forInnerAndOuterType(Class, Class) forInnerAndOuterType(Object.class, + * Object.class)}. To obtain a builder for {@code } you will have to call + * {@code TransformingCollectionBuilder. forInnerAndOuterTypeUnknown()}. + * + * @param + * the inner type of the created transforming collection, i.e. the type of the elements contained in the + * wrapped/inner collection + * @param + * the outer type of the created transforming collection, i.e. the type of elements appearing to be in + * the created collection + * @return a new builder + */ + public static TransformingCollectionBuilder forInnerAndOuterTypeUnknown() { + return forInnerAndOuterType(Object.class, Object.class); + } + + // #end CONSTRUCTION + + // #begin SET FIELDS + + /** + * Sets the transformation from inner to outer elements which will be used by the created collection. + * + * @param transformToOuter + * transforms inner to outer elements + * @return this builder + */ + public TransformingCollectionBuilder toOuter(Function transformToOuter) { + Objects.requireNonNull(transformToOuter, "The argument 'transformToOuter' must not be null."); + + this.transformToOuter = transformToOuter; + return this; + } + + /** + * Sets the transformation from outer to inner elements which will be used by the created collection. + * + * @param transformToInner + * transforms outer to inner elements + * @return this builder + */ + public TransformingCollectionBuilder toInner(Function transformToInner) { + Objects.requireNonNull(transformToInner, "The argument 'transformToInner' must not be null."); + + this.transformToInner = transformToInner; + return this; + } + + // #end SET FIELDS + + // #begin BUILD + + /** + * Creates a {@link TransformingCollection} which transforms/decorates the specified collection. + * + * @param collection + * the collection to transform; will be the inner collection of the returned transformation + * @return a new {@link TransformingCollection} + */ + public TransformingCollection transformCollection(Collection collection) { + return new TransformingCollection<>( + collection, innerTypeToken, outerTypeToken, transformToOuter, transformToInner); + } + + /** + * Creates a {@link TransformingSet} which transforms/decorates the specified set. + * + * @param set + * the set to transform; will be the inner set of the returned transformation + * @return a new {@link TransformingSet} + */ + public TransformingSet transformSet(Set set) { + return new TransformingSet<>( + set, innerTypeToken, outerTypeToken, transformToOuter, transformToInner); + } + + /** + * Creates a {@link TransformingList} which transforms/decorates the specified list. + * + * @param list + * the list to transform; will be the inner list of the returned transformation + * @return a new {@link TransformingList} + */ + public TransformingList transformList(List list) { + return new TransformingList<>( + list, innerTypeToken, outerTypeToken, transformToOuter, transformToInner); + } + + // #end BUILD + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/TransformingIterator.java b/src/main/java/org/codefx/libfx/collection/transform/TransformingIterator.java new file mode 100644 index 0000000..d3e2bac --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/TransformingIterator.java @@ -0,0 +1,59 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Iterator; +import java.util.Objects; +import java.util.function.Function; + +/** + * An {@link Iterator} which wraps another iterator and transforms the returned elements from their inner type {@code I} + * to an outer type {@code O}. + *

+ * The transformation of null elements is fixed to {@code null -> null}. The transformation function specified during + * construction does not have to handle null input elements and is not allowed to produce a null result. + * + * @param + * the inner type, i.e. the type of the elements returned by the wrapped/inner iterator + * @param + * the outer type, i.e. the type of elements returned by this iterator + */ +public final class TransformingIterator extends AbstractTransformingIterator { + + private final Iterator innerIterator; + private final Function transformToOuter; + + /** + * Creates a new iterator. + *

+ * If the specified iterator is used by any other instance, the behavior is undefined. The specified transform + * function will not be called with null elements and is not allowed to return null. + * + * @param innerIterator + * the wrapped/inner iterator + * @param transformToOuter + * transforms elements from the inner type {@code I} to the outer type {@code O} + */ + public TransformingIterator( + Iterator innerIterator, Function transformToOuter) { + Objects.requireNonNull(innerIterator, "The argument 'innerIterator' must not be null."); + Objects.requireNonNull(transformToOuter, "The argument 'transformToOuter' must not be null."); + + this.innerIterator = innerIterator; + this.transformToOuter = transformToOuter; + } + + @Override + protected Iterator getInnerIterator() { + return innerIterator; + } + + @Override + protected O transformToOuter(I innerElement) { + if (innerElement == null) + return null; + + O outerElement = transformToOuter.apply(innerElement); + Objects.requireNonNull(outerElement, "The transformation must not create null instances."); + return outerElement; + } + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/TransformingList.java b/src/main/java/org/codefx/libfx/collection/transform/TransformingList.java new file mode 100644 index 0000000..c08e35b --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/TransformingList.java @@ -0,0 +1,117 @@ +package org.codefx.libfx.collection.transform; + +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +/** + * A {@link List} which decorates another list and transforms the element type from the inner type {@code I} to an outer + * type {@code O}. + *

+ * See the {@link org.codefx.libfx.collection.transform package} documentation for general comments on transformation. + *

+ * This implementation mitigates the type safety problems by using a token of the inner and the outer type to check + * instances against them. This solves some of the critical situations but not all of them. In those other cases + * {@link ClassCastException}s might occur when an element can not be transformed by the transformation functions. + *

+ * Null elements are allowed unless the inner list does not accept them. These are handled explicitly and fixed to the + * transformation {@code null -> null}. The transforming functions specified during construction neither have to handle + * that case nor are they allowed to produce null elements. + *

+ * If the {@link #stream() stream} returned by this list is told to {@link java.util.stream.Stream#sorted() sort} + * itself, it will do so on the base of the comparator returned by the inner list's spliterator (e.g. based on the + * natural order of {@code O} if it has one). + *

+ * {@code TransformingList}s are created with a {@link TransformingCollectionBuilder}. + * + * @param + * the inner type, i.e. the type of the elements contained in the wrapped/inner collection + * @param + * the outer type, i.e. the type of elements appearing to be in this collection + */ +public final class TransformingList extends AbstractTransformingList { + + // #begin FIELDS + + private final List innerList; + private final Class innerTypeToken; + private final Class outerTypeToken; + private final Function transformToOuter; + private final Function transformToInner; + + // #end FIELDS + + /** + * Creates a new transforming list. + * + * @param innerList + * the wrapped list + * @param innerTypeToken + * the token for the inner type + * @param outerTypeToken + * the token for the outer type + * @param transformToOuter + * transforms an element from an inner to an outer type; will never be called with null argument and must + * not produce null + * @param transformToInner + * transforms an element from an outer to an inner type; will never be called with null argument and must + * not produce null + */ + public TransformingList( + List innerList, + Class innerTypeToken, Class outerTypeToken, + Function transformToOuter, Function transformToInner) { + + Objects.requireNonNull(innerList, "The argument 'innerList' must not be null."); + Objects.requireNonNull(innerTypeToken, "The argument 'innerTypeToken' must not be null."); + Objects.requireNonNull(outerTypeToken, "The argument 'outerTypeToken' must not be null."); + Objects.requireNonNull(transformToOuter, "The argument 'transformToOuter' must not be null."); + Objects.requireNonNull(transformToInner, "The argument 'transformToInner' must not be null."); + + this.innerList = innerList; + this.innerTypeToken = innerTypeToken; + this.outerTypeToken = outerTypeToken; + this.transformToOuter = transformToOuter; + this.transformToInner = transformToInner; + } + + // #begin IMPLEMENTATION OF 'AbstractTransformingList' + + @Override + protected List getInnerList() { + return innerList; + } + + @Override + protected boolean isInnerElement(Object object) { + return object == null || innerTypeToken.isInstance(object); + } + + @Override + protected O transformToOuter(I innerElement) { + if (innerElement == null) + return null; + + O outerElement = transformToOuter.apply(innerElement); + Objects.requireNonNull(outerElement, "The transformation must not create null instances."); + return outerElement; + } + + @Override + protected boolean isOuterElement(Object object) { + return object == null || outerTypeToken.isInstance(object); + } + + @Override + protected I transformToInner(O outerElement) { + if (outerElement == null) + return null; + + I innerElement = transformToInner.apply(outerElement); + Objects.requireNonNull(innerElement, "The transformation must not create null instances."); + return innerElement; + } + + // #end IMPLEMENTATION OF 'AbstractTransformingList' + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/TransformingListIterator.java b/src/main/java/org/codefx/libfx/collection/transform/TransformingListIterator.java new file mode 100644 index 0000000..a552c89 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/TransformingListIterator.java @@ -0,0 +1,77 @@ +package org.codefx.libfx.collection.transform; + +import java.util.ListIterator; +import java.util.Objects; +import java.util.function.Function; + +/** + * A {@link ListIterator} which wraps another list iterator and transforms elements from an inner type {@code I} to an + * outer type {@code O} and vice versa. + *

+ * The transformation of null elements of either inner or outer type is fixed to {@code null -> null}. The + * transformation functions specified during construction do not have to handle null input elements and are not allowed + * to produce a null result. + * + * @param + * the inner type, i.e. the type of the elements returned by the wrapped/inner list iterator + * @param + * the outer type, i.e. the type of elements returned by this list iterator + */ +public final class TransformingListIterator extends AbstractTransformingListIterator { + + private final ListIterator innerListIterator; + private final Function transformToOuter; + private final Function transformToInner; + + /** + * Creates a new transforming list iterator. + *

+ * If the specified list iterator is used by any other instance, the behavior is undefined. The specified transform + * functions will not be called with null elements and are not allowed to return null. + * + * @param innerListIterator + * the wrapped/inner list iterator + * @param transformToOuter + * transforms elements from the inner type {@code I} to the outer type {@code O} + * @param transformToInner + * transforms elements from the outer type {@code O} to the inner type {@code I} + */ + public TransformingListIterator( + ListIterator innerListIterator, + Function transformToOuter, Function transformToInner) { + + Objects.requireNonNull(innerListIterator, "The argument 'innerListIterator' must not be null."); + Objects.requireNonNull(transformToOuter, "The argument 'transformToOuter' must not be null."); + Objects.requireNonNull(transformToInner, "The argument 'transformToInner' must not be null."); + + this.innerListIterator = innerListIterator; + this.transformToOuter = transformToOuter; + this.transformToInner = transformToInner; + } + + @Override + protected ListIterator getInnerIterator() { + return innerListIterator; + } + + @Override + protected O transformToOuter(I innerElement) { + if (innerElement == null) + return null; + + O outerElement = transformToOuter.apply(innerElement); + Objects.requireNonNull(outerElement, "The transformation must not create null instances."); + return outerElement; + } + + @Override + protected I transformToInner(O outerElement) { + if (outerElement == null) + return null; + + I innerElement = transformToInner.apply(outerElement); + Objects.requireNonNull(innerElement, "The transformation must not create null instances."); + return innerElement; + } + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/TransformingMap.java b/src/main/java/org/codefx/libfx/collection/transform/TransformingMap.java new file mode 100644 index 0000000..80ae3d7 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/TransformingMap.java @@ -0,0 +1,183 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +/** + * A {@link Map} which decorates another map and transforms the key and value types from the inner types {@code IK}, + * {@code IV} to outer types {@code OK}, {@code OV}. + *

+ * See the {@link org.codefx.libfx.collection.transform package} documentation for general comments on transformation. + *

+ * This implementation mitigates the type safety problems by using tokens of the inner and the outer types to check + * instances against them. This solves some of the critical situations but not all of them. In those other cases + * {@link ClassCastException}s might occur when an element can not be transformed by the transformation functions. + *

+ * Null keys and values are allowed unless the inner map does not accept them. These are handled explicitly and fixed to + * the transformation {@code null -> null}. The transforming functions specified during construction neither have to + * handle that case nor are they allowed to produce null elements. + *

+ * If the {@link java.util.stream.Stream streams} returned by this map's views are told to + * {@link java.util.stream.Stream#sorted() sort} themself, they will do so on the base of the comparator returned by the + * inner map view's spliterator (e.g. based on the natural order of {@code IK} or {@code IV} if it has one). + *

+ * {@code TransformingMap}s are created with a {@link TransformingMapBuilder}. + * + * @param + * the inner key type, i.e. the type of the keys contained in the wrapped/inner map + * @param + * the outer key type, i.e. the type of keys appearing to be in this map + * @param + * the inner value type, i.e. the type of the values contained in the wrapped/inner map + * @param + * the outer value type, i.e. the type of values appearing to be in this map + */ +public final class TransformingMap extends AbstractTransformingMap { + + // #begin FIELDS + + private final Map innerMap; + + private final Class outerKeyTypeToken; + private final Class innerKeyTypeToken; + private final Function transformToOuterKey; + private final Function transformToInnerKey; + + private final Class outerValueTypeToken; + private final Class innerValueTypeToken; + private final Function transformToOuterValue; + private final Function transformToInnerValue; + + // #end FIELDS + + // #begin CONSTRUCTION + + /** + * Creates a new transforming map. + * + * @param innerMap + * the wrapped map + * @param innerKeyTypeToken + * the token for the inner key type + * @param outerKeyTypeToken + * the token for the outer key type + * @param transformToOuterKey + * transforms a key from an inner to an outer key type; will never be called with null argument and must + * not produce null + * @param transformToInnerKey + * transforms a key from an outer to an inner key type; will never be called with null argument and must + * not produce null + * @param innerValueTypeToken + * the token for the inner value type + * @param outerValueTypeToken + * the token for the outer value type + * @param transformToOuterValue + * transforms a value from an inner to an outer value type; will never be called with null argument and + * must not produce null + * @param transformToInnerValue + * transforms a value from an outer to an inner value type; will never be called with null argument and + * must not produce null + */ + TransformingMap( + Map innerMap, + Class innerKeyTypeToken, Class outerKeyTypeToken, + Function transformToOuterKey, + Function transformToInnerKey, + Class innerValueTypeToken, Class outerValueTypeToken, + Function transformToOuterValue, + Function transformToInnerValue) { + + Objects.requireNonNull(innerMap, "The argument 'innerMap' must not be null."); + Objects.requireNonNull(innerKeyTypeToken, "The argument 'innerKeyTypeToken' must not be null."); + Objects.requireNonNull(outerKeyTypeToken, "The argument 'outerKeyTypeToken' must not be null."); + Objects.requireNonNull(transformToOuterKey, "The argument 'transformToOuterKey' must not be null."); + Objects.requireNonNull(transformToInnerKey, "The argument 'transformToInnerKey' must not be null."); + Objects.requireNonNull(innerValueTypeToken, "The argument 'innerValueTypeToken' must not be null."); + Objects.requireNonNull(outerValueTypeToken, "The argument 'outerValueTypeToken' must not be null."); + Objects.requireNonNull(transformToOuterValue, "The argument 'transformToOuterValue' must not be null."); + Objects.requireNonNull(transformToInnerValue, "The argument 'transformToInnerValue' must not be null."); + + this.innerMap = innerMap; + this.outerKeyTypeToken = outerKeyTypeToken; + this.innerKeyTypeToken = innerKeyTypeToken; + this.transformToOuterKey = transformToOuterKey; + this.transformToInnerKey = transformToInnerKey; + this.outerValueTypeToken = outerValueTypeToken; + this.innerValueTypeToken = innerValueTypeToken; + this.transformToOuterValue = transformToOuterValue; + this.transformToInnerValue = transformToInnerValue; + } + + // #end CONSTRUCTION + + // #begin IMPLEMENTATION OF 'AbstractTransformingMap' + + @Override + protected Map getInnerMap() { + return innerMap; + } + + @Override + protected boolean isInnerKey(Object object) { + return object == null || innerKeyTypeToken.isInstance(object); + } + + @Override + protected OK transformToOuterKey(IK innerKey) { + if (innerKey == null) + return null; + + OK outerKey = transformToOuterKey.apply(innerKey); + Objects.requireNonNull(outerKey, "The transformation must not create null instances."); + return outerKey; + } + + @Override + protected boolean isOuterKey(Object object) { + return object == null || outerKeyTypeToken.isInstance(object); + } + + @Override + protected IK transformToInnerKey(OK outerKey) { + if (outerKey == null) + return null; + + IK innerKey = transformToInnerKey.apply(outerKey); + Objects.requireNonNull(innerKey, "The transformation must not create null instances."); + return innerKey; + } + + @Override + protected boolean isInnerValue(Object object) { + return object == null || innerValueTypeToken.isInstance(object); + } + + @Override + protected OV transformToOuterValue(IV innerValue) { + if (innerValue == null) + return null; + + OV outerValue = transformToOuterValue.apply(innerValue); + Objects.requireNonNull(outerValue, "The transformation must not create null instances."); + return outerValue; + } + + @Override + protected boolean isOuterValue(Object object) { + return object == null || outerValueTypeToken.isInstance(object); + } + + @Override + protected IV transformToInnerValue(OV outerValue) { + if (outerValue == null) + return null; + + IV innerValue = transformToInnerValue.apply(outerValue); + Objects.requireNonNull(innerValue, "The transformation must not create null instances."); + return innerValue; + } + + // #end IMPLEMENTATION OF 'AbstractTransformingMap' + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/TransformingMapBuilder.java b/src/main/java/org/codefx/libfx/collection/transform/TransformingMapBuilder.java new file mode 100644 index 0000000..3df0c45 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/TransformingMapBuilder.java @@ -0,0 +1,193 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +/** + * Builder for {@link TransformingMap}s. + *

+ * A builder can be obtained by calling {@link #forTypes(Class, Class, Class, Class) forTypes} or + * {@link #forTypesUnknown()}. The building method TODO can only be called after transformations from inner to outer + * keys and values and vice versa have been set. + * + * @param + * the inner key type of the created transforming map, i.e. the type of the keys contained in the + * wrapped/inner map + * @param + * the outer key type of the created transforming map, i.e. the type of keys appearing to be in this map + * @param + * the inner value type of the created transforming map, i.e. the type of the values contained in the + * wrapped/inner map + * @param + * the outer value type of the created transforming map, i.e. the type of values appearing to be in this map + */ +public class TransformingMapBuilder { + + // #begin FIELDS + + private final Class outerKeyTypeToken; + private final Class innerKeyTypeToken; + private Function transformToOuterKey; + private Function transformToInnerKey; + + private final Class outerValueTypeToken; + private final Class innerValueTypeToken; + private Function transformToOuterValue; + private Function transformToInnerValue; + + // #end FIELDS + + // #begin CONSTRUCTION + + private TransformingMapBuilder( + Class innerKeyTypeToken, Class outerKeyTypeToken, + Class innerValueTypeToken, Class outerValueTypeToken) { + + Objects.requireNonNull(innerKeyTypeToken, "The argument 'innerKeyTypeToken' must not be null."); + Objects.requireNonNull(outerKeyTypeToken, "The argument 'outerKeyTypeToken' must not be null."); + Objects.requireNonNull(innerValueTypeToken, "The argument 'innerValueTypeToken' must not be null."); + Objects.requireNonNull(outerValueTypeToken, "The argument 'outerValueTypeToken' must not be null."); + + this.innerKeyTypeToken = innerKeyTypeToken; + this.outerKeyTypeToken = outerKeyTypeToken; + this.innerValueTypeToken = innerValueTypeToken; + this.outerValueTypeToken = outerValueTypeToken; + } + + /** + * Returns a new builder for the specified inner and outer key and value types. + * + * @param + * the inner key type of the created transforming map, i.e. the type of the keys contained in the + * wrapped/inner map + * @param + * the outer key type of the created transforming map, i.e. the type of keys appearing to be in this map + * @param + * the inner value type of the created transforming map, i.e. the type of the values contained in the + * wrapped/inner map + * @param + * the outer value type of the created transforming map, i.e. the type of values appearing to be in this + * map + * @param innerKeyTypeToken + * the token for the inner key type + * @param outerKeyTypeToken + * the token for the outer key type + * @param innerValueTypeToken + * the token for the inner value type + * @param outerValueTypeToken + * the token for the outer value type + * @return a new builder + */ + public static TransformingMapBuilder forTypes( + Class innerKeyTypeToken, Class outerKeyTypeToken, + Class innerValueTypeToken, Class outerValueTypeToken) { + + return new TransformingMapBuilder<>( + innerKeyTypeToken, outerKeyTypeToken, innerValueTypeToken, outerValueTypeToken); + } + + /** + * Returns a new builder for unknown inner and outer key and value types. + *

+ * This is equivalent to calling {@link #forTypes(Class, Class, Class, Class) forTypes(Object.class, Object.class, + * Object.class, Object.class)}. To obtain a builder for {@code } you will have to call + * {@code TransformingMapBuilder. forTypesUnknown()}. + * + * @param + * the inner key type of the created transforming map, i.e. the type of the keys contained in the + * wrapped/inner map + * @param + * the outer key type of the created transforming map, i.e. the type of keys appearing to be in this map + * @param + * the inner value type of the created transforming map, i.e. the type of the values contained in the + * wrapped/inner map + * @param + * the outer value type of the created transforming map, i.e. the type of values appearing to be in this + * map + * @return a new builder + */ + public static TransformingMapBuilder forTypesUnknown() { + return forTypes(Object.class, Object.class, Object.class, Object.class); + } + + // #end CONSTRUCTION + + // #begin SET FIELDS + + /** + * Sets the transformation from inner to outer keys which will be used by the created map. + * + * @param transformToOuterKey + * transforms inner to outer keys + * @return this builder + */ + public TransformingMapBuilder toOuterKey(Function transformToOuterKey) { + Objects.requireNonNull(transformToOuterKey, "The argument 'transformToOuterKey' must not be null."); + + this.transformToOuterKey = transformToOuterKey; + return this; + } + + /** + * Sets the transformation from outer to inner keys which will be used by the created map. + * + * @param transformToInnerKey + * transforms outer to inner keys + * @return this builder + */ + public TransformingMapBuilder toInnerKey(Function transformToInnerKey) { + Objects.requireNonNull(transformToInnerKey, "The argument 'transformToInnerKey' must not be null."); + + this.transformToInnerKey = transformToInnerKey; + return this; + } + + /** + * Sets the transformation from inner to outer values which will be used by the created map. + * + * @param transformToOuterValue + * transforms inner to outer values + * @return this builder + */ + public TransformingMapBuilder toOuterValue(Function transformToOuterValue) { + Objects.requireNonNull(transformToOuterValue, "The argument 'transformToOuterValue' must not be null."); + + this.transformToOuterValue = transformToOuterValue; + return this; + } + + /** + * Sets the transformation from outer to inner values which will be used by the created map. + * + * @param transformToInnerValue + * transforms outer to inner values + * @return this builder + */ + public TransformingMapBuilder toInnerValue(Function transformToInnerValue) { + Objects.requireNonNull(transformToInnerValue, "The argument 'transformToInnerValue' must not be null."); + + this.transformToInnerValue = transformToInnerValue; + return this; + } + + // #end SET FIELDS + + // #begin BUILD + + /** + * Creates a {@link TransformingMap} which transforms/decorates the specified map. + * + * @param map + * the map to transform; will be the inner map of the returned transformation + * @return a new {@link TransformingMap} + */ + public TransformingMap transformMap(Map map) { + return new TransformingMap<>(map, + innerKeyTypeToken, outerKeyTypeToken, transformToOuterKey, transformToInnerKey, + innerValueTypeToken, outerValueTypeToken, transformToOuterValue, transformToInnerValue); + } + + // #end BUILD + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/TransformingSet.java b/src/main/java/org/codefx/libfx/collection/transform/TransformingSet.java new file mode 100644 index 0000000..75aaa1b --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/TransformingSet.java @@ -0,0 +1,117 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +/** + * A {@link Set} which decorates another set and transforms the element type from the inner type {@code I} to an outer + * type {@code O}. + *

+ * See the {@link org.codefx.libfx.collection.transform package} documentation for general comments. + *

+ * This implementation mitigates the type safety problems by using a token of the inner and the outer type to check + * instances against them. This solves some of the critical situations but not all of them. In those other cases + * {@link ClassCastException}s might occur when an element can not be transformed by the transformation functions. + *

+ * Null elements are allowed unless the inner set does not accept them. These are handled explicitly and fixed to the + * transformation {@code null -> null}. The transforming functions specified during construction neither have to handle + * that case nor are they allowed to produce null elements. + *

+ * If the {@link #stream() stream} returned by this set is told to {@link java.util.stream.Stream#sorted() sort} itself, + * it will do so on the base of the comparator returned by the inner sets's spliterator (e.g. based on the natural order + * of {@code I} if it has one). + *

+ * {@code TransformingSet}s are created with a {@link TransformingCollectionBuilder}. + * + * @param + * the inner type, i.e. the type of the elements contained in the wrapped/inner set + * @param + * the outer type, i.e. the type of elements appearing to be in this set + */ +public final class TransformingSet extends AbstractTransformingSet { + + // #begin FIELDS + + private final Set innerSet; + private final Class outerTypeToken; + private final Class innerTypeToken; + private final Function transformToOuter; + private final Function transformToInner; + + // #end FIELDS + + /** + * Creates a new transforming set. + * + * @param innerSet + * the wrapped set + * @param innerTypeToken + * the token for the inner type + * @param outerTypeToken + * the token for the outer type + * @param transformToOuter + * transforms an element from an inner to an outer type; will never be called with null argument and must + * not produce null + * @param transformToInner + * transforms an element from an outer to an inner type; will never be called with null argument and must + * not produce null + */ + TransformingSet( + Set innerSet, + Class innerTypeToken, Class outerTypeToken, + Function transformToOuter, Function transformToInner) { + + Objects.requireNonNull(innerSet, "The argument 'innerSet' must not be null."); + Objects.requireNonNull(innerTypeToken, "The argument 'innerTypeToken' must not be null."); + Objects.requireNonNull(outerTypeToken, "The argument 'outerTypeToken' must not be null."); + Objects.requireNonNull(transformToOuter, "The argument 'transformToOuter' must not be null."); + Objects.requireNonNull(transformToInner, "The argument 'transformToInner' must not be null."); + + this.innerSet = innerSet; + this.innerTypeToken = innerTypeToken; + this.outerTypeToken = outerTypeToken; + this.transformToOuter = transformToOuter; + this.transformToInner = transformToInner; + } + + // #begin IMPLEMENTATION OF 'AbstractTransformingSet' + + @Override + protected Set getInnerSet() { + return innerSet; + } + + @Override + protected boolean isInnerElement(Object object) { + return object == null || innerTypeToken.isInstance(object); + } + + @Override + protected O transformToOuter(I innerElement) { + if (innerElement == null) + return null; + + O outerElement = transformToOuter.apply(innerElement); + Objects.requireNonNull(outerElement, "The transformation must not create null instances."); + return outerElement; + } + + @Override + protected boolean isOuterElement(Object object) { + return object == null || outerTypeToken.isInstance(object); + } + + @Override + protected I transformToInner(O outerElement) { + if (outerElement == null) + return null; + + I innerElement = transformToInner.apply(outerElement); + Objects.requireNonNull(innerElement, "The transformation must not create null instances."); + return innerElement; + } + + // #end IMPLEMENTATION OF 'AbstractTransformingSet' + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/TransformingSpliterator.java b/src/main/java/org/codefx/libfx/collection/transform/TransformingSpliterator.java new file mode 100644 index 0000000..23483c4 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/TransformingSpliterator.java @@ -0,0 +1,87 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Objects; +import java.util.Spliterator; +import java.util.function.Function; + +/** + * A {@link Spliterator} which wraps another spliterator and transforms the returned elements from an inner type + * {@code I} to an outer type {@code O} and vice versa. + *

+ * The transformation of null elements of either inner or outer type is fixed to {@code null -> null}. The + * transformation functions specified during construction do not have to handle null input elements and are not allowed + * to produce a null result. + *

+ * Note that this spliterator reports the exact same {@link Spliterator#SORTED SORTED} {@link #characteristics() + * characteristic} as the inner one. It's {@link #getComparator()} transforms the elements it should compare from the + * outer to the inner type and calls the inner spliterator's {@link Spliterator#getComparator() comparator} with it. + * This means that sorting streams is always done by the inner spliterator's logic. + * + * @param + * the inner type, i.e. the type of the elements returned by the wrapped/inner spliterator + * @param + * the outer type, i.e. the type of elements returned by this spliterator + */ +public final class TransformingSpliterator extends AbstractTransformingSpliterator { + + private final Spliterator innerSpliterator; + private final Function transformToOuter; + private final Function transformToInner; + + /** + * Creates a new transforming spliterator. + *

+ * If the specified spliterator is used by any other instance, the behavior is undefined. The specified transform + * functions will not be called with null elements and are not allowed to return null. + * + * @param innerSpliterator + * the wrapped/inner spliterator + * @param transformToOuter + * transforms elements from the inner type {@code I} to the outer type {@code O} + * @param transformToInner + * transforms elements from the outer type {@code O} to the inner type {@code I} + */ + public TransformingSpliterator( + Spliterator innerSpliterator, + Function transformToOuter, Function transformToInner) { + + Objects.requireNonNull(innerSpliterator, "The argument 'innerSpliterator' must not be null."); + Objects.requireNonNull(transformToInner, "The argument 'transformToInner' must not be null."); + Objects.requireNonNull(transformToOuter, "The argument 'transformToOuter' must not be null."); + + this.innerSpliterator = innerSpliterator; + this.transformToInner = transformToInner; + this.transformToOuter = transformToOuter; + } + + @Override + protected Spliterator getInnerSpliterator() { + return innerSpliterator; + } + + @Override + protected O transformToOuter(I innerElement) { + if (innerElement == null) + return null; + + O outerElement = transformToOuter.apply(innerElement); + Objects.requireNonNull(outerElement, "The transformation must not create null instances."); + return outerElement; + } + + @Override + protected I transformToInner(O outerElement) { + if (outerElement == null) + return null; + + I innerElement = transformToInner.apply(outerElement); + Objects.requireNonNull(innerElement, "The transformation must not create null instances."); + return innerElement; + } + + @Override + protected Spliterator wrapNewSpliterator(Spliterator newSpliterator) { + return new TransformingSpliterator<>(newSpliterator, transformToOuter, transformToInner); + } + +} diff --git a/src/main/java/org/codefx/libfx/collection/transform/package-info.java b/src/main/java/org/codefx/libfx/collection/transform/package-info.java new file mode 100644 index 0000000..38b8153 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/transform/package-info.java @@ -0,0 +1,92 @@ +/** + *

+ * Provides transforming collections. + *

+ *

Overview

+ *

+ * A transforming collection is essentially a decorator of an existing collection which transforms the collection's + * elements from one type to another. Note that all such classes are views! They operate on the wrapped collection and + * all changes to either of them is reflected in the other. + *

+ * The decorated collection is usually referred to as the inner collection and it's generic type accordingly as + * the inner type. The transforming collection and its generic type are referred to as outer collection + * and outer type, respectively. + *

+ *

Details

+ *

+ * The following details are important to use transforming collections without unexpected problems. + *

+ *

Forwarding

+ *

+ * Unless otherwise noted the views forward all method calls (of abstract and default methods existing in JDK 8) to + * the same method on the inner collection. This implies that all guarantees made by such methods (e.g. regarding + * atomicity) are upheld by the transformation. + *

+ *

Transformation

+ *

+ * The transformation is computed with a pair of functions. One is used to transform outer elements to inner elements + * and another one for the other direction. In the case of a {@link java.util.Map Map} two such pairs exist: one for + * keys and one for values. + *

+ * The transforming functions must be inverse to each other with regard to {@link java.lang.Object#equals(Object) + * equals}, i.e. {@code outer.equals(toOuter(toInner(outer))} and {@code inner.equals(toInner(toOuter(inner))} must be + * true for all {@code outer} and {@code inner} elements. If this is not the case, the collections might behave in an + * unpredictable manner. + *

+ * Note that the same is not true for identity, i.e. {@code outer == toOuter(toInner(outer))} may be false. It is + * explicitly allowed to create new outer elements from inner elements and vice versa. This means that outer elements + * may have no meaningful identity. E.g. on adding an outer instance {@code outerOrg} it can be transformed to + * {@code inner} and on access back to {@code outer}. Whether {@code outerOrg == outer} holds, depends on the + * transformation functions and is generally unspecified - it might never, sometimes or always be true. Transforming + * collections might give more details on their behavior regarding this. + *

+ * A special case of transformation occurs when the inner and outer type have a type relationship. This can shortcut one + * of the transformations to the identity, i.e. because an instance of one type is also of the other type the + * corresponding transformation can simply return the instance itself. This makes the collection "leak" elements of the + * wrong type. If the suptype does not fully obey the Liskov Substitution Principle this can lead to + * unexpected behavior. + *

+ * Transforming functions should have no side effects. No guarantees are made regarding how often transformations are + * called. + *

+ *

Type Safety

+ *

+ * All operations on transforming collections are type safe in the usual static, compile-time way. But since many + * methods from the collection interfaces allow {@link java.lang.Object Object}s (e.g + * {@link java.util.Collection#contains(Object) Collection.contains(Object)}) or collections of unknown generic type + * (e.g {@link java.util.Collection#addAll(java.util.Collection) Collection.addAll(Collection<?>)}) as arguments, + * this does not cover all cases which can occur at runtime. + *

+ * If one of those methods is called with a type which does not match the transforming collection's outer type the + * method may throw a {@link java.lang.ClassCastException ClassCastException}. While this is in accordance with the + * methods' contracts it might still be unexpected. + *

+ * One way to circumvent this to pay close attention when using the collection and ensuring that such calls can not + * occur (which is often easy). Another way is to write wrappers which catch and silently ignore the exception. + *

+ *

Similar Features From Other Libraries

+ *

+ * To my (nipa's) knowledge two other libraries offer similar + * functionality, namely Apache Commons Collections + * and Google Guava. Both have shortcomings which this implementation aims + * to overcome. + *

+ *

Apache Commons Collections

+ *

+ * Commons provides the {@code TransformedCollection}. It only affects add methods, + * "thus objects must be removed or searched for using their transformed form." The implementations in this package + * suffer from no such limitation. + *

+ *

Google Guava

+ *

+ * Guava contains a utility method {@code transform(List, Function)} which returns a transformed view of the specified map. But + * "the transform is one-way and new items cannot be stored in the returned list." The implementations in this package + * explicitly allow editing both instances. + *

+ */ +package org.codefx.libfx.collection.transform; \ No newline at end of file diff --git a/src/main/java/org/codefx/libfx/collection/tree/navigate/ComponentHierarchyNavigator.java b/src/main/java/org/codefx/libfx/collection/tree/navigate/ComponentHierarchyNavigator.java new file mode 100644 index 0000000..79d02b5 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/navigate/ComponentHierarchyNavigator.java @@ -0,0 +1,103 @@ +package org.codefx.libfx.collection.tree.navigate; + +import java.awt.Component; +import java.awt.Container; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; + +/** + * A {@link TreeNavigator} for an AWT component hierarchy. + *

+ * This implementation is thread-safe in the sense that individual method calls will not fail if the component hierarchy + * is changed concurrently. But it can not prevent return values from getting stale so chaining calls might lead to + * unexpected results, e.g.: + * + *

+ * Component component = ...
+ * if (getParent(component).isPresent()) {
+ * 	// the component has a parent, so it should have a child index, too;
+ * 	// but the component hierarchy may have changed, so 'indexPresent' may be false
+ * 	boolean indexPresent = getChildIndex(component).isPresent();
+ * }
+ * 
+ * Similarly: + * + *
+ * Component parent = ...
+ * Optional<Component> child1 = getChild(parent, 0);
+ * Optional<Component> child2 = getChild(parent, 0);
+ * // if the component hierarchy changed between the two calls, this may be false
+ * boolean sameChildren = child1.equals(child2);
+ * 
+ */ +public class ComponentHierarchyNavigator implements TreeNavigator { + + @Override + public Optional getParent(Component child) { + Objects.requireNonNull(child, "The argument 'child' must not be null."); + + return Optional.ofNullable(child.getParent()); + } + + @Override + public OptionalInt getChildIndex(Component node) { + Objects.requireNonNull(node, "The argument 'node' must not be null."); + + Component parent = node.getParent(); + if (parent == null) + return OptionalInt.empty(); + + if (!(parent instanceof Container)) + return OptionalInt.empty(); + + Component[] siblings = ((Container) parent).getComponents(); + return getIndex(node, siblings); + } + + private static OptionalInt getIndex(Component node, Component[] siblings) { + try { + for (int i = 0; i < siblings.length; i++) + if (siblings[i] == node) + return OptionalInt.of(i); + return OptionalInt.empty(); + } catch (ArrayIndexOutOfBoundsException ex) { + return OptionalInt.empty(); + } + } + + @Override + public int getChildrenCount(Component parent) { + Objects.requireNonNull(parent, "The argument 'parent' must not be null."); + + if (!(parent instanceof Container)) + return 0; + + return ((Container) parent).getComponents().length; + } + + @Override + public Optional getChild(Component parent, int childIndex) { + Objects.requireNonNull(parent, "The argument 'parent' must not be null."); + if (childIndex < 0) + throw new IllegalArgumentException("The argument 'childIndex' must be non-negative."); + + if (!(parent instanceof Container)) + return Optional.empty(); + + return getChild((Container) parent, childIndex); + } + + private static Optional getChild(Container parent, int childIndex) { + if (parent.getComponents().length <= childIndex) + return Optional.empty(); + + try { + // even though we checked first, due to threading this might fail + return Optional.of(parent.getComponent(childIndex)); + } catch (ArrayIndexOutOfBoundsException ex) { + return Optional.empty(); + } + } + +} diff --git a/src/main/java/org/codefx/libfx/collection/tree/navigate/JComponentHierarchyNavigator.java b/src/main/java/org/codefx/libfx/collection/tree/navigate/JComponentHierarchyNavigator.java new file mode 100644 index 0000000..f9b541c --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/navigate/JComponentHierarchyNavigator.java @@ -0,0 +1,108 @@ +package org.codefx.libfx.collection.tree.navigate; + +import java.awt.Component; +import java.awt.Container; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; + +import javax.swing.JComponent; + +/** + * A {@link TreeNavigator} for a Swing component hierarchy. + *

+ * Note that {@link #getParent(JComponent) getParent} will return an (optional) {@link JComponent}. This is not possible + * if the parent is not of a subtype (e.g. a {@link javax.swing.JFrame JFrame}) in which case an empty {@code Optional} + * is returned. Similarly, children of {@code JComponent}s might not be {@code JComponent}s themselves. For this reason + * {@link #getChild(JComponent, int) getChild} will return an empty {@code Optional} for a child which is no + * {@code JComponent}. If this is undesired, consider using a {@link ComponentHierarchyNavigator} instead. + *

+ * This implementation is thread-safe in the sense that individual method calls will not fail if the component hierarchy + * is changed concurrently. But it can not prevent return values from getting stale so chaining calls might lead to + * unexpected results, e.g.: + * + *

+ * JComponent component = ...
+ * if (getParent(component).isPresent()) {
+ * 	// the component has a parent, so it should have a child index, too;
+ * 	// but the component hierarchy may have changed, so 'indexPresent' may be false
+ * 	boolean indexPresent = getChildIndex(component).isPresent();
+ * }
+ * 
+ * Similarly: + * + *
+ * JComponent parent = ...
+ * Optional<JComponent> child1 = getChild(parent, 0);
+ * Optional<JComponent> child2 = getChild(parent, 0);
+ * // if the component hierarchy changed between the two calls, this may be false
+ * boolean sameChildren = child1.equals(child2);
+ * 
+ */ +public class JComponentHierarchyNavigator implements TreeNavigator { + + @Override + public Optional getParent(JComponent child) { + Objects.requireNonNull(child, "The argument 'child' must not be null."); + + Container parent = child.getParent(); + if (!(parent instanceof JComponent)) + return Optional.empty(); + + return Optional.ofNullable((JComponent) parent); + } + + @Override + public OptionalInt getChildIndex(JComponent node) { + Objects.requireNonNull(node, "The argument 'node' must not be null."); + + Component parent = node.getParent(); + if (parent == null) + return OptionalInt.empty(); + + if (!(parent instanceof JComponent)) + return OptionalInt.empty(); + + Component[] siblings = ((JComponent) parent).getComponents(); + return getIndex(node, siblings); + } + + private static OptionalInt getIndex(Component node, Component[] siblings) { + try { + for (int i = 0; i < siblings.length; i++) + if (siblings[i] == node) + return OptionalInt.of(i); + return OptionalInt.empty(); + } catch (ArrayIndexOutOfBoundsException ex) { + return OptionalInt.empty(); + } + } + + @Override + public int getChildrenCount(JComponent parent) { + Objects.requireNonNull(parent, "The argument 'parent' must not be null."); + + return parent.getComponents().length; + } + + @Override + public Optional getChild(JComponent parent, int childIndex) { + Objects.requireNonNull(parent, "The argument 'parent' must not be null."); + if (childIndex < 0) + throw new IllegalArgumentException("The argument 'childIndex' must be non-negative."); + + if (parent.getComponents().length <= childIndex) + return Optional.empty(); + + try { + // even though we checked first, due to threading this might fail + Component child = parent.getComponent(childIndex); + if (!(child instanceof JComponent)) + return Optional.empty(); + return Optional.of((JComponent) child); + } catch (ArrayIndexOutOfBoundsException ex) { + return Optional.empty(); + } + } + +} diff --git a/src/main/java/org/codefx/libfx/collection/tree/navigate/SceneGraphNavigator.java b/src/main/java/org/codefx/libfx/collection/tree/navigate/SceneGraphNavigator.java new file mode 100644 index 0000000..44dcaf8 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/navigate/SceneGraphNavigator.java @@ -0,0 +1,93 @@ +package org.codefx.libfx.collection.tree.navigate; + +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; + +import javafx.scene.Node; +import javafx.scene.Parent; + +/** + * A {@link TreeNavigator} for a JavaFX scene graph. + *

+ * This implementation is thread-safe in the sense that individual method calls will not fail if the scene graph is + * changed concurrently. But it can not prevent return values from getting stale so chaining calls might lead to + * unexpected results, e.g.: + * + *

+ * Node node = ...
+ * if (getParent(node).isPresent()) {
+ * 	// the node has a parent, so it should have a child index, too;
+ * 	// but the scene graph may have changed, so 'indexPresent' may be false
+ * 	boolean indexPresent = getChildIndex(node).isPresent();
+ * }
+ * 
+ * Similarly: + * + *
+ * Node parent = ...
+ * Optional<Node> child1 = getChild(parent, 0);
+ * Optional<Node> child2 = getChild(parent, 0);
+ * // if the scene graph changed between the two calls, this may be false
+ * boolean sameChildren = child1.equals(child2);
+ * 
+ */ +public class SceneGraphNavigator implements TreeNavigator { + + @Override + public Optional getParent(Node child) { + Objects.requireNonNull(child, "The argument 'child' must not be null."); + + return Optional.ofNullable(child.getParent()); + } + + @Override + public OptionalInt getChildIndex(Node node) { + Objects.requireNonNull(node, "The argument 'node' must not be null."); + + Parent parent = node.getParent(); + if (parent == null) + return OptionalInt.empty(); + + int childIndex = parent.getChildrenUnmodifiable().indexOf(node); + if (childIndex == -1) + return OptionalInt.empty(); + + return OptionalInt.of(childIndex); + } + + @Override + public int getChildrenCount(Node parent) { + Objects.requireNonNull(parent, "The argument 'parent' must not be null."); + + if (!(parent instanceof Parent)) + return 0; + + return ((Parent) parent).getChildrenUnmodifiable().size(); + } + + @Override + public Optional getChild(Node parent, int childIndex) { + Objects.requireNonNull(parent, "The argument 'parent' must not be null."); + if (childIndex < 0) + throw new IllegalArgumentException("The argument 'childIndex' must be non-negative."); + + if (!(parent instanceof Parent)) + return Optional.empty(); + + return getChild((Parent) parent, childIndex); + } + + private static Optional getChild(Parent parent, int childIndex) { + if (parent.getChildrenUnmodifiable().size() <= childIndex) + return Optional.empty(); + + try { + // even though we checked first, due to threading this might fail + return Optional.of(parent.getChildrenUnmodifiable().get(childIndex)); + } catch (IndexOutOfBoundsException ex) { + return Optional.empty(); + } + } + +} diff --git a/src/main/java/org/codefx/libfx/collection/tree/navigate/TreeNavigator.java b/src/main/java/org/codefx/libfx/collection/tree/navigate/TreeNavigator.java new file mode 100644 index 0000000..e2a1e4f --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/navigate/TreeNavigator.java @@ -0,0 +1,58 @@ +package org.codefx.libfx.collection.tree.navigate; + +import java.util.Optional; +import java.util.OptionalInt; + +/** + * A tree navigator allows to walk through a tree (i.e. a connected, directed, acyclic graph). + *

+ * This interface can be implemented to navigate arbitrary tree-like data structures without requiring them to implement + * a specific interface. + *

+ * The navigation relies on child nodes having some fixed order so they can be accessed via an index. + * + * @param + * the type of elements contained in the tree + */ +public interface TreeNavigator { + + // PARENT + + /** + * @param child + * an node in the tree + * @return the child's parent; if it is the root, it doesn't have a parent, so {@link Optional#empty() empty} is + * returned + */ + Optional getParent(E child); + + // NODE + + /** + * @param node + * a node in the tree + * @return the index of the node within the list of children of its parent; if it is the root, it doesn't have a + * parent, so {@link OptionalInt#empty() empty} is returned + */ + OptionalInt getChildIndex(E node); + + // CHILDREN + + /** + * @param parent + * a node in the tree + * @return the number of children the node has + */ + int getChildrenCount(E parent); + + /** + * @param parent + * a node in the tree + * @param childIndex + * a non-negative number specifying the index of the requested child + * @return the child of the node with the child index; if no such child exists (e.g. because no children exist) + * {@link OptionalInt#empty() empty} is returned + */ + Optional getChild(E parent, int childIndex); + +} diff --git a/src/main/java/org/codefx/libfx/collection/tree/navigate/package-info.java b/src/main/java/org/codefx/libfx/collection/tree/navigate/package-info.java new file mode 100644 index 0000000..bfde501 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/navigate/package-info.java @@ -0,0 +1,9 @@ +/** + * Provides an abstraction to navigate tree-like + * data structures without requiring them to implement a specific interface. + *

+ * It contains the {@link org.codefx.libfx.collection.tree.navigate.TreeNavigator TreeNavigator} interface and several + * implementations, e.g. for AWT, Swing and JavaFX component hierarchies. + */ +package org.codefx.libfx.collection.tree.navigate; + diff --git a/src/main/java/org/codefx/libfx/collection/tree/stream/BackwardsDfsTreeIterationStrategy.java b/src/main/java/org/codefx/libfx/collection/tree/stream/BackwardsDfsTreeIterationStrategy.java new file mode 100644 index 0000000..dc04afe --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/stream/BackwardsDfsTreeIterationStrategy.java @@ -0,0 +1,124 @@ +package org.codefx.libfx.collection.tree.stream; + +import java.util.Objects; +import java.util.Optional; + +import org.codefx.libfx.collection.tree.navigate.TreeNavigator; + +/** + * A {@link TreeIterationStrategy} which iterates a tree's nodes with a backwards depth-first search. + *

+ * This means that given an initial path from the root {@code R} to some node {@code N} , this strategy will start with + * {@code N} and move backwards visiting parents, left siblings and their children (from right to left) until it reaches + * {@code R}. This will enumerate the same nodes as a depth-first search which started in {@code R} and ended in + * {@code N} but in reverse order (containing both {@code N} and {@code R}). + *

+ * This implementation is only guaranteed to work on trees, i.e. a connected, directed, acyclic graph. Using it on other + * graphs can lead to unexpected behavior including infinite loops. + * + * @param + * the type of elements contained in the tree + */ +final class BackwardsDfsTreeIterationStrategy implements TreeIterationStrategy { + + // #begin FIELDS + + private final TreeNavigator navigator; + + /** + * The path from the root to the last node returned by {@link #goToNextNode()} + *

+ * The path will also contain at least one node before the first call to {@link #goToNextNode()}. These are the + * nodes specified during construction and the one at the end of the path must be returned by the first call to + * {@code goToNextNode()}. Otherwise the end of the path would never be returned by the strategy. Whether + * {@code goToNextNode()} must return this node or find a new one is indicated by {@link #beforeFirst}. + *

+ * If the path is empty, no more nodes will be returned. + */ + private final TreePath> path; + + /** + * Indicates whether {@link #goToNextNode()} was not already called at least once. + */ + private boolean beforeFirst; + + // #end FIELDS + + // #begin CONSTRUCTION + + /** + * Creates a new backwards depth-first search strategy starting with the specified initial path. + *

+ * The iteration will begin with the node at the end of the initial path. It will stop when it successfully + * backtracked to the first node in that path, i.e. the (sub-)tree's root. (Remember, this does not happen on a + * straight route from nodes to their parents but via backwards depth-first search.) + *

+ * The specified path must correspond to the navigator's view on the tree, i.e. each element in the path must be the + * parent (in the tree) of the next one. + *

+ * See {@link TreePathFactory} for easy ways to create an initial path. + * + * @param navigator + * the navigator used to navigate the tree + * @param initialPath + * the initial path from the root of the (sub-)tree iterated by this strategy; must contain at least one + * element; the last element in the path will be returned by the first call to {@link #goToNextNode()} + */ + public BackwardsDfsTreeIterationStrategy(TreeNavigator navigator, TreePath> initialPath) { + Objects.requireNonNull(navigator, "The argument 'navigator' must not be null."); + Objects.requireNonNull(initialPath, "The argument 'initialPath' must not be null."); + if (initialPath.isEmpty()) + throw new IllegalArgumentException("The 'initialPath' must not be empty."); + + this.navigator = navigator; + this.path = initialPath; + this.beforeFirst = true; + } + + // #end CONSTRUCTION + + @Override + public Optional goToNextNode() { + // if the path is empty, iteration ended and no more nodes will be returned + if (path.isEmpty()) + return Optional.empty(); + + // if this is the first call, do not move to the next node + if (beforeFirst) + beforeFirst = false; + else + movePathEndToNextNode(); + + return path.getEnd().map(TreeNode::getElement); + } + + private void movePathEndToNextNode() { + Optional> leftSibling = goToParentAndGetLeftSibling(); + if (leftSibling.isPresent()) + goToRightmostAncestor(leftSibling.get()); + } + + private Optional> goToParentAndGetLeftSibling() { + TreeNode node = path.removeEnd(); + Optional> parent = path.getEnd(); + if (parent.isPresent()) { + int leftSiblingIndex = node.getChildIndex().getAsInt() - 1; + return navigator + .getChild(parent.get().getElement(), leftSiblingIndex) + .map(ls -> SimpleTreeNode.innerNode(ls, leftSiblingIndex)); + } else + return Optional.empty(); + } + + private void goToRightmostAncestor(TreeNode leftSibling) { + Optional> rightmostChild = Optional.of(leftSibling); + while (rightmostChild.isPresent()) { + path.append(rightmostChild.get()); + int rightmostChildIndex = navigator.getChildrenCount(rightmostChild.get().getElement()) - 1; + rightmostChild = navigator + .getChild(rightmostChild.get().getElement(), rightmostChildIndex) + .map(child -> SimpleTreeNode.innerNode(child, rightmostChildIndex)); + } + } +} diff --git a/src/main/java/org/codefx/libfx/collection/tree/stream/DfsTreeIterationStrategy.java b/src/main/java/org/codefx/libfx/collection/tree/stream/DfsTreeIterationStrategy.java new file mode 100644 index 0000000..f9709ac --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/stream/DfsTreeIterationStrategy.java @@ -0,0 +1,134 @@ +package org.codefx.libfx.collection.tree.stream; + +import java.util.Objects; +import java.util.Optional; + +import org.codefx.libfx.collection.tree.navigate.TreeNavigator; + +/** + * A {@link TreeIterationStrategy} which uses depth-first + * search to iterate a tree's nodes. + *

+ * This implementation is only guaranteed to work on trees, i.e. a connected, directed, acyclic graph. Using it on other + * graphs can lead to unexpected behavior including infinite loops. + * + * @param + * the type of elements contained in the tree + */ +final class DfsTreeIterationStrategy implements TreeIterationStrategy { + + // #begin FIELDS + + private final TreeNavigator navigator; + + /** + * The path from the root to the last node returned by {@link #goToNextNode()} + *

+ * The path will also contain at least one node before the first call to {@link #goToNextNode()}. These are the + * nodes specified during construction and the one at the end of the path must be returned by the first call to + * {@code goToNextNode()}. Otherwise the end of the path (e.g. the root specified during construction) would never + * be returned by the strategy. Whether {@code goToNextNode()} must return this node or find a new one is indicated + * by {@link #beforeFirst}. + *

+ * If the path is empty, no more nodes will be returned. + */ + private final TreePath> path; + + /** + * Indicates whether {@link #goToNextNode()} was not already called at least once. + */ + private boolean beforeFirst; + + // #end FIELDS + + // #begin CONSTRUCTION + + /** + * Creates a new depth-first search strategy starting with the specified initial path. + *

+ * The iteration will begin with the node at the end of the initial path. Note that this does not make the node the + * root of the (sub-)tree over which this strategy iterates. Instead it will at some point try to find right + * siblings or uncles of this node. It will only stop when backtracking to the first node in that path, i.e. the + * subtree's root. + *

+ * The specified path must correspond to the navigator's view on the tree, i.e. each element in the path must be the + * parent (in the tree) of the next one. + *

+ * See {@link TreePathFactory} for easy ways to create an initial path. + * + * @param navigator + * the navigator used to navigate the tree + * @param initialPath + * the initial path from the root of the (sub-)tree iterated by this strategy; must contain at least one + * element; the last element in the path will be returned by the first call to {@link #goToNextNode()} + */ + public DfsTreeIterationStrategy(TreeNavigator navigator, TreePath> initialPath) { + Objects.requireNonNull(navigator, "The argument 'navigator' must not be null."); + Objects.requireNonNull(initialPath, "The argument 'initialPath' must not be null."); + if (initialPath.isEmpty()) + throw new IllegalArgumentException("The 'initialPath' must not be empty."); + + this.navigator = navigator; + this.path = initialPath; + this.beforeFirst = true; + } + + // #end CONSTRUCTION + + // #begin GO TO NEXT NODE + + @Override + public Optional goToNextNode() { + // if the path is empty, iteration ended and no more nodes will be returned + if (path.isEmpty()) + return Optional.empty(); + + // if this is the first call, do not move to the next node + if (beforeFirst) + beforeFirst = false; + else + movePathEndToNextNode(); + + return path.getEnd().map(TreeNode::getElement); + } + + private void movePathEndToNextNode() { + Optional> leftmostChild = getLeftmostChild(); + if (leftmostChild.isPresent()) + goToLeftmostChild(leftmostChild.get()); + else + goToRightSiblingOrUncle(); + } + + private Optional> getLeftmostChild() { + return path + .getEnd() + .flatMap(node -> navigator.getChild(node.getElement(), 0)) + .map(child -> SimpleTreeNode.innerNode(child, 0)); + } + + private void goToLeftmostChild(TreeNode leftmostChild) { + path.append(leftmostChild); + } + + private void goToRightSiblingOrUncle() { + Optional> siblingOrUncle = Optional.empty(); + + while (!siblingOrUncle.isPresent() && !path.isEmpty()) { + TreeNode currentNode = path.removeEnd(); + Optional> parentNode = path.getEnd(); + if (parentNode.isPresent()) { + E parentElement = parentNode.get().getElement(); + int rightSiblingIndex = currentNode.getChildIndex().getAsInt() + 1; + siblingOrUncle = navigator + .getChild(parentElement, rightSiblingIndex) + .map(sibling -> SimpleTreeNode.innerNode(sibling, rightSiblingIndex)); + } + } + + siblingOrUncle.ifPresent(path::append); + } + + // #end GO TO NEXT NODE + +} diff --git a/src/main/java/org/codefx/libfx/collection/tree/stream/SimpleTreeNode.java b/src/main/java/org/codefx/libfx/collection/tree/stream/SimpleTreeNode.java new file mode 100644 index 0000000..b2df113 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/stream/SimpleTreeNode.java @@ -0,0 +1,87 @@ +package org.codefx.libfx.collection.tree.stream; + +import java.util.Objects; +import java.util.OptionalInt; + +/** + * A straight forward implementation of {@link TreeNode}. + * + * @param + * the type of the contained element + */ +final class SimpleTreeNode implements TreeNode { + + private final E element; + + private final OptionalInt childIndex; + + private SimpleTreeNode(E element, OptionalInt childIndex) { + Objects.requireNonNull(element, "The argument 'element' must not be null."); + Objects.requireNonNull(childIndex, "The argument 'childIndex' must not be null."); + if (childIndex.isPresent() && childIndex.getAsInt() < 0) + throw new IllegalArgumentException("The 'childIndex' must be missing or non-negative."); + + this.element = element; + this.childIndex = childIndex; + } + + /** + * Creates a node for a node of a tree (possibly the root). + * + * @param + * the type of the content contained in the node + * @param element + * the element + * @param childIndex + * the index of the node within the list of children of its parent; as an {@link OptionalInt} because it + * can be empty if the node is the root + * @return a node containing the element + */ + public static SimpleTreeNode node(E element, OptionalInt childIndex) { + return new SimpleTreeNode<>(element, childIndex); + } + + /** + * Creates a node for the root of a (sub-)tree. + * + * @param + * the type of the content contained in the node + * @param element + * the root element + * @return a node containing the element + */ + public static SimpleTreeNode root(E element) { + return new SimpleTreeNode<>(element, OptionalInt.empty()); + } + + /** + * Creates a node for an inner node of a tree. + * + * @param + * the type of the content contained in the node + * @param element + * the element + * @param childIndex + * the index of the node within the list of children of its parent + * @return a node containing the element + */ + public static SimpleTreeNode innerNode(E element, int childIndex) { + return new SimpleTreeNode<>(element, OptionalInt.of(childIndex)); + } + + @Override + public E getElement() { + return element; + } + + @Override + public OptionalInt getChildIndex() { + return childIndex; + } + + @Override + public String toString() { + return "Node [" + element + ", " + childIndex + "]"; + } + +} diff --git a/src/main/java/org/codefx/libfx/collection/tree/stream/StackTreePath.java b/src/main/java/org/codefx/libfx/collection/tree/stream/StackTreePath.java new file mode 100644 index 0000000..d41975a --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/stream/StackTreePath.java @@ -0,0 +1,68 @@ +package org.codefx.libfx.collection.tree.stream; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; + +/** + * A {@link TreePath} which uses a stack to manage the path. + * + * @param + * the type of nodes in this path + */ +final class StackTreePath implements TreePath { + + /** + * The path from the root to the current node as a stack with the root at the bottom and the most recently visited + * node at the top. + */ + private final Deque path; + + /** + * Creates an empty path. + */ + public StackTreePath() { + this.path = new ArrayDeque<>(); + } + + /** + * Creates a new path which is initialized to the specified list of nodes. + *

+ * The list is interpreted as a path which starts in the list's first element. Hence for an initial path from + * {@code start} to {@code end} the list should be such that {@code initialPath.indexOf(start) == 0} and + * {@code initialPath.indexOf(end) == initialPath.size() - 1}. + * + * @param initialPath + * the initial path + */ + public StackTreePath(List initialPath) { + this(); + Objects.requireNonNull(initialPath, "The argument 'initialPath' must not be null."); + initialPath.forEach(this::append); + } + + @Override + public boolean isEmpty() { + return path.isEmpty(); + } + + @Override + public Optional getEnd() { + return Optional.ofNullable(path.peek()); + } + + @Override + public void append(N node) { + Objects.requireNonNull(node, "The argument 'node' must not be null."); + path.push(node); + } + + @Override + public N removeEnd() throws NoSuchElementException { + return path.pop(); + } + +} diff --git a/src/main/java/org/codefx/libfx/collection/tree/stream/TreeIterationStrategy.java b/src/main/java/org/codefx/libfx/collection/tree/stream/TreeIterationStrategy.java new file mode 100644 index 0000000..55b3ec4 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/stream/TreeIterationStrategy.java @@ -0,0 +1,18 @@ +package org.codefx.libfx.collection.tree.stream; + +import java.util.Optional; + +/** + * A strategy of how to navigate through a tree (i.e. a connected, directed, acyclic graph). + * + * @param + * the type of elements contained in the tree + */ +public interface TreeIterationStrategy { + + /** + * @return the next node; {@link Optional#empty() empty} if no next node exists + */ + Optional goToNextNode(); + +} diff --git a/src/main/java/org/codefx/libfx/collection/tree/stream/TreeIterator.java b/src/main/java/org/codefx/libfx/collection/tree/stream/TreeIterator.java new file mode 100644 index 0000000..b22c00f --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/stream/TreeIterator.java @@ -0,0 +1,91 @@ +package org.codefx.libfx.collection.tree.stream; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; + +/** + * An {@link Iterator} which uses a {@link TreeIterationStrategy} to iterate over a tree. + * + * @param + * the type of elements returned by this iterator + */ +class TreeIterator implements Iterator { + + /* + * The important task of choosing the next node in the tree is delegated to the iteration strategy. This class + * merely delays looking for that node as long as possible. Its state consists of a single node of the tree and + * whether it was already returned + */ + + // #begin FIELDS + + private final TreeIterationStrategy iterationStrategy; + + /** + * The next node to return; if it is {@link Optional#empty() empty}, the iteration will end. + */ + private Optional nextNode; + + /** + * Indicates whether the {@link #nextNode} was already returned as an element in {@link #next()}. + */ + private boolean returnedNextNode; + + // #end FIELDS + + // #begin CONSTRUCTION + + /** + * Creates a new tree iterator which uses the specified strategy to determine the order of nodes. + * + * @param iterationStrategy + * the strategy used by this iterator + */ + public TreeIterator(TreeIterationStrategy iterationStrategy) { + Objects.requireNonNull(iterationStrategy, "The argument 'iterationStrategy' must not be null."); + + this.iterationStrategy = iterationStrategy; + this.nextNode = Optional.empty(); + this.returnedNextNode = true; + } + + // #end CONSTRUCTION + + // #begin IMPLEMENTATION OF 'Iterator' + + @Override + public final boolean hasNext() { + goToNextNodeIfNecessary(); + return nextNode.isPresent(); + } + + @Override + public final E next() { + goToNextNodeIfNecessary(); + return returnNextNode(); + } + + // #end IMPLEMENTATION OF 'Iterator' + + // #begin GO TO NEXT NODE & RETURN + + private void goToNextNodeIfNecessary() { + if (returnedNextNode) { + nextNode = iterationStrategy.goToNextNode(); + returnedNextNode = false; + } + } + + private E returnNextNode() { + if (nextNode.isPresent()) { + returnedNextNode = true; + return nextNode.get(); + } else + throw new NoSuchElementException("All nodes in the tree have been visited."); + } + + // #end GO TO NEXT NODE & RETURN + +} diff --git a/src/main/java/org/codefx/libfx/collection/tree/stream/TreeNode.java b/src/main/java/org/codefx/libfx/collection/tree/stream/TreeNode.java new file mode 100644 index 0000000..d0f4ac8 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/stream/TreeNode.java @@ -0,0 +1,29 @@ +package org.codefx.libfx.collection.tree.stream; + +import java.util.OptionalInt; + +import org.codefx.libfx.collection.tree.navigate.TreeNavigator; + +/** + * Encapsulates an element in a tree. + *

+ * A {@code TreeNode} is useful to create a {@link TreeIterationStrategy} together with a {@link TreeNavigator}. The + * node's child index is stored in the node to reduce the number of {@link TreeNavigator#getChildIndex(Object) + * getChildIndex} calls made to the navigator. + * + * @param + * the type of the contained element + */ +interface TreeNode { + + /** + * @return the encapsulated element + */ + E getElement(); + + /** + * @return the index of the node within the list of children of its parent + */ + OptionalInt getChildIndex(); + +} diff --git a/src/main/java/org/codefx/libfx/collection/tree/stream/TreePath.java b/src/main/java/org/codefx/libfx/collection/tree/stream/TreePath.java new file mode 100644 index 0000000..82dbb1a --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/stream/TreePath.java @@ -0,0 +1,46 @@ +package org.codefx.libfx.collection.tree.stream; + +import java.util.NoSuchElementException; +import java.util.Optional; + +/** + * A path in a tree from some node to an ancestor. + *

+ * A {@code TreePath} is useful to create a {@link TreeIterationStrategy}. Such strategies (usually) only instrument the + * end of the current path (e.g. when appending nodes to move down the tree or removing them to move up). This interface + * is hence geared towards that use case and limits interaction with the path to its end, which can be {@link #getEnd() + * retrieved}, {@link #append(Object) appended} to and {@link #removeEnd() removed}. + * + * @param + * the type of nodes + */ +interface TreePath { + + /** + * @return whether this path is empty + */ + boolean isEmpty(); + + /** + * @return the end of this path if it exists; otherwise {@link Optional#empty() empty}. + */ + Optional getEnd(); + + /** + * Appends the specified node to the end of this path. + * + * @param node + * the node to append + */ + void append(N node); + + /** + * Removes (and returns) the end of this path. + * + * @return the former end node of this path + * @throws NoSuchElementException + * if the path {@link #isEmpty() isEmpty} + */ + N removeEnd() throws NoSuchElementException; + +} diff --git a/src/main/java/org/codefx/libfx/collection/tree/stream/TreePathFactory.java b/src/main/java/org/codefx/libfx/collection/tree/stream/TreePathFactory.java new file mode 100644 index 0000000..9afe551 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/stream/TreePathFactory.java @@ -0,0 +1,164 @@ +package org.codefx.libfx.collection.tree.stream; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; + +import org.codefx.libfx.collection.tree.navigate.TreeNavigator; + +/** + * Utility class to easily create {@link TreePath}s for recurring situations. + */ +class TreePathFactory { + + /** + * @param + * the type of elements contained in the tree + * @param node + * the only node of the returned path + * @return a tree path containing the node + */ + public static TreePath> createWithSingleNode(E node) { + Objects.requireNonNull(node, "The argument 'node' must not be null."); + return new StackTreePath<>(Collections.singletonList(SimpleTreeNode.root(node))); + } + + /** + * @param + * the type of elements contained in the tree + * @param navigator + * the navigator used to navigate the tree + * @param pathAsList + * the path as a list where the path' first node is the first element in the list + * @return a tree path containing the nodes for the elements in the list + */ + public static TreePath> createFromElementList(TreeNavigator navigator, List pathAsList) { + Objects.requireNonNull(navigator, "The argument 'navigator' must not be null."); + Objects.requireNonNull(pathAsList, "The argument 'pathAsList' must not be null."); + + TreePath> path = new StackTreePath<>(); + pathAsList.forEach(element -> { + OptionalInt childIndex = navigator.getChildIndex(element); + TreeNode node = SimpleTreeNode.node(element, childIndex); + path.append(node); + }); + return path; + } + + /** + * @param + * the type of elements contained in the tree + * @param navigator + * the navigator used to navigate the tree + * @param node + * a node in the tree + * @param descendant + * a descendant of the specified node + * @return a tree path leading from the node to the descendant + * @throws IllegalArgumentException + * if the node is no ancestor of the descendant + */ + public static TreePath> createFromNodeToDescendant( + TreeNavigator navigator, E node, E descendant) throws IllegalArgumentException { + + Objects.requireNonNull(navigator, "The argument 'navigator' must not be null."); + Objects.requireNonNull(node, "The argument 'node' must not be null."); + Objects.requireNonNull(descendant, "The argument 'descendant' must not be null."); + + if (node == descendant) + return createWithSingleNode(node); + + List> pathFromDescendantBackToNode = createPathFromDescendantBackToNode(navigator, node, descendant); + return createPathByInverting(pathFromDescendantBackToNode); + } + + private static List> createPathFromDescendantBackToNode( + TreeNavigator navigator, E node, E descendant) { + + List> pathFromDescendantToNode = createPathWithSingleNode(navigator, descendant); + Optional parent = addAllAncestorsToPathUntilReachingNode( + navigator, node, descendant, pathFromDescendantToNode); + addNodeToPathOrThrowException(navigator, node, parent, descendant, pathFromDescendantToNode); + return pathFromDescendantToNode; + } + + private static List> createPathWithSingleNode(TreeNavigator navigator, E descendant) { + List> pathFromStartBackToNode = new ArrayList<>(); + TreeNode descendantNode = SimpleTreeNode.node(descendant, navigator.getChildIndex(descendant)); + pathFromStartBackToNode.add(descendantNode); + return pathFromStartBackToNode; + } + + private static Optional addAllAncestorsToPathUntilReachingNode( + TreeNavigator navigator, E node, E descendant, List> pathFromStartBackToNode) { + + Optional parent = navigator.getParent(descendant); + while (parent.isPresent() && parent.get() != node) { + TreeNode parentNode = SimpleTreeNode.node(parent.get(), navigator.getChildIndex(parent.get())); + pathFromStartBackToNode.add(parentNode); + parent = navigator.getParent(parent.get()); + } + return parent; + } + + private static void addNodeToPathOrThrowException( + TreeNavigator navigator, E node, Optional parent, E descendant, + List> pathFromStartBackToNode) { + + if (!parent.isPresent() || parent.get() != node) + throw new IllegalArgumentException( + "The specified node '" + node + + "' is no ancestor of the specified descendant '" + descendant + "'."); + else { + TreeNode startNode = SimpleTreeNode.node(node, navigator.getChildIndex(node)); + pathFromStartBackToNode.add(startNode); + } + } + + private static TreePath> createPathByInverting(List> pathFromDescendantBackToNode) { + StackTreePath> treePath = new StackTreePath>(); + for (int i = pathFromDescendantBackToNode.size() - 1; i >= 0; i--) + treePath.append(pathFromDescendantBackToNode.get(i)); + return treePath; + } + + /** + * @param + * the type of elements contained in the tree + * @param navigator + * the navigator used to navigate the tree + * @param node + * a node in the tree + * @return a tree path leading from the root to the node + */ + public static TreePath> createFromRootToNode(TreeNavigator navigator, E node) { + Objects.requireNonNull(navigator, "The argument 'navigator' must not be null."); + Objects.requireNonNull(node, "The argument 'node' must not be null."); + + List> pathFromDescendantBackToNode = createPathFromNodeBackToRoot(navigator, node); + return createPathByInverting(pathFromDescendantBackToNode); + } + + private static List> createPathFromNodeBackToRoot( + TreeNavigator navigator, E node) { + + List> pathFromNodeToRoot = createPathWithSingleNode(navigator, node); + addAllAncestorNodesToPath(navigator, node, pathFromNodeToRoot); + return pathFromNodeToRoot; + } + + private static void addAllAncestorNodesToPath( + TreeNavigator navigator, E node, List> pathFromStartBackToNode) { + + Optional parent = navigator.getParent(node); + while (parent.isPresent()) { + TreeNode parentNode = SimpleTreeNode.node(parent.get(), navigator.getChildIndex(parent.get())); + pathFromStartBackToNode.add(parentNode); + parent = navigator.getParent(parent.get()); + } + } + +} diff --git a/src/main/java/org/codefx/libfx/collection/tree/stream/TreeStreams.java b/src/main/java/org/codefx/libfx/collection/tree/stream/TreeStreams.java new file mode 100644 index 0000000..6be732a --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/stream/TreeStreams.java @@ -0,0 +1,182 @@ +package org.codefx.libfx.collection.tree.stream; + +import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.codefx.libfx.collection.tree.navigate.TreeNavigator; + +/** + * Creates streams of nodes. + *

+ * Unless otherwise noted all streams are {@link Spliterator#ORDERED ordered} and sequential. They are generated lazily, + * i.e. the {@link TreeNavigator} is only used to find nodes which are known to be processed by the stream. This is + * implies that short-circuiting operation (like {@link Stream#limit(long) limit}) will lead to the evaluation of less + * nodes. + *

+ * The streams are only defined on trees, i.e. connected, directed, acyclic graphs. Creating them on other graphs can + * lead to unexpected behavior including infinite streams. + */ +public class TreeStreams { + + // #begin DFS + + // forward + + /** + * Returns a stream which enumerates nodes in the (sub-)tree rooted in the specified root in the order of a depth-first search. + *

+ * It is not necessary for the specified node to be the tree's actual root. It will be treated as the root of a + * subtree and only this subtree will be streamed. + * + * @param + * the type of nodes contained in the tree + * @param navigator + * the navigator used to navigate the tree + * @param root + * the root node for the searched (sub-)tree + * @return a stream of nodes + */ + public static Stream dfsFromRoot(TreeNavigator navigator, N root) { + TreePath> initialPath = TreePathFactory.createWithSingleNode(root); + TreeIterationStrategy strategy = new DfsTreeIterationStrategy(navigator, initialPath); + return byStrategy(strategy); + } + + /** + * Returns a stream which enumerates nodes in a tree in the order of a depth-first search which starts in the specified + * node. + *

+ * While the search will start in the specified node it will eventually backtrack above it, i.e. the search is not + * limited to the subtree rooted in the node. This is equivalent to starting a full depth-first search in the tree's + * root but ignoring all encountered nodes until the specified start node is found. + * + * @param + * the type of nodes contained in the tree + * @param navigator + * the navigator used to navigate the tree + * @param startNode + * the node in which the search starts + * @return a stream of nodes + */ + public static Stream dfsFromWithin(TreeNavigator navigator, N startNode) { + TreePath> initialPath = TreePathFactory.createFromRootToNode(navigator, startNode); + TreeIterationStrategy strategy = new DfsTreeIterationStrategy(navigator, initialPath); + return byStrategy(strategy); + } + + /** + * Returns a stream which enumerates nodes in the (sub-)tree rooted in the specified root in the order of a depth-first search which starts in the specified + * start node. + *

+ * This is a combination of {@link #dfsFromRoot(TreeNavigator, Object) dfsFromRoot} and + * {@link #dfsFromWithin(TreeNavigator, Object) dfsFromWithin}. Only the (sub-)tree rooted in the specified root is + * searched (i.e. the search will never backtrack above it) but the root actually startes in the specified start + * node. + * + * @param + * the type of nodes contained in the tree + * @param navigator + * the navigator used to navigate the tree + * @param root + * the root node for the searched (sub-)tree + * @param startNode + * the node in which the search starts + * @return a stream of nodes + */ + public static Stream dfsFromWithin(TreeNavigator navigator, N root, N startNode) { + TreePath> initialPath = TreePathFactory.createFromNodeToDescendant(navigator, root, startNode); + TreeIterationStrategy strategy = new DfsTreeIterationStrategy(navigator, initialPath); + return byStrategy(strategy); + } + + // backward + + /** + * Returns a stream which enumerates nodes in a tree in the order of a backwards depth-first search which starts in the specified + * node. + *

+ * The stream enumerates the nodes which would be encountered by starting a depth-first search in the tree's root + * and stopping at the specified node, but in reverse order. + * + * @param + * the type of nodes contained in the tree + * @param navigator + * the navigator used to navigate the tree + * @param startNode + * the node in which the search starts + * @return a stream of nodes + */ + public static Stream backwardDfs(TreeNavigator navigator, N startNode) { + TreePath> initialPath = TreePathFactory.createFromRootToNode(navigator, startNode); + TreeIterationStrategy strategy = new DfsTreeIterationStrategy(navigator, initialPath); + return byStrategy(strategy); + } + + /** + * Returns a stream which enumerates nodes in the (sub-)tree rooted in the specified root in the order of a + * backwards depth-first search which starts + * in the specified node. + *

+ * The stream enumerates the nodes which would be encountered by starting a depth-first search in the specified root + * and stopping at the specified node, but in reverse order. + * + * @param + * the type of nodes contained in the tree + * @param navigator + * the navigator used to navigate the tree + * @param root + * the root node for the searched (sub-)tree + * @param startNode + * the node in which the search starts + * @return a stream of nodes + */ + public static Stream backwardDfsToRoot(TreeNavigator navigator, N root, N startNode) { + TreePath> initialPath = TreePathFactory.createFromNodeToDescendant(navigator, root, startNode); + TreeIterationStrategy strategy = new DfsTreeIterationStrategy(navigator, initialPath); + return byStrategy(strategy); + } + + // #end DFS + + /** + * Returns a stream which enumerates a tree's nodes according to the specified {@link TreeIterationStrategy}. + * + * @param + * the type of nodes contained in the tree + * @param strategy + * the strategy used to enumerate the tree's nodes + * @return a stream of nodes + */ + public static Stream byStrategy(TreeIterationStrategy strategy) { + return byStrategy(strategy, Spliterator.NONNULL | Spliterator.ORDERED, false); + } + + /** + * Returns a stream which enumerates a tree's nodes according to the specified {@link TreeIterationStrategy} and + * stream characteristics. + * + * @param + * the type of nodes contained in the tree + * @param strategy + * the strategy used to enumerate the tree's nodes + * @param characteristics + * the characteristics of the {@link Spliterator} used to create the stream + * @param parallel + * if true then the returned stream is a parallel stream; if false the returned stream is a sequential + * stream. + * @return a stream of nodes + */ + public static Stream byStrategy(TreeIterationStrategy strategy, int characteristics, boolean parallel) { + Iterator iterator = new TreeIterator(strategy); + Spliterator spliterator = Spliterators.spliteratorUnknownSize(iterator, characteristics); + return StreamSupport.stream(spliterator, parallel); + } + +} diff --git a/src/main/java/org/codefx/libfx/collection/tree/stream/package-info.java b/src/main/java/org/codefx/libfx/collection/tree/stream/package-info.java new file mode 100644 index 0000000..02d4761 --- /dev/null +++ b/src/main/java/org/codefx/libfx/collection/tree/stream/package-info.java @@ -0,0 +1,15 @@ +/** + * Provides the possibility to stream the elements of tree-like data structures. + *

+ * Its main purpose is to enable easy creation of {@link java.util.stream.Stream Stream}s of nodes over tree-like data + * structures without requiring them to implement a specific interface. Instead of utilizing such an interface to + * navigate the tree, a {@link org.codefx.libfx.collection.tree.navigate.TreeNavigator TreeNavigator} is used. + *

+ * Use {@link org.codefx.libfx.collection.tree.stream.TreeStreams TreeStreams} to create such streams. If the existing + * iteration strategies (like, e.g., depth-first search) do not suffice, a + * {@link org.codefx.libfx.collection.tree.stream.TreeIterationStrategy TreeIterationStrategy} can be specified. The + * strategy might make use of a {@link org.codefx.libfx.collection.tree.navigate.TreeNavigator TreeNavigator}. + */ +package org.codefx.libfx.collection.tree.stream; + diff --git a/src/main/java/org/codefx/libfx/concurrent/when/ExecuteAlwaysWhen.java b/src/main/java/org/codefx/libfx/concurrent/when/ExecuteAlwaysWhen.java index f24d93d..a7fc307 100644 --- a/src/main/java/org/codefx/libfx/concurrent/when/ExecuteAlwaysWhen.java +++ b/src/main/java/org/codefx/libfx/concurrent/when/ExecuteAlwaysWhen.java @@ -58,7 +58,7 @@ public class ExecuteAlwaysWhen { * on the first execution of the action. */ - // #region FIELDS + // #begin FIELDS /** * The {@link ObservableValue} upon whose value the action's execution depends. @@ -110,6 +110,10 @@ public class ExecuteAlwaysWhen { * the action which will be executed */ ExecuteAlwaysWhen(ObservableValue observable, Predicate condition, Consumer action) { + assert observable != null : "The argument 'observable' must not be null."; + assert condition != null : "The argument 'condition' must not be null."; + assert action != null : "The argument 'action' must not be null."; + this.observable = observable; this.condition = condition; this.action = action; @@ -120,7 +124,7 @@ public class ExecuteAlwaysWhen { alreadyExecuted = new AtomicBoolean(false); } - // #region METHODS + // #begin METHODS /** * Executes the action (every time) when the observable's value passes the condition. diff --git a/src/main/java/org/codefx/libfx/concurrent/when/ExecuteOnceWhen.java b/src/main/java/org/codefx/libfx/concurrent/when/ExecuteOnceWhen.java index 9e9f7e4..f16dfbf 100644 --- a/src/main/java/org/codefx/libfx/concurrent/when/ExecuteOnceWhen.java +++ b/src/main/java/org/codefx/libfx/concurrent/when/ExecuteOnceWhen.java @@ -25,7 +25,7 @@ * value (the one tested during {@code executeWhen()}) or one of several which were set by those threads. *

* Use {@link ExecuteWhen} to build an instance of this class. - * + * * @param * the type the observed {@link ObservableValue}'s wraps */ @@ -48,7 +48,7 @@ public class ExecuteOnceWhen { * condition, it is checked (and set to false). If it contained true, the action will be executed. */ - // #region FIELDS + // #begin FIELDS /** * The {@link ObservableValue} upon whose value the action's execution depends. @@ -96,6 +96,10 @@ public class ExecuteOnceWhen { * the action which will be executed */ ExecuteOnceWhen(ObservableValue observable, Predicate condition, Consumer action) { + assert observable != null : "The argument 'observable' must not be null."; + assert condition != null : "The argument 'condition' must not be null."; + assert action != null : "The argument 'action' must not be null."; + this.observable = observable; this.condition = condition; this.action = action; @@ -105,7 +109,7 @@ public class ExecuteOnceWhen { willExecute = new AtomicBoolean(true); } - // #region METHODS + // #begin METHODS /** * Executes the action (once) when the observable's value passes the condition. diff --git a/src/main/java/org/codefx/libfx/concurrent/when/ExecuteWhen.java b/src/main/java/org/codefx/libfx/concurrent/when/ExecuteWhen.java index cd80fe6..95e4049 100644 --- a/src/main/java/org/codefx/libfx/concurrent/when/ExecuteWhen.java +++ b/src/main/java/org/codefx/libfx/concurrent/when/ExecuteWhen.java @@ -26,7 +26,7 @@ */ public class ExecuteWhen { - // #region FIELDS + // #begin FIELDS /** * The {@link ObservableValue} upon whose value the action's execution depends. @@ -40,7 +40,7 @@ public class ExecuteWhen { // #end FIELDS - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new instance for the specified observable @@ -49,6 +49,8 @@ public class ExecuteWhen { * the {@link ObservableValue} which will be observed by the created {@code Execute...When} instances */ private ExecuteWhen(ObservableValue observable) { + Objects.requireNonNull(observable, "The argument 'observable' must not be null."); + this.observable = observable; condition = Optional.empty(); } @@ -68,7 +70,7 @@ public static ExecuteWhen on(ObservableValue observable) { // #end CONSTRUCTION - // #region SETTING VALUES + // #begin SETTING VALUES /** * Specifies the condition the observable's value must fulfill in order for the action to be executed. @@ -79,13 +81,14 @@ public static ExecuteWhen on(ObservableValue observable) { */ public ExecuteWhen when(Predicate condition) { Objects.requireNonNull(condition, "The argument 'condition' must not be null."); + this.condition = Optional.of(condition); return this; } // #end SETTING VALUES - // #region BUILD + // #begin BUILD /** * Creates an instance which: @@ -104,6 +107,8 @@ public ExecuteWhen when(Predicate condition) { * if {@link #when(Predicate)} was not called */ public ExecuteOnceWhen thenOnce(Consumer action) throws IllegalStateException { + Objects.requireNonNull(action, "The argument 'action' must not be null."); + ensureConditionWasSet(); return new ExecuteOnceWhen(observable, condition.get(), action); } @@ -125,6 +130,8 @@ public ExecuteOnceWhen thenOnce(Consumer action) throws IllegalSta * if {@link #when(Predicate)} was not called */ public ExecuteAlwaysWhen thenAlways(Consumer action) throws IllegalStateException { + Objects.requireNonNull(action, "The argument 'action' must not be null."); + ensureConditionWasSet(); return new ExecuteAlwaysWhen(observable, condition.get(), action); } @@ -139,7 +146,7 @@ private void ensureConditionWasSet() throws IllegalStateException { boolean noCondition = !condition.isPresent(); if (noCondition) throw new IllegalStateException( - "Set a condition with 'when(Predicate)' before calling any 'then' method."); + "Set a condition with 'when(Predicate)' before calling any 'then...' method."); } // #end BUILD diff --git a/src/main/java/org/codefx/libfx/concurrent/when/package-info.java b/src/main/java/org/codefx/libfx/concurrent/when/package-info.java index d339b82..039436f 100644 --- a/src/main/java/org/codefx/libfx/concurrent/when/package-info.java +++ b/src/main/java/org/codefx/libfx/concurrent/when/package-info.java @@ -1,10 +1,10 @@ /** - * With {@link org.codefx.libfx.concurrent.when.ExecuteOnceWhen ExecuteOnceWhen} and - * {@link org.codefx.libfx.concurrent.when.ExecuteAlwaysWhen ExecuteAlwaysWhen} this package provides two classes which - * help to make sure some action which is triggered by a change on an {@link javafx.beans.value.ObservableValue - * ObservableValue} gets executed under threading. Refer to the two classes for a detailed description. + * Allows to make sure some action which is triggered by a change on an {@link javafx.beans.value.ObservableValue + * ObservableValue} gets executed under threading. *

- * Instances of those classes can be built with {@link org.codefx.libfx.concurrent.when.ExecuteWhen ExecuteWhen}. + * Refer to the two classes {@link org.codefx.libfx.concurrent.when.ExecuteOnceWhen ExecuteOnceWhen} and + * {@link org.codefx.libfx.concurrent.when.ExecuteAlwaysWhen ExecuteAlwaysWhen} for a detailed description. Instances of + * those classes can be built with {@link org.codefx.libfx.concurrent.when.ExecuteWhen ExecuteWhen}. * * @see org.codefx.libfx.concurrent.when.ExecuteWhen ExecuteWhen */ diff --git a/src/main/java/org/codefx/libfx/control/package-info.java b/src/main/java/org/codefx/libfx/control/package-info.java index f0a6a20..fe8d819 100644 --- a/src/main/java/org/codefx/libfx/control/package-info.java +++ b/src/main/java/org/codefx/libfx/control/package-info.java @@ -1,6 +1,6 @@ /** - * This package provides functionality around UI Controls. Subpackages might provide additional functionality for - * existing Swing or JavaFX controls, implement new ones or provide other features related to controls. + * Provides functionality around UI Controls. Subpackages might provide additional functionality for existing Swing or + * JavaFX controls, implement new ones or provide other features related to controls. */ package org.codefx.libfx.control; diff --git a/src/main/java/org/codefx/libfx/control/properties/AbstractControlPropertyListenerHandle.java b/src/main/java/org/codefx/libfx/control/properties/AbstractControlPropertyListenerHandle.java index 2434a52..f93519d 100644 --- a/src/main/java/org/codefx/libfx/control/properties/AbstractControlPropertyListenerHandle.java +++ b/src/main/java/org/codefx/libfx/control/properties/AbstractControlPropertyListenerHandle.java @@ -11,7 +11,7 @@ */ abstract class AbstractControlPropertyListenerHandle implements ControlPropertyListenerHandle { - // #region FIELDS + // #begin FIELDS /** * The properties to which the {@link #listener} will be added. @@ -35,7 +35,7 @@ abstract class AbstractControlPropertyListenerHandle implements ControlPropertyL // #end FIELDS - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new listener handle. Initially detached. @@ -74,7 +74,7 @@ private MapChangeListener createListener(Object key) { // #end CONSTRUCTION - // #region PROCESS VALUE + // #begin PROCESS VALUE /** * Processes the specified value for the {@link #key} before removing the pair from the {@link #properties} @@ -98,7 +98,7 @@ private void processAndRemoveValue(Object value) { // #end PROCESS VALUE - // #region IMPLEMENTATION OF 'ControlPropertyListenerHandle' + // #begin IMPLEMENTATION OF 'ControlPropertyListenerHandle' @Override public void attach() { diff --git a/src/main/java/org/codefx/libfx/control/properties/ControlPropertyListenerBuilder.java b/src/main/java/org/codefx/libfx/control/properties/ControlPropertyListenerBuilder.java index 4b4a1d9..f2b17bd 100644 --- a/src/main/java/org/codefx/libfx/control/properties/ControlPropertyListenerBuilder.java +++ b/src/main/java/org/codefx/libfx/control/properties/ControlPropertyListenerBuilder.java @@ -35,7 +35,7 @@ */ public class ControlPropertyListenerBuilder { - // #region FIELDS + // #begin FIELDS /** * The properties which will be observed. @@ -59,7 +59,7 @@ public class ControlPropertyListenerBuilder { // #end FIELDS - // #region CONSTRUCTION & SETTING VALUES + // #begin CONSTRUCTION & SETTING VALUES /** * Creates a new builder. @@ -136,7 +136,7 @@ public ControlPropertyListenerBuilder processValue(Consumer valueP // #end CONSTRUCTION & SETTING VALUES - // #region BUILD + // #begin BUILD /** * Creates a new property listener according to the arguments specified before and diff --git a/src/main/java/org/codefx/libfx/control/properties/package-info.java b/src/main/java/org/codefx/libfx/control/properties/package-info.java index 2bd979e..2214dfb 100644 --- a/src/main/java/org/codefx/libfx/control/properties/package-info.java +++ b/src/main/java/org/codefx/libfx/control/properties/package-info.java @@ -1,8 +1,8 @@ /** + * Makes using a {@link javafx.scene.control.Control Control}'s {@link javafx.scene.control.Control#getProperties() + * propertyMap} more convenient. *

- * This package provides functionality to make using a {@link javafx.scene.control.Control Control}'s - * {@link javafx.scene.control.Control#getProperties() propertyMap} easier. As such its main use will be to creators of - * controls. + * As such its main use will be to creators of controls. *

*

Listening to the Property Map

*

diff --git a/src/main/java/org/codefx/libfx/control/webview/DefaultWebViewHyperlinkListenerHandle.java b/src/main/java/org/codefx/libfx/control/webview/DefaultWebViewHyperlinkListenerHandle.java index b56374c..4e9389e 100644 --- a/src/main/java/org/codefx/libfx/control/webview/DefaultWebViewHyperlinkListenerHandle.java +++ b/src/main/java/org/codefx/libfx/control/webview/DefaultWebViewHyperlinkListenerHandle.java @@ -37,7 +37,7 @@ class DefaultWebViewHyperlinkListenerHandle implements WebViewHyperlinkListenerH * it easier, all instances of org.w3c.dom classes carry a 'dom'-prefix. */ - // #region FIELDS + // #begin FIELDS /** * The {@link WebView} to which the {@link #domEventListener} will be attached. @@ -104,7 +104,7 @@ public DefaultWebViewHyperlinkListenerHandle( domEventListener = this::callHyperlinkListenerWithEvent; } - // #region ATTACH + // #begin ATTACH @Override public void attach() { @@ -149,7 +149,7 @@ private void attachListenerInApplicationThread() { // #end ATTACH - // #region DETACH + // #begin DETACH @Override public void detach() { @@ -185,7 +185,7 @@ private void detachInApplicationThread() { // #end DETACH - // #region COMMON MANAGEMENT METHODS + // #begin COMMON MANAGEMENT METHODS /** * Executes the specified function on each link in the {@link #webView}'s current document for each @@ -245,7 +245,7 @@ private boolean manageListenerForEventType(DomEventType domEventType) { // #end COMMON MANAGEMENT METHODS - // #region PROCESS EVENT + // #begin PROCESS EVENT /** * Converts the specified {@code domEvent} into a {@link HyperlinkEvent} and calls the {@link #eventListener} with diff --git a/src/main/java/org/codefx/libfx/control/webview/WebViews.java b/src/main/java/org/codefx/libfx/control/webview/WebViews.java index de507a2..f053ba5 100644 --- a/src/main/java/org/codefx/libfx/control/webview/WebViews.java +++ b/src/main/java/org/codefx/libfx/control/webview/WebViews.java @@ -25,7 +25,7 @@ private WebViews() { // nothing to do } - // #region HYPERLINK LISTENERS + // #begin HYPERLINK LISTENERS // create listener handles @@ -146,7 +146,7 @@ private static WebViewHyperlinkListenerHandle addHyperlinkListenerDetached( // #end HYPERLINK LISTENERS - // #region EVENTS + // #begin EVENTS /** * Indicates whether the specified DOM event can be converted to a {@link HyperlinkEvent}. @@ -206,7 +206,7 @@ public static String hyperlinkEventToString(HyperlinkEvent event) { // #end HYPERLINK EVENTS - // #region DOM EVENTS + // #begin DOM EVENTS /** * Returns a string representation of the specified event. diff --git a/src/main/java/org/codefx/libfx/control/webview/package-info.java b/src/main/java/org/codefx/libfx/control/webview/package-info.java index c1d402c..efca17c 100644 --- a/src/main/java/org/codefx/libfx/control/webview/package-info.java +++ b/src/main/java/org/codefx/libfx/control/webview/package-info.java @@ -1,7 +1,7 @@ /** *

- * This package provides functionality around JavaFX' {@link javafx.scene.web.WebView WebView}. All of it can be - * accessed via the utility class {@link org.codefx.libfx.control.webview.WebViews WebViews}. + * Provides functionality surrounding JavaFX' {@link javafx.scene.web.WebView WebView}. All of it can be accessed via + * the utility class {@link org.codefx.libfx.control.webview.WebViews WebViews}. *

*

Hyperlink Listener

*

diff --git a/src/main/java/org/codefx/libfx/dom/DomEventType.java b/src/main/java/org/codefx/libfx/dom/DomEventType.java index 66151e1..487f263 100644 --- a/src/main/java/org/codefx/libfx/dom/DomEventType.java +++ b/src/main/java/org/codefx/libfx/dom/DomEventType.java @@ -14,7 +14,7 @@ */ public enum DomEventType { - // #region INSTANCES + // #begin INSTANCES /** * A mouse click. @@ -50,7 +50,7 @@ public enum DomEventType { // #end INSTANCES - // #region DEFINITION + // #begin DEFINITION /** * The event's name. @@ -78,7 +78,7 @@ public String getDomName() { // #end DEFINITION - // #region HELPER + // #begin HELPER /** * Returns the DOM event type for the specified event name. diff --git a/src/main/java/org/codefx/libfx/dom/SingleDomEventConverter.java b/src/main/java/org/codefx/libfx/dom/SingleDomEventConverter.java index 0d3d35c..a54f09c 100644 --- a/src/main/java/org/codefx/libfx/dom/SingleDomEventConverter.java +++ b/src/main/java/org/codefx/libfx/dom/SingleDomEventConverter.java @@ -46,7 +46,7 @@ public SingleDomEventConverter(Event domEvent, Object source) { this.source = source; } - // #region CONVERT + // #begin CONVERT /** * Indicates whether the DOM event specified during construction can be converted to a {@link HyperlinkEvent}. diff --git a/src/main/java/org/codefx/libfx/dom/package-info.java b/src/main/java/org/codefx/libfx/dom/package-info.java index 8ca8a50..a4fc6e1 100644 --- a/src/main/java/org/codefx/libfx/dom/package-info.java +++ b/src/main/java/org/codefx/libfx/dom/package-info.java @@ -1,6 +1,6 @@ /** *

- * This package provides functionality around DOM, i.e. classes from {@code org.w3c.dom}. + * Provides functionality around DOM, i.e. classes from {@code org.w3c.dom}. *

*

Event Conversion

The class {@link org.codefx.libfx.dom.DomEventConverter DomEventConverter} defines methods * which allow the conversion of {@link org.w3c.dom.events.Event DOM Events} to {@link javax.swing.event.HyperlinkEvent diff --git a/src/main/java/org/codefx/libfx/listener/handle/GenericListenerHandle.java b/src/main/java/org/codefx/libfx/listener/handle/GenericListenerHandle.java index eac4d01..9f07fb8 100644 --- a/src/main/java/org/codefx/libfx/listener/handle/GenericListenerHandle.java +++ b/src/main/java/org/codefx/libfx/listener/handle/GenericListenerHandle.java @@ -15,7 +15,7 @@ */ final class GenericListenerHandle implements ListenerHandle { - // #region FIELDS + // #begin FIELDS /** * The observable instance to which the {@link #listener} will be added. @@ -44,7 +44,7 @@ final class GenericListenerHandle implements ListenerHandle { // #end FIELDS - // #region CONSTRUCITON + // #begin CONSTRUCITON /** * Creates a new listener handle for the specified arguments. The listener is initially detached. @@ -74,7 +74,7 @@ public GenericListenerHandle( // #end CONSTRUCITON - // #region IMPLEMENTATION OF 'ListenerHandle' + // #begin IMPLEMENTATION OF 'ListenerHandle' @Override public void attach() { diff --git a/src/main/java/org/codefx/libfx/listener/handle/ListenerAttachHandle.java b/src/main/java/org/codefx/libfx/listener/handle/ListenerAttachHandle.java new file mode 100644 index 0000000..534d817 --- /dev/null +++ b/src/main/java/org/codefx/libfx/listener/handle/ListenerAttachHandle.java @@ -0,0 +1,16 @@ +package org.codefx.libfx.listener.handle; + +/** + * A listener handle can be used to {@link #attach() attach} a listener to some observable instance. + * + * @see ListenerHandle + */ +public interface ListenerAttachHandle { + + /** + * Adds the listener to the observable. Calling this method when the listener is already added is a no-op and will + * not result in the listener being called more than once. + */ + void attach(); + +} diff --git a/src/main/java/org/codefx/libfx/listener/handle/ListenerDetachHandle.java b/src/main/java/org/codefx/libfx/listener/handle/ListenerDetachHandle.java new file mode 100644 index 0000000..096b5f1 --- /dev/null +++ b/src/main/java/org/codefx/libfx/listener/handle/ListenerDetachHandle.java @@ -0,0 +1,15 @@ +package org.codefx.libfx.listener.handle; + +/** + * A listener handle can be used to {@link #detach() detach} a listener from some observable instance. + * + * @see ListenerHandle + */ +public interface ListenerDetachHandle { + + /** + * Removes the listener from the observable. Calling this method when the listener is not added is a no-op. + */ + void detach(); + +} diff --git a/src/main/java/org/codefx/libfx/listener/handle/ListenerHandle.java b/src/main/java/org/codefx/libfx/listener/handle/ListenerHandle.java index 735ab58..6bfbc55 100644 --- a/src/main/java/org/codefx/libfx/listener/handle/ListenerHandle.java +++ b/src/main/java/org/codefx/libfx/listener/handle/ListenerHandle.java @@ -2,8 +2,10 @@ /** * A listener handle can be used to {@link #attach() attach} and {@link #detach() detach} a listener to/from some - * observable instance. Using the handler the calling code must not manage references to both the observed instance and - * the listener, which can improve readability. + * observable instance. + *

+ * Using the handler the calling code does not have to manage references to both the observed instance and the listener, + * which can improve readability. *

* A handle is created and returned by methods which connect a listener with an observable instance. This usually means * that the listener is actually added to the observable but it is also possible to simply return a handler and wait for @@ -12,17 +14,8 @@ * Unless otherwise noted it is not safe to share a handle between different threads. The behavior is undefined if * parallel calls are made to {@code attach()} and/or {@code detach()}. */ -public interface ListenerHandle { - - /** - * Adds the listener to the observable. Calling this method when the listener is already added is a no-op and will - * not result in the listener being called more than once. - */ - void attach(); +public interface ListenerHandle extends ListenerAttachHandle, ListenerDetachHandle { - /** - * Removes the listener from the observable. Calling this method when the listener is not added is a no-op. - */ - void detach(); + // no additional methods defined } diff --git a/src/main/java/org/codefx/libfx/listener/handle/ListenerHandleBuilder.java b/src/main/java/org/codefx/libfx/listener/handle/ListenerHandleBuilder.java index d935457..0b5300f 100644 --- a/src/main/java/org/codefx/libfx/listener/handle/ListenerHandleBuilder.java +++ b/src/main/java/org/codefx/libfx/listener/handle/ListenerHandleBuilder.java @@ -34,6 +34,18 @@ * .onDetach((property, listener) -> property.removeListener(listener)) * .build(); * + * Or, with method references: + * + *

+ * Property<String> textProperty;
+ * ChangeListener<String> textListener;
+ *
+ * ListenerHandle textListenerHandle = ListenerHandleBuilder
+ * 	.from(textProperty, textListener)
+ * 	.onAttach(Property::addListener))
+ * 	.onDetach(Property::removeListener)
+ * 	.build();
+ * 
* * @param * the type of the observable instance (e.g {@link javafx.beans.value.ObservableValue ObservableValue} or @@ -43,7 +55,7 @@ */ public final class ListenerHandleBuilder { - // #region FIELDS + // #begin FIELDS /** * The observable instance to which the {@link #listener} will be added. @@ -67,7 +79,7 @@ public final class ListenerHandleBuilder { // #end FIELDS - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a builder for a generic {@link ListenerHandle}. @@ -108,7 +120,7 @@ public static ListenerHandleBuilder from(O observable, L listener) // #end CONSTRUCTION - // #region SET AND BUILD + // #begin SET AND BUILD /** * Sets the function which is executed when the built {@link ListenerHandle} must add the listener because @@ -182,37 +194,37 @@ public ListenerHandle buildDetached() throws IllegalStateException { * if {@link #add} or {@link #remove} is empty. */ private void verifyAddAndRemovePresent() throws IllegalStateException { - boolean onAttachNotCalled = !add.isPresent(); - boolean onDetachNotCalled = !remove.isPresent(); - boolean canBuild = !onAttachNotCalled && !onDetachNotCalled; + boolean onAttachCalled = add.isPresent(); + boolean onDetachCalled = remove.isPresent(); + boolean canBuild = onAttachCalled && onDetachCalled; if (canBuild) return; else - throwExceptionForMissingCall(onAttachNotCalled, onDetachNotCalled); + throwExceptionForMissingCall(onAttachCalled, onDetachCalled); } /** * Throws an {@link IllegalStateException} for a missing call. * - * @param onAttachNotCalled + * @param onAttachCalled * indicates whether {@link #onAttach(BiConsumer)} has been called - * @param onDetachNotCalled + * @param onDetachCalled * indicates whether {@link #onDetach(BiConsumer)} has been called * @throws IllegalStateException * if at least one of the specified booleans is true */ - private static void throwExceptionForMissingCall(boolean onAttachNotCalled, boolean onDetachNotCalled) + private static void throwExceptionForMissingCall(boolean onAttachCalled, boolean onDetachCalled) throws IllegalStateException { - if (onAttachNotCalled && onDetachNotCalled) + if (!onAttachCalled && !onDetachCalled) throw new IllegalStateException( "A listener handle can not be build until 'onAttach' and 'onDetach' have been called."); - if (onAttachNotCalled) + if (!onAttachCalled) throw new IllegalStateException("A listener handle can not be build until 'onAttach' has been called."); - if (onDetachNotCalled) + if (!onDetachCalled) throw new IllegalStateException("A listener handle can not be build until 'onDetach' has been called."); } diff --git a/src/main/java/org/codefx/libfx/listener/handle/ListenerHandles.java b/src/main/java/org/codefx/libfx/listener/handle/ListenerHandles.java index 87c9bea..c12ecf8 100644 --- a/src/main/java/org/codefx/libfx/listener/handle/ListenerHandles.java +++ b/src/main/java/org/codefx/libfx/listener/handle/ListenerHandles.java @@ -73,8 +73,8 @@ public static ListenerHandle createAttached(Observable observable, InvalidationL public static ListenerHandle createDetached(Observable observable, InvalidationListener invalidationListener) { return ListenerHandleBuilder .from(observable, invalidationListener) - .onAttach((obs, listener) -> obs.addListener(listener)) - .onDetach((obs, listener) -> obs.removeListener(listener)) + .onAttach(Observable::addListener) + .onDetach(Observable::removeListener) .buildDetached(); } @@ -115,8 +115,8 @@ public static ListenerHandle createDetached( return ListenerHandleBuilder .from(observableValue, changeListener) - .onAttach((observable, listener) -> observable.addListener(listener)) - .onDetach((observable, listener) -> observable.removeListener(listener)) + .onAttach(ObservableValue::addListener) + .onDetach(ObservableValue::removeListener) .buildDetached(); } @@ -157,8 +157,8 @@ public static > ListenerHandle createDetached( return ListenerHandleBuilder .from(observableArray, changeListener) - .onAttach((observable, listener) -> observable.addListener(listener)) - .onDetach((observable, listener) -> observable.removeListener(listener)) + .onAttach(ObservableArray::addListener) + .onDetach(ObservableArray::removeListener) .buildDetached(); } @@ -199,8 +199,8 @@ public static ListenerHandle createDetached( return ListenerHandleBuilder .from(observableList, changeListener) - .onAttach((observable, listener) -> observable.addListener(listener)) - .onDetach((observable, listener) -> observable.removeListener(listener)) + .onAttach(ObservableList::addListener) + .onDetach(ObservableList::removeListener) .buildDetached(); } @@ -241,8 +241,8 @@ public static ListenerHandle createDetached( return ListenerHandleBuilder .from(observableSet, changeListener) - .onAttach((observable, listener) -> observable.addListener(listener)) - .onDetach((observable, listener) -> observable.removeListener(listener)) + .onAttach(ObservableSet::addListener) + .onDetach(ObservableSet::removeListener) .buildDetached(); } @@ -287,8 +287,8 @@ public static ListenerHandle createDetached( return ListenerHandleBuilder .from(observableMap, changeListener) - .onAttach((observable, listener) -> observable.addListener(listener)) - .onDetach((observable, listener) -> observable.removeListener(listener)) + .onAttach(ObservableMap::addListener) + .onDetach(ObservableMap::removeListener) .buildDetached(); } diff --git a/src/main/java/org/codefx/libfx/listener/handle/package-info.java b/src/main/java/org/codefx/libfx/listener/handle/package-info.java index fac5b67..c219c4a 100644 --- a/src/main/java/org/codefx/libfx/listener/handle/package-info.java +++ b/src/main/java/org/codefx/libfx/listener/handle/package-info.java @@ -1,5 +1,5 @@ /** - * This package provides classes which make it easier to add and remove listeners from observable instances. + * Provides classes which make it easier to add and remove listeners from observable instances. *

* Using the default JavaFX 8 features, it is necessary to store both the observed instance and the listener if the * latter has to be added or removed repeatedly. A {@link org.codefx.libfx.listener.handle.ListenerHandle diff --git a/src/main/java/org/codefx/libfx/nesting/AbstractNestingBuilderOnObservable.java b/src/main/java/org/codefx/libfx/nesting/AbstractNestingBuilderOnObservable.java index da3a497..d0333af 100644 --- a/src/main/java/org/codefx/libfx/nesting/AbstractNestingBuilderOnObservable.java +++ b/src/main/java/org/codefx/libfx/nesting/AbstractNestingBuilderOnObservable.java @@ -32,7 +32,7 @@ abstract class AbstractNestingBuilderOnObservable { * indicates which kind of builder this is. */ - //#region PROPERTIES + //#begin PROPERTIES /** * The outer observable upon which all nestings depend. This is only non-null for the outer builder (indicated by @@ -54,7 +54,7 @@ abstract class AbstractNestingBuilderOnObservable { //#end PROPERTIES - //#region CONSTRUCTION + //#begin CONSTRUCTION /** * Creates a new nesting builder which acts as the outer builder, i.e. has the specified {@link #outerObservable} . @@ -94,7 +94,7 @@ protected

AbstractNestingBuilderOnObservable( //#end CONSTRUCTION - // #region BUILD + // #begin BUILD /** * Creates a new nesting from this builder's settings. This method can be called arbitrarily often and each call @@ -163,7 +163,7 @@ private void fillNestingConstructionKit(NestingConstructionKit kit) { //#end BUILD - // #region LISTENERS + // #begin LISTENERS /** * Adds the specified invalidation listener to the nesting hierarchy's inner {@link Observable}. @@ -182,7 +182,7 @@ public NestedInvalidationListenerHandle addListener(InvalidationListener listene //#end LISTENERS - // #region PRIVATE CLASSES + // #begin PRIVATE CLASSES /** * An editable class which can be used to collect all instances needed to call @@ -191,7 +191,7 @@ public NestedInvalidationListenerHandle addListener(InvalidationListener listene @SuppressWarnings("rawtypes") protected static class NestingConstructionKit { - // #region PROPERTIES + // #begin PROPERTIES /** * The outer {@link ObservableValue} @@ -206,7 +206,7 @@ protected static class NestingConstructionKit { //#end PROPERTIES - // #region CONSTRUCTOR + // #begin CONSTRUCTOR /** * Creates a new empty construction kit. @@ -217,7 +217,7 @@ public NestingConstructionKit() { //#end CONSTRUCTOR - // #region ACCESSORS + // #begin ACCESSORS /** * @return the outer {@link ObservableValue} diff --git a/src/main/java/org/codefx/libfx/nesting/AbstractNestingBuilderOnObservableValue.java b/src/main/java/org/codefx/libfx/nesting/AbstractNestingBuilderOnObservableValue.java index b6e12f7..8b1e37d 100644 --- a/src/main/java/org/codefx/libfx/nesting/AbstractNestingBuilderOnObservableValue.java +++ b/src/main/java/org/codefx/libfx/nesting/AbstractNestingBuilderOnObservableValue.java @@ -18,7 +18,7 @@ abstract class AbstractNestingBuilderOnObservableValue> extends AbstractNestingBuilderOnObservable { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new nesting builder which acts as the outer builder. @@ -48,7 +48,7 @@ protected

AbstractNestingBuilderOnObservableValue( //#end CONSTRUCTION - // #region LISTENERS + // #begin LISTENERS /** * Adds the specified change listener to the nesting hierarchy's inner {@link ObservableValue}. diff --git a/src/main/java/org/codefx/libfx/nesting/AbstractNestingBuilderOnProperty.java b/src/main/java/org/codefx/libfx/nesting/AbstractNestingBuilderOnProperty.java index 0ffcca5..de9c4ad 100644 --- a/src/main/java/org/codefx/libfx/nesting/AbstractNestingBuilderOnProperty.java +++ b/src/main/java/org/codefx/libfx/nesting/AbstractNestingBuilderOnProperty.java @@ -14,7 +14,7 @@ abstract class AbstractNestingBuilderOnProperty> extends AbstractNestingBuilderOnObservableValue { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new nesting builder which acts as the outer builder. diff --git a/src/main/java/org/codefx/libfx/nesting/BooleanPropertyNestingBuilder.java b/src/main/java/org/codefx/libfx/nesting/BooleanPropertyNestingBuilder.java index 43954ce..d41c776 100644 --- a/src/main/java/org/codefx/libfx/nesting/BooleanPropertyNestingBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/BooleanPropertyNestingBuilder.java @@ -10,7 +10,7 @@ */ public class BooleanPropertyNestingBuilder extends AbstractNestingBuilderOnProperty { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new nesting builder which acts as a nested builder. @@ -31,7 +31,7 @@

BooleanPropertyNestingBuilder( //#end CONSTRUCTION - // #region BUILD + // #begin BUILD /** * Creates a nested property from this builder's settings. This method can be called arbitrarily often and each call diff --git a/src/main/java/org/codefx/libfx/nesting/DeepNesting.java b/src/main/java/org/codefx/libfx/nesting/DeepNesting.java index c0d6178..5a71231 100644 --- a/src/main/java/org/codefx/libfx/nesting/DeepNesting.java +++ b/src/main/java/org/codefx/libfx/nesting/DeepNesting.java @@ -55,7 +55,7 @@ final class DeepNesting implements Nesting { //#formatter:on - // #region PROPERTIES + // #begin PROPERTIES /** * The level of the nesting, which is also the length of the arrays. @@ -92,7 +92,7 @@ final class DeepNesting implements Nesting { //#end PROPERTIES - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new deep nesting which depends on the specified outer observable and uses specified nesting steps. @@ -196,7 +196,7 @@ private void updateNestingFromLevel(int startLevel) { new NestingUpdater(startLevel).update(); } - // #region ACCESSORS + // #begin ACCESSORS /** * {@inheritDoc} @@ -208,7 +208,7 @@ public ReadOnlyProperty> innerObservableProperty() { //#end ACCESSORS - // #region PRIVATE CLASSES + // #begin PRIVATE CLASSES /** * Initializes {@link DeepNesting#observables}, {@link DeepNesting#values} and {@link DeepNesting#inner} as well as diff --git a/src/main/java/org/codefx/libfx/nesting/DoublePropertyNestingBuilder.java b/src/main/java/org/codefx/libfx/nesting/DoublePropertyNestingBuilder.java index 8b5905c..c4fae13 100644 --- a/src/main/java/org/codefx/libfx/nesting/DoublePropertyNestingBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/DoublePropertyNestingBuilder.java @@ -10,7 +10,7 @@ */ public class DoublePropertyNestingBuilder extends AbstractNestingBuilderOnProperty { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new nesting builder which acts as a nested builder. @@ -31,7 +31,7 @@

DoublePropertyNestingBuilder( //#end CONSTRUCTION - // #region BUILD + // #begin BUILD /** * Creates a nested property from this builder's settings. This method can be called arbitrarily often and each call diff --git a/src/main/java/org/codefx/libfx/nesting/FloatPropertyNestingBuilder.java b/src/main/java/org/codefx/libfx/nesting/FloatPropertyNestingBuilder.java index 5d8c302..261b264 100644 --- a/src/main/java/org/codefx/libfx/nesting/FloatPropertyNestingBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/FloatPropertyNestingBuilder.java @@ -10,7 +10,7 @@ */ public class FloatPropertyNestingBuilder extends AbstractNestingBuilderOnProperty { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new nesting builder which acts as a nested builder. @@ -31,7 +31,7 @@

FloatPropertyNestingBuilder( //#end CONSTRUCTION - // #region BUILD + // #begin BUILD /** * Creates a nested property from this builder's settings. This method can be called arbitrarily often and each call diff --git a/src/main/java/org/codefx/libfx/nesting/IntegerPropertyNestingBuilder.java b/src/main/java/org/codefx/libfx/nesting/IntegerPropertyNestingBuilder.java index 3f36394..ee8f64d 100644 --- a/src/main/java/org/codefx/libfx/nesting/IntegerPropertyNestingBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/IntegerPropertyNestingBuilder.java @@ -10,7 +10,7 @@ */ public class IntegerPropertyNestingBuilder extends AbstractNestingBuilderOnProperty { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new nesting builder which acts as a nested builder. @@ -31,7 +31,7 @@

IntegerPropertyNestingBuilder( //#end CONSTRUCTION - // #region BUILD + // #begin BUILD /** * Creates a nested property from this builder's settings. This method can be called arbitrarily often and each call diff --git a/src/main/java/org/codefx/libfx/nesting/LongPropertyNestingBuilder.java b/src/main/java/org/codefx/libfx/nesting/LongPropertyNestingBuilder.java index 2053c49..d724864 100644 --- a/src/main/java/org/codefx/libfx/nesting/LongPropertyNestingBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/LongPropertyNestingBuilder.java @@ -10,7 +10,7 @@ */ public class LongPropertyNestingBuilder extends AbstractNestingBuilderOnProperty { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new nesting builder which acts as a nested builder. @@ -31,7 +31,7 @@

LongPropertyNestingBuilder( //#end CONSTRUCTION - // #region BUILD + // #begin BUILD /** * Creates a nested property from this builder's settings. This method can be called arbitrarily often and each call diff --git a/src/main/java/org/codefx/libfx/nesting/Nesting.java b/src/main/java/org/codefx/libfx/nesting/Nesting.java index 7a588e1..6bbc9d1 100644 --- a/src/main/java/org/codefx/libfx/nesting/Nesting.java +++ b/src/main/java/org/codefx/libfx/nesting/Nesting.java @@ -8,10 +8,11 @@ /** *

- * A nesting encapsulates a hierarchy of nested {@link ObservableValue ObservableValues} and its - * {@link #innerObservableProperty() innerObservable} property always contains the current innermost {@code Observable} - * in that hierarchy as an {@link Optional}. A {@code Nesting} can be used as a basic building block for other nested - * functionality. + * A nesting encapsulates a hierarchy of nested {@link ObservableValue ObservableValues}. + *

+ * Its {@link #innerObservableProperty() innerObservable} property always contains the current innermost + * {@code Observable} in that hierarchy as an {@link Optional}. A {@code Nesting} can be used as a basic building block + * for other nested functionality. *

Nesting Hierarchy

A nesting hierarchy is composed of some {@code ObservableValues}, often simply called * observables, and nesting steps which lead from one observable to the next. *

@@ -24,7 +25,7 @@ * Hence they must all implement {@link ObservableValue ObservableValue}. No step is used on the inner observable so it * suffices that it implements {@link Observable}. *

Example

Consider a class {@code Employee} which has an {@code Property
address}, where - * {@code Address} has a {@code StringProperty streetName}. There might be a {@code Property currentEmployee}, + * {@code Address} has a {@code StringProperty streetName}. There might be a {@code Property currentEmployee}, * which always holds the current employee. *

* In this case the hierarchy would be {@code currentEmployee -> address -> streetName} where {@code currentEmployee} is diff --git a/src/main/java/org/codefx/libfx/nesting/NestingObserver.java b/src/main/java/org/codefx/libfx/nesting/NestingObserver.java index d06d53c..b9c9ce0 100644 --- a/src/main/java/org/codefx/libfx/nesting/NestingObserver.java +++ b/src/main/java/org/codefx/libfx/nesting/NestingObserver.java @@ -31,7 +31,7 @@ */ public final class NestingObserver { - // #region PROPERTIES + // #begin PROPERTIES /** * The observed {@link Nesting}. @@ -58,7 +58,7 @@ public final class NestingObserver { //#end PROPERTIES - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new {@link NestingObserver} from the specified {@link NestingObserverBuilder builder}. @@ -90,7 +90,7 @@ public static NestingObserverBuilder forNesting(Nestin //#end CONSTRUCTION - // #region OBSERVE + // #begin OBSERVE /** * Initializes the observer by observing the initial status and any changes made to it. @@ -127,7 +127,7 @@ private void observeInnerObservableChange( //#end BIND - // #region INNER CLASSES + // #begin INNER CLASSES /** * Builds a {@link NestingObserver}. diff --git a/src/main/java/org/codefx/libfx/nesting/ObjectPropertyNestingBuilder.java b/src/main/java/org/codefx/libfx/nesting/ObjectPropertyNestingBuilder.java index 104dce0..ba8466b 100644 --- a/src/main/java/org/codefx/libfx/nesting/ObjectPropertyNestingBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/ObjectPropertyNestingBuilder.java @@ -28,7 +28,7 @@ */ public class ObjectPropertyNestingBuilder extends AbstractNestingBuilderOnProperty> { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new nesting builder which acts as the outer builder. @@ -59,7 +59,7 @@

ObjectPropertyNestingBuilder( //#end CONSTRUCTION - // #region NEST + // #begin NEST /** * Returns a builder for nestings whose inner observable is an {@link Observable}. The created nestings depend on @@ -242,7 +242,7 @@ public StringPropertyNestingBuilder nestStringProperty(NestingStep { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new nesting builder which acts as the outer builder. diff --git a/src/main/java/org/codefx/libfx/nesting/ObservableNumberValueNestingBuilder.java b/src/main/java/org/codefx/libfx/nesting/ObservableNumberValueNestingBuilder.java index 7a55874..620b6c5 100644 --- a/src/main/java/org/codefx/libfx/nesting/ObservableNumberValueNestingBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/ObservableNumberValueNestingBuilder.java @@ -11,7 +11,7 @@ public class ObservableNumberValueNestingBuilder extends AbstractNestingBuilderOnObservableValue { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new nesting builder which acts as the outer builder. diff --git a/src/main/java/org/codefx/libfx/nesting/ObservableValueNestingBuilder.java b/src/main/java/org/codefx/libfx/nesting/ObservableValueNestingBuilder.java index ee9d5b0..145c47a 100644 --- a/src/main/java/org/codefx/libfx/nesting/ObservableValueNestingBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/ObservableValueNestingBuilder.java @@ -27,7 +27,7 @@ */ public class ObservableValueNestingBuilder extends AbstractNestingBuilderOnObservableValue> { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new nesting builder which acts as the outer builder. @@ -58,7 +58,7 @@

ObservableValueNestingBuilder( //#end CONSTRUCTION - // #region NEST + // #begin NEST /** * Returns a builder for nestings whose inner observable is an {@link Observable}. The created nestings depend on diff --git a/src/main/java/org/codefx/libfx/nesting/StringPropertyNestingBuilder.java b/src/main/java/org/codefx/libfx/nesting/StringPropertyNestingBuilder.java index 4cf2afd..4ae397c 100644 --- a/src/main/java/org/codefx/libfx/nesting/StringPropertyNestingBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/StringPropertyNestingBuilder.java @@ -10,7 +10,7 @@ */ public class StringPropertyNestingBuilder extends AbstractNestingBuilderOnProperty { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new nesting builder which acts as a nested builder. @@ -31,7 +31,7 @@

StringPropertyNestingBuilder( //#end CONSTRUCTION - // #region BUILD + // #begin BUILD /** * Creates a nested property from this builder's settings. This method can be called arbitrarily often and each call diff --git a/src/main/java/org/codefx/libfx/nesting/listener/NestedChangeListenerBuilder.java b/src/main/java/org/codefx/libfx/nesting/listener/NestedChangeListenerBuilder.java index edabc99..a87e14f 100644 --- a/src/main/java/org/codefx/libfx/nesting/listener/NestedChangeListenerBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/listener/NestedChangeListenerBuilder.java @@ -17,7 +17,7 @@ */ public class NestedChangeListenerBuilder> { - // #region PROPERTIES + // #begin PROPERTIES /** * The {@link Nesting} to which the listener will be added. @@ -31,7 +31,7 @@ public class NestedChangeListenerBuilder> { //#end PROPERTIES - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new builder for the specified nesting. @@ -72,7 +72,7 @@ public static > NestedChangeListenerBuilder listener) { //#end METHODS - // #region PRIVATE CLASSES + // #begin PRIVATE CLASSES /** * A subtype of {@link NestedChangeListenerBuilder} which can actually build a listener with diff --git a/src/main/java/org/codefx/libfx/nesting/listener/NestedChangeListenerHandle.java b/src/main/java/org/codefx/libfx/nesting/listener/NestedChangeListenerHandle.java index e801e6b..4b1f1d5 100644 --- a/src/main/java/org/codefx/libfx/nesting/listener/NestedChangeListenerHandle.java +++ b/src/main/java/org/codefx/libfx/nesting/listener/NestedChangeListenerHandle.java @@ -30,7 +30,7 @@ */ public class NestedChangeListenerHandle implements NestedListenerHandle { - // #region PROPERTIES + // #begin PROPERTIES /** * The {@link Nesting} to whose inner observable the {@link #listener} is attached. @@ -54,7 +54,7 @@ public class NestedChangeListenerHandle implements NestedListenerHandle { //#end PROPERTIES - // #region CONSTUCTION + // #begin CONSTUCTION /** * Creates a new {@link NestedChangeListenerHandle} which can {@link #attach() attach} the specified listener to the @@ -87,7 +87,7 @@ public class NestedChangeListenerHandle implements NestedListenerHandle { //#end CONSTUCTION - // #region ADD & REMOVE + // #begin ADD & REMOVE /** * Adds the {@link #listener} to the specified observable, when indicated by {@link #attached}. @@ -112,7 +112,7 @@ private void remove(ObservableValue observable) { // #end ADD & REMOVE - // #region IMPLEMENTATION OF 'NestedListenerHandle' + // #begin IMPLEMENTATION OF 'NestedListenerHandle' @Override public void attach() { diff --git a/src/main/java/org/codefx/libfx/nesting/listener/NestedInvalidationListenerBuilder.java b/src/main/java/org/codefx/libfx/nesting/listener/NestedInvalidationListenerBuilder.java index 9ece5a3..8d7bb14 100644 --- a/src/main/java/org/codefx/libfx/nesting/listener/NestedInvalidationListenerBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/listener/NestedInvalidationListenerBuilder.java @@ -11,7 +11,7 @@ */ public class NestedInvalidationListenerBuilder { - // #region PROPERTIES + // #begin PROPERTIES /** * The {@link Nesting} to which the listener will be added. @@ -25,7 +25,7 @@ public class NestedInvalidationListenerBuilder { //#end PROPERTIES - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new builder for the specified nesting. @@ -64,7 +64,7 @@ public static NestedInvalidationListenerBuilder forNesting(Nesting nesting) { //#end CONSTRUCTION - // #region METHODS + // #begin METHODS /** * Specified the listener which will be added to the nesting. @@ -82,7 +82,7 @@ public Buildable withListener(InvalidationListener listener) { //#end METHODS - // #region PRIVATE CLASSES + // #begin PRIVATE CLASSES /** * A subtype of {@link NestedInvalidationListenerBuilder} which can actually build a listener with {@link #buildAttached()}. diff --git a/src/main/java/org/codefx/libfx/nesting/listener/NestedInvalidationListenerHandle.java b/src/main/java/org/codefx/libfx/nesting/listener/NestedInvalidationListenerHandle.java index 3a803da..5f896cf 100644 --- a/src/main/java/org/codefx/libfx/nesting/listener/NestedInvalidationListenerHandle.java +++ b/src/main/java/org/codefx/libfx/nesting/listener/NestedInvalidationListenerHandle.java @@ -27,7 +27,7 @@ */ public class NestedInvalidationListenerHandle implements NestedListenerHandle { - // #region PROPERTIES + // #begin PROPERTIES /** * The {@link Nesting} to whose inner observable the {@link #listener} is attached. @@ -51,7 +51,7 @@ public class NestedInvalidationListenerHandle implements NestedListenerHandle { //#end PROPERTIES - // #region CONSTUCTION + // #begin CONSTUCTION /** * Creates a new {@link NestedInvalidationListenerHandle} which adds the specified listener to the specified @@ -82,7 +82,7 @@ public class NestedInvalidationListenerHandle implements NestedListenerHandle { //#end CONSTUCTION - // #region ADD & REMOVE + // #begin ADD & REMOVE /** * Adds the {@link #listener} to the specified observable, when indicated by {@link #attached}. @@ -107,7 +107,7 @@ private void remove(Observable observable) { // #end ADD & REMOVE - // #region IMPLEMENTATION OF 'NestedListenerHandle' + // #begin IMPLEMENTATION OF 'NestedListenerHandle' @Override public void attach() { diff --git a/src/main/java/org/codefx/libfx/nesting/package-info.java b/src/main/java/org/codefx/libfx/nesting/package-info.java index 7edbf5f..f8ff38b 100644 --- a/src/main/java/org/codefx/libfx/nesting/package-info.java +++ b/src/main/java/org/codefx/libfx/nesting/package-info.java @@ -1,7 +1,7 @@ /** *

- * This package provides functionality around nesting hierarchies - a term which is explained in all detail in the - * comment on {@link org.codefx.libfx.nesting.Nesting Nesting}. + * Provides functionality around nesting hierarchies - a term which is explained in all detail in the comment on + * {@link org.codefx.libfx.nesting.Nesting Nesting}. *

Nesting

A {@code Nesting} encapsulates a hierarchy of nested {@code ObservableValues} and collapses them * into a property which always contains the current innermost {@code Observable} in that hierarchy. A {@code Nesting} * can be used as a basic building block for other nested functionality (see below). diff --git a/src/main/java/org/codefx/libfx/nesting/property/AbstractNestedPropertyBuilder.java b/src/main/java/org/codefx/libfx/nesting/property/AbstractNestedPropertyBuilder.java index b68407a..734ca3a 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/AbstractNestedPropertyBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/property/AbstractNestedPropertyBuilder.java @@ -1,29 +1,42 @@ package org.codefx.libfx.nesting.property; import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; import javafx.beans.property.Property; import org.codefx.libfx.nesting.Nesting; +import org.codefx.libfx.nesting.property.InnerObservableMissingBehavior.WhenInnerObservableGoesMissing; +import org.codefx.libfx.nesting.property.InnerObservableMissingBehavior.WhenInnerObservableMissingOnUpdate; /** * Abstract superclass to nested property builders. Collects common builder settings; e.g. for the new property's * {@link Property#getBean() bean} and {@link Property#getName() name}. * + * @param + * the most concrete type of the value wrapped by the property which will be built * @param * the type of the nesting hierarchy's inner observable (which is a {@link Property}) * @param

* the type of {@link Property} which will be built + * @param + * the most concrete type of this builder (used for fluent API) */ -abstract class AbstractNestedPropertyBuilder, P extends NestedProperty> { +abstract class AbstractNestedPropertyBuilder, P extends NestedProperty, B extends AbstractNestedPropertyBuilder> { - // #region PROPERTIES + // #begin PROPERTIES /** * The nesting which will be used for all nested properties. */ private final Nesting nesting; + /** + * The behavior for the case that the inner observable is missing. + */ + private final MutableInnerObservableMissingBehavior innerObservableMissingBehavior; + /** * The property's future {@link Property#getBean() bean}. */ @@ -36,7 +49,7 @@ abstract class AbstractNestedPropertyBuilder, P extends Ne //#end PROPERTIES - // #region CONSTRUCTOR + // #begin CONSTRUCTOR /** * Creates a new abstract builder which uses the specified nesting. @@ -47,11 +60,12 @@ abstract class AbstractNestedPropertyBuilder, P extends Ne protected AbstractNestedPropertyBuilder(Nesting nesting) { Objects.requireNonNull(nesting, "The argument 'nesting' must not be null."); this.nesting = nesting; + this.innerObservableMissingBehavior = new MutableInnerObservableMissingBehavior<>(); } //#end CONSTRUCTOR - // #region ABSTRACT METHODS + // #begin ABSTRACT METHODS /** * Creates a new property instance. This method can be called arbitrarily often and each call returns a new @@ -63,76 +77,236 @@ protected AbstractNestedPropertyBuilder(Nesting nesting) { //#end ABSTRACT METHODS - // #region ACCESSORS + // #begin MUTATORS /** - * @return the nesting which will be used for all nested properties + * Sets the property's {@link Property#getBean() bean}. + * + * @param bean + * the property's future bean + * @return this builder */ - protected final Nesting getNesting() { - return nesting; + public final B setBean(Object bean) { + Objects.requireNonNull(bean, "The argument 'bean' must not be null."); + this.bean = bean; + return thisAsB(); } /** - * @return the property's future {@link Property#getBean() bean}. + * Sets the property's {@link Property#getName() name}. + * + * @param name + * the property's future name + * @return this builder */ - protected final Object getBean() { - return bean; + public B setName(String name) { + Objects.requireNonNull(name, "The argument 'name' must not be null."); + this.name = name; + return thisAsB(); } /** - * Sets the property's future {@link Property#getBean() bean}. + * The property will keep its value when the inner observable goes missing (see {@link NestedProperty} for details + * on this). + *

+ * This is the default behavior. * - * @param bean - * the property's future bean + * @return this builder */ - protected final void setTheBean(Object bean) { - Objects.requireNonNull(bean, "The argument 'bean' must not be null."); - this.bean = bean; + public B onInnerObservableMissingKeepValue() { + innerObservableMissingBehavior.whenGoesMissing(WhenInnerObservableGoesMissing.KEEP_VALUE); + return thisAsB(); } /** - * Sets the property's future {@link Property#getBean() bean}. + * The property will change to the default value for the wrapped type when the inner observable goes missing (see + * {@link NestedProperty} for details on this). + *

+ * For primitive wrapping properties (e.g. {@link NestedIntegerProperty}), this will set the primitive default (e.g. + * 0); for reference wrapping properties this will be null. * - * @param bean - * the property's future bean * @return this builder */ - public AbstractNestedPropertyBuilder setBean(Object bean) { - Objects.requireNonNull(bean, "The argument 'bean' must not be null."); - this.bean = bean; - return this; + public B onInnerObservableMissingSetDefaultValue() { + innerObservableMissingBehavior.whenGoesMissing(WhenInnerObservableGoesMissing.SET_DEFAULT_VALUE); + return thisAsB(); } /** - * @return the property's future {@link Property#getBean() bean}. + * The property will change to the specified value when the inner observable goes missing (see + * {@link NestedProperty} for details on this). + *

+ * This method does not accept null as a value. Call {@link #onInnerObservableMissingSetDefaultValue()} if the + * property should change to the default value for the wrapped type (e.g. 0 for {@link NestedIntegerProperty}). + * + * @param value + * the value to set + * @return this builder */ - protected final String getName() { - return name; + public B onInnerObservableMissingSetValue(T value) { + Objects.requireNonNull(value, "The argument 'value' must not be null."); + + innerObservableMissingBehavior.whenGoesMissing(WhenInnerObservableGoesMissing.SET_VALUE_FROM_SUPPLIER); + innerObservableMissingBehavior.valueForMissing(() -> value); + return thisAsB(); } /** - * Sets the property's future {@link Property#getName() name}. + * The property will change to the value computed by the specified supplier when the inner observable goes missing + * (see {@link NestedProperty} for details on this). + *

+ * The supplier may produce null in which case primitive wrapping properties will fall back to the type's default + * value (e.g. 0 for {@link NestedIntegerProperty}). * - * @param name - * the property's future name + * @param valueSupplier + * the supplier which computes the value to set; may produce null + * @return this builder */ - protected final void setTheName(String name) { - Objects.requireNonNull(name, "The argument 'name' must not be null."); - this.name = name; + public B onInnerObservableMissingComputeValue(Supplier valueSupplier) { + Objects.requireNonNull(valueSupplier, "The argument 'valueSupplier' must not be null."); + + innerObservableMissingBehavior.whenGoesMissing(WhenInnerObservableGoesMissing.SET_VALUE_FROM_SUPPLIER); + innerObservableMissingBehavior.valueForMissing(valueSupplier); + return thisAsB(); } /** - * Sets the property's future {@link Property#getName() name}. + * The property will throw an {@link IllegalStateException} when it is updated (e.g. by calling + * {@link Property#setValue(Object) setValue} or via a binding) while the inner observable is missing (see + * {@link NestedProperty} for details on this). + *

+ * This is the default behavior. + * + * @return this builder + */ + public B onUpdateWhenInnerObservableMissingThrowException() { + innerObservableMissingBehavior.onUpdate(WhenInnerObservableMissingOnUpdate.THROW_EXCEPTION); + return thisAsB(); + } + + /** + * The property will accept new values when it is updated (e.g. by calling {@link Property#setValue(Object) + * setValue} or via a binding) while the inner observable is missing (see {@link NestedProperty} for details on + * this). + *

+ * Once the nesting changes to a new (non-missing) inner observable, the property will change to that observable's + * value. * - * @param name - * the property's future name * @return this builder */ - public AbstractNestedPropertyBuilder setName(String name) { - setTheName(name); - return this; + public B onUpdateWhenInnerObservableMissingAcceptValues() { + innerObservableMissingBehavior + .onUpdate(WhenInnerObservableMissingOnUpdate.ACCEPT_VALUE_UNTIL_NEXT_INNER_OBSERVABLE); + return thisAsB(); + } + + /** + * Performs an unchecked cast to {@code B} which + * + * @return this builder as an instance of {@code B} + */ + @SuppressWarnings("unchecked") + private B thisAsB() { + B thisAsB = (B) this; + return thisAsB; + } + + // #end MUTATORS + + // #begin ACCESSORS FOR SUBCLASSES + + /** + * @return the nesting which will be used for all nested properties + */ + protected final Nesting getNesting() { + return nesting; + } + + /** + * @return the property's {@link Property#getBean() bean}. + */ + protected final Object getBean() { + return bean; + } + + /** + * @return the property's {@link Property#getBean() bean}. + */ + protected final String getName() { + return name; + } + + /** + * @return the property's behavior for the case that the inner observable is missing + */ + protected final InnerObservableMissingBehavior getInnerObservableMissingBehavior() { + return new ImmutableInnerObservableMissingBehavior<>(innerObservableMissingBehavior); + } + + //#end ACCESSORS FOR SUBCLASSES + + // #begin NESTED CLASSES + + private static class MutableInnerObservableMissingBehavior { + + private static final WhenInnerObservableGoesMissing DEFAULT_WHEN_GOES_MISSING = WhenInnerObservableGoesMissing.KEEP_VALUE; + private static final WhenInnerObservableMissingOnUpdate DEFAULT_ON_UPDATE = WhenInnerObservableMissingOnUpdate.THROW_EXCEPTION; + + private WhenInnerObservableGoesMissing whenGoesMissing; + private Optional> valueForMissing; + private WhenInnerObservableMissingOnUpdate onUpdate; + + public MutableInnerObservableMissingBehavior() { + this.whenGoesMissing = DEFAULT_WHEN_GOES_MISSING; + this.valueForMissing = Optional.empty(); + this.onUpdate = DEFAULT_ON_UPDATE; + } + + public void whenGoesMissing(WhenInnerObservableGoesMissing whenGoesMissing) { + assert whenGoesMissing != null : "The argument 'whenGoesMissing' must not be null."; + this.whenGoesMissing = whenGoesMissing; + } + + public void valueForMissing(Supplier valueForMissing) { + this.valueForMissing = Optional.of(valueForMissing); + } + + public void onUpdate(WhenInnerObservableMissingOnUpdate onUpdate) { + assert onUpdate != null : "The argument 'onUpdate' must not be null."; + this.onUpdate = onUpdate; + } + + } + + private static class ImmutableInnerObservableMissingBehavior implements InnerObservableMissingBehavior { + + private final WhenInnerObservableGoesMissing whenGoesMissing; + private final Optional> valueForMissing; + private final WhenInnerObservableMissingOnUpdate onUpdate; + + public ImmutableInnerObservableMissingBehavior(MutableInnerObservableMissingBehavior behavior) { + this.whenGoesMissing = behavior.whenGoesMissing; + this.valueForMissing = behavior.valueForMissing; + this.onUpdate = behavior.onUpdate; + } + + @Override + public WhenInnerObservableGoesMissing whenGoesMissing() { + return whenGoesMissing; + } + + @Override + public Optional> valueForMissing() { + return valueForMissing; + } + + @Override + public WhenInnerObservableMissingOnUpdate onUpdate() { + return onUpdate; + } + } - //#end ACCESSORS + // #end NESTED CLASSES } diff --git a/src/main/java/org/codefx/libfx/nesting/property/InnerObservableMissingBehavior.java b/src/main/java/org/codefx/libfx/nesting/property/InnerObservableMissingBehavior.java new file mode 100644 index 0000000..677571f --- /dev/null +++ b/src/main/java/org/codefx/libfx/nesting/property/InnerObservableMissingBehavior.java @@ -0,0 +1,70 @@ +package org.codefx.libfx.nesting.property; + +import java.util.Optional; +import java.util.function.Supplier; + +import javafx.beans.property.Property; + +/** + * Internal specification of how a nested property behaves when the inner observable is missing. + * + * @param + * the type contained in the nested property, e.g. {@link Integer} for {@link NestedIntegerProperty} + */ +interface InnerObservableMissingBehavior { + + /** + * @return the behavior when the inner observable goes missing + */ + WhenInnerObservableGoesMissing whenGoesMissing(); + + /** + * @return a supplier which will produce a value in the case that the inner observable goes missing as an + * {@link Optional} + */ + Optional> valueForMissing(); + + /** + * @return behavior when the nested property is updated while the inner observable is missing + */ + WhenInnerObservableMissingOnUpdate onUpdate(); + + /** + * Behavior when the inner observable goes missing. + */ + public enum WhenInnerObservableGoesMissing { + + /** + * The nested property will keep its value. + */ + KEEP_VALUE, + + /** + * The nested property will change to the default value for the wrapped type. + */ + SET_DEFAULT_VALUE, + + /** + * The nested property will change to the value which is specified by the supplier. + */ + SET_VALUE_FROM_SUPPLIER + + } + + /** + * Behavior when {@link Property#setValue(Object) setValue} is called while the inner observable is missing. + */ + public enum WhenInnerObservableMissingOnUpdate { + + /** + * The nested property will throw an exception. + */ + THROW_EXCEPTION, + + /** + * The nested property will accept the value but it will be overwritten when the next inner observable is set. + */ + ACCEPT_VALUE_UNTIL_NEXT_INNER_OBSERVABLE, + + } +} diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedBooleanProperty.java b/src/main/java/org/codefx/libfx/nesting/property/NestedBooleanProperty.java index 5cf6901..9250d36 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/NestedBooleanProperty.java +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedBooleanProperty.java @@ -1,7 +1,5 @@ package org.codefx.libfx.nesting.property; -import java.util.Objects; - import javafx.beans.property.BooleanProperty; import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyBooleanProperty; @@ -14,47 +12,70 @@ */ public class NestedBooleanProperty extends SimpleBooleanProperty implements NestedProperty { - // #region PROPERTIES - - /** - * The property indicating whether the nesting's inner observable is currently present, i.e. not null. - */ - private final BooleanProperty innerObservablePresent; - - //#end PROPERTIES + private final NestedPropertyInternals internals; - // #region CONSTUCTION + // #begin CONSTUCTION /** - * Creates a new property. Except {@code nesting} all arguments can be null. + * Creates a new property. * * @param nesting * the nesting this property is based on + * @param innerObservableMissingBehavior + * defines the behavior for the case that the inner observable is missing * @param bean * the bean which owns this property; can be null * @param name * this property's name; can be null */ - NestedBooleanProperty(Nesting> nesting, Object bean, String name) { + NestedBooleanProperty( + Nesting> nesting, + InnerObservableMissingBehavior innerObservableMissingBehavior, + Object bean, + String name) { + super(bean, name); - Objects.requireNonNull(nesting, "The argument 'nesting' must not be null."); - this.innerObservablePresent = new SimpleBooleanProperty(this, "innerObservablePresent"); + assert nesting != null : "The argument 'nesting' must not be null."; + assert innerObservableMissingBehavior != null : "The argument 'innerObservableMissingBehavior' must not be null."; - PropertyToNestingBinding.bind(this, isPresent -> innerObservablePresent.set(isPresent), nesting); + this.internals = new NestedPropertyInternals<>( + this, nesting, innerObservableMissingBehavior, this::setValueSuper); + internals.initializeBinding(); } //#end CONSTUCTION - // #region IMPLEMENTATION OF 'NestedProperty' + // #begin OVERRIDE SET(VALUE) + + @Override + public void set(boolean newValue) { + internals.setCheckingMissingInnerObservable(newValue); + } + + @Override + public void setValue(Boolean newValue) { + internals.setCheckingMissingInnerObservable(newValue); + } + + private void setValueSuper(Boolean newValue) { + if (newValue == null) + super.set(false); + else + super.set(newValue.booleanValue()); + } + + // #end OVERRIDE SET(VALUE) + + // #begin IMPLEMENTATION OF 'NestedProperty' @Override public ReadOnlyBooleanProperty innerObservablePresentProperty() { - return innerObservablePresent; + return internals.innerObservablePresentProperty(); } @Override public boolean isInnerObservablePresent() { - return innerObservablePresent.get(); + return internals.innerObservablePresentProperty().get(); } //#end IMPLEMENTATION OF 'NestedProperty' diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedBooleanPropertyBuilder.java b/src/main/java/org/codefx/libfx/nesting/property/NestedBooleanPropertyBuilder.java index db0dad7..6da092e 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/NestedBooleanPropertyBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedBooleanPropertyBuilder.java @@ -1,7 +1,6 @@ package org.codefx.libfx.nesting.property; import javafx.beans.property.BooleanProperty; -import javafx.beans.property.Property; import org.codefx.libfx.nesting.Nesting; @@ -9,9 +8,10 @@ * A builder for a {@link NestedBooleanProperty} which is bound to the {@link Nesting#innerObservableProperty() * innerObservable} of a {@link Nesting}. */ -public class NestedBooleanPropertyBuilder extends AbstractNestedPropertyBuilder { +public class NestedBooleanPropertyBuilder extends + AbstractNestedPropertyBuilder { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new builder which uses the specified nesting. @@ -36,37 +36,11 @@ public static NestedBooleanPropertyBuilder forNesting(Nesting n //#end CONSTRUCTION - // #region METHODS + // #begin METHODS @Override public NestedBooleanProperty build() { - return new NestedBooleanProperty(getNesting(), getBean(), getName()); - } - - /** - * Sets the property's future {@link Property#getBean() bean}. - * - * @param bean - * the property's future bean - * @return this builder - */ - @Override - public NestedBooleanPropertyBuilder setBean(Object bean) { - setTheBean(bean); - return this; - } - - /** - * Sets the property's future {@link Property#getName() name}. - * - * @param name - * the property's future name - * @return this builder - */ - @Override - public NestedBooleanPropertyBuilder setName(String name) { - setTheName(name); - return this; + return new NestedBooleanProperty(getNesting(), getInnerObservableMissingBehavior(), getBean(), getName()); } //#end METHODS diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedDoubleProperty.java b/src/main/java/org/codefx/libfx/nesting/property/NestedDoubleProperty.java index 54f6c55..adee297 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/NestedDoubleProperty.java +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedDoubleProperty.java @@ -1,12 +1,8 @@ package org.codefx.libfx.nesting.property; -import java.util.Objects; - -import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import org.codefx.libfx.nesting.Nesting; @@ -16,47 +12,70 @@ */ public class NestedDoubleProperty extends SimpleDoubleProperty implements NestedProperty { - // #region PROPERTIES - - /** - * The property indicating whether the nesting's inner observable is currently present, i.e. not null. - */ - private final BooleanProperty innerObservablePresent; - - //#end PROPERTIES + private final NestedPropertyInternals internals; - // #region CONSTUCTION + // #begin CONSTUCTION /** - * Creates a new property. Except {@code nesting} all arguments can be null. + * Creates a new property. * * @param nesting * the nesting this property is based on + * @param innerObservableMissingBehavior + * defines the behavior for the case that the inner observable is missing * @param bean * the bean which owns this property; can be null * @param name * this property's name; can be null */ - NestedDoubleProperty(Nesting> nesting, Object bean, String name) { + NestedDoubleProperty( + Nesting> nesting, + InnerObservableMissingBehavior innerObservableMissingBehavior, + Object bean, + String name) { + super(bean, name); - Objects.requireNonNull(nesting, "The argument 'nesting' must not be null."); - this.innerObservablePresent = new SimpleBooleanProperty(this, "innerObservablePresent"); + assert nesting != null : "The argument 'nesting' must not be null."; + assert innerObservableMissingBehavior != null : "The argument 'innerObservableMissingBehavior' must not be null."; - PropertyToNestingBinding.bind(this, isPresent -> innerObservablePresent.set(isPresent), nesting); + this.internals = new NestedPropertyInternals<>( + this, nesting, innerObservableMissingBehavior, this::setValueSuper); + internals.initializeBinding(); } //#end CONSTUCTION - // #region IMPLEMENTATION OF 'NestedProperty' + // #begin OVERRIDE SET(VALUE) + + @Override + public void set(double newValue) { + internals.setCheckingMissingInnerObservable(newValue); + } + + @Override + public void setValue(Number newValue) { + internals.setCheckingMissingInnerObservable(newValue); + } + + private void setValueSuper(Number newValue) { + if (newValue == null) + super.set(0); + else + super.set(newValue.doubleValue()); + } + + // #end OVERRIDE SET(VALUE) + + // #begin IMPLEMENTATION OF 'NestedProperty' @Override public ReadOnlyBooleanProperty innerObservablePresentProperty() { - return innerObservablePresent; + return internals.innerObservablePresentProperty(); } @Override public boolean isInnerObservablePresent() { - return innerObservablePresent.get(); + return internals.innerObservablePresentProperty().get(); } //#end IMPLEMENTATION OF 'NestedProperty' diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedDoublePropertyBuilder.java b/src/main/java/org/codefx/libfx/nesting/property/NestedDoublePropertyBuilder.java index fb2fa15..36d5c8d 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/NestedDoublePropertyBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedDoublePropertyBuilder.java @@ -1,7 +1,6 @@ package org.codefx.libfx.nesting.property; import javafx.beans.property.DoubleProperty; -import javafx.beans.property.Property; import org.codefx.libfx.nesting.Nesting; @@ -9,9 +8,10 @@ * A builder for a {@link NestedDoubleProperty} which is bound to the {@link Nesting#innerObservableProperty() * innerObservable} of a {@link Nesting}. */ -public class NestedDoublePropertyBuilder extends AbstractNestedPropertyBuilder { +public class NestedDoublePropertyBuilder extends + AbstractNestedPropertyBuilder { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new builder which uses the specified nesting. @@ -36,37 +36,11 @@ public static NestedDoublePropertyBuilder forNesting(Nesting nes //#end CONSTRUCTION - // #region METHODS + // #begin METHODS @Override public NestedDoubleProperty build() { - return new NestedDoubleProperty(getNesting(), getBean(), getName()); - } - - /** - * Sets the property's future {@link Property#getBean() bean}. - * - * @param bean - * the property's future bean - * @return this builder - */ - @Override - public NestedDoublePropertyBuilder setBean(Object bean) { - setTheBean(bean); - return this; - } - - /** - * Sets the property's future {@link Property#getName() name}. - * - * @param name - * the property's future name - * @return this builder - */ - @Override - public NestedDoublePropertyBuilder setName(String name) { - setTheName(name); - return this; + return new NestedDoubleProperty(getNesting(), getInnerObservableMissingBehavior(), getBean(), getName()); } //#end METHODS diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedFloatProperty.java b/src/main/java/org/codefx/libfx/nesting/property/NestedFloatProperty.java index 96435e3..ce48ddb 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/NestedFloatProperty.java +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedFloatProperty.java @@ -1,12 +1,8 @@ package org.codefx.libfx.nesting.property; -import java.util.Objects; - -import javafx.beans.property.BooleanProperty; import javafx.beans.property.FloatProperty; import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleFloatProperty; import org.codefx.libfx.nesting.Nesting; @@ -16,47 +12,70 @@ */ public class NestedFloatProperty extends SimpleFloatProperty implements NestedProperty { - // #region PROPERTIES - - /** - * The property indicating whether the nesting's inner observable is currently present, i.e. not null. - */ - private final BooleanProperty innerObservablePresent; - - //#end PROPERTIES + private final NestedPropertyInternals internals; - // #region CONSTUCTION + // #begin CONSTUCTION /** - * Creates a new property. Except {@code nesting} all arguments can be null. + * Creates a new property. * * @param nesting * the nesting this property is based on + * @param innerObservableMissingBehavior + * defines the behavior for the case that the inner observable is missing * @param bean * the bean which owns this property; can be null * @param name * this property's name; can be null */ - NestedFloatProperty(Nesting> nesting, Object bean, String name) { + NestedFloatProperty( + Nesting> nesting, + InnerObservableMissingBehavior innerObservableMissingBehavior, + Object bean, + String name) { + super(bean, name); - Objects.requireNonNull(nesting, "The argument 'nesting' must not be null."); - this.innerObservablePresent = new SimpleBooleanProperty(this, "innerObservablePresent"); + assert nesting != null : "The argument 'nesting' must not be null."; + assert innerObservableMissingBehavior != null : "The argument 'innerObservableMissingBehavior' must not be null."; - PropertyToNestingBinding.bind(this, isPresent -> innerObservablePresent.set(isPresent), nesting); + this.internals = new NestedPropertyInternals<>( + this, nesting, innerObservableMissingBehavior, this::setValueSuper); + internals.initializeBinding(); } //#end CONSTUCTION - // #region IMPLEMENTATION OF 'NestedProperty' + // #begin OVERRIDE SET(VALUE) + + @Override + public void set(float newValue) { + internals.setCheckingMissingInnerObservable(newValue); + } + + @Override + public void setValue(Number newValue) { + internals.setCheckingMissingInnerObservable(newValue); + } + + private void setValueSuper(Number newValue) { + if (newValue == null) + super.set(0); + else + super.set(newValue.floatValue()); + } + + // #end OVERRIDE SET(VALUE) + + // #begin IMPLEMENTATION OF 'NestedProperty' @Override public ReadOnlyBooleanProperty innerObservablePresentProperty() { - return innerObservablePresent; + return internals.innerObservablePresentProperty(); } @Override public boolean isInnerObservablePresent() { - return innerObservablePresent.get(); + return internals.innerObservablePresentProperty().get(); } //#end IMPLEMENTATION OF 'NestedProperty' diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedFloatPropertyBuilder.java b/src/main/java/org/codefx/libfx/nesting/property/NestedFloatPropertyBuilder.java index 1951343..2ac0d5e 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/NestedFloatPropertyBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedFloatPropertyBuilder.java @@ -1,7 +1,6 @@ package org.codefx.libfx.nesting.property; import javafx.beans.property.FloatProperty; -import javafx.beans.property.Property; import org.codefx.libfx.nesting.Nesting; @@ -9,9 +8,10 @@ * A builder for a {@link NestedFloatProperty} which is bound to the {@link Nesting#innerObservableProperty() * innerObservable} of a {@link Nesting}. */ -public class NestedFloatPropertyBuilder extends AbstractNestedPropertyBuilder { +public class NestedFloatPropertyBuilder extends + AbstractNestedPropertyBuilder { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new builder which uses the specified nesting. @@ -36,37 +36,11 @@ public static NestedFloatPropertyBuilder forNesting(Nesting nesti //#end CONSTRUCTION - // #region METHODS + // #begin METHODS @Override public NestedFloatProperty build() { - return new NestedFloatProperty(getNesting(), getBean(), getName()); - } - - /** - * Sets the property's future {@link Property#getBean() bean}. - * - * @param bean - * the property's future bean - * @return this builder - */ - @Override - public NestedFloatPropertyBuilder setBean(Object bean) { - setTheBean(bean); - return this; - } - - /** - * Sets the property's future {@link Property#getName() name}. - * - * @param name - * the property's future name - * @return this builder - */ - @Override - public NestedFloatPropertyBuilder setName(String name) { - setTheName(name); - return this; + return new NestedFloatProperty(getNesting(), getInnerObservableMissingBehavior(), getBean(), getName()); } //#end METHODS diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedIntegerProperty.java b/src/main/java/org/codefx/libfx/nesting/property/NestedIntegerProperty.java index 75dbe48..4186d43 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/NestedIntegerProperty.java +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedIntegerProperty.java @@ -1,12 +1,8 @@ package org.codefx.libfx.nesting.property; -import java.util.Objects; - -import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import org.codefx.libfx.nesting.Nesting; @@ -16,47 +12,70 @@ */ public class NestedIntegerProperty extends SimpleIntegerProperty implements NestedProperty { - // #region PROPERTIES - - /** - * The property indicating whether the nesting's inner observable is currently present, i.e. not null. - */ - private final BooleanProperty innerObservablePresent; - - //#end PROPERTIES + private final NestedPropertyInternals internals; - // #region CONSTUCTION + // #begin CONSTUCTION /** - * Creates a new property. Except {@code nesting} all arguments can be null. + * Creates a new property. * * @param nesting * the nesting this property is based on + * @param innerObservableMissingBehavior + * defines the behavior for the case that the inner observable is missing * @param bean * the bean which owns this property; can be null * @param name * this property's name; can be null */ - NestedIntegerProperty(Nesting> nesting, Object bean, String name) { + NestedIntegerProperty( + Nesting> nesting, + InnerObservableMissingBehavior innerObservableMissingBehavior, + Object bean, + String name) { + super(bean, name); - Objects.requireNonNull(nesting, "The argument 'nesting' must not be null."); - this.innerObservablePresent = new SimpleBooleanProperty(this, "innerObservablePresent"); + assert nesting != null : "The argument 'nesting' must not be null."; + assert innerObservableMissingBehavior != null : "The argument 'innerObservableMissingBehavior' must not be null."; - PropertyToNestingBinding.bind(this, isPresent -> innerObservablePresent.set(isPresent), nesting); + this.internals = new NestedPropertyInternals<>( + this, nesting, innerObservableMissingBehavior, this::setValueSuper); + internals.initializeBinding(); } //#end CONSTUCTION - // #region IMPLEMENTATION OF 'NestedProperty' + // #begin OVERRIDE SET(VALUE) + + @Override + public void set(int newValue) { + internals.setCheckingMissingInnerObservable(newValue); + } + + @Override + public void setValue(Number newValue) { + internals.setCheckingMissingInnerObservable(newValue); + } + + private void setValueSuper(Number newValue) { + if (newValue == null) + super.set(0); + else + super.set(newValue.intValue()); + } + + // #end OVERRIDE SET(VALUE) + + // #begin IMPLEMENTATION OF 'NestedProperty' @Override public ReadOnlyBooleanProperty innerObservablePresentProperty() { - return innerObservablePresent; + return internals.innerObservablePresentProperty(); } @Override public boolean isInnerObservablePresent() { - return innerObservablePresent.get(); + return internals.innerObservablePresentProperty().get(); } //#end IMPLEMENTATION OF 'NestedProperty' diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedIntegerPropertyBuilder.java b/src/main/java/org/codefx/libfx/nesting/property/NestedIntegerPropertyBuilder.java index c779819..7f1c533 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/NestedIntegerPropertyBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedIntegerPropertyBuilder.java @@ -1,7 +1,6 @@ package org.codefx.libfx.nesting.property; import javafx.beans.property.IntegerProperty; -import javafx.beans.property.Property; import org.codefx.libfx.nesting.Nesting; @@ -9,9 +8,10 @@ * A builder for a {@link NestedIntegerProperty} which is bound to the {@link Nesting#innerObservableProperty() * innerObservable} of a {@link Nesting}. */ -public class NestedIntegerPropertyBuilder extends AbstractNestedPropertyBuilder { +public class NestedIntegerPropertyBuilder extends + AbstractNestedPropertyBuilder { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new builder which uses the specified nesting. @@ -36,37 +36,11 @@ public static NestedIntegerPropertyBuilder forNesting(Nesting n //#end CONSTRUCTION - // #region METHODS + // #begin METHODS @Override public NestedIntegerProperty build() { - return new NestedIntegerProperty(getNesting(), getBean(), getName()); - } - - /** - * Sets the property's future {@link Property#getBean() bean}. - * - * @param bean - * the property's future bean - * @return this builder - */ - @Override - public NestedIntegerPropertyBuilder setBean(Object bean) { - setTheBean(bean); - return this; - } - - /** - * Sets the property's future {@link Property#getName() name}. - * - * @param name - * the property's future name - * @return this builder - */ - @Override - public NestedIntegerPropertyBuilder setName(String name) { - setTheName(name); - return this; + return new NestedIntegerProperty(getNesting(), getInnerObservableMissingBehavior(), getBean(), getName()); } //#end METHODS diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedLongProperty.java b/src/main/java/org/codefx/libfx/nesting/property/NestedLongProperty.java index da1a48d..4ff7741 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/NestedLongProperty.java +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedLongProperty.java @@ -1,12 +1,8 @@ package org.codefx.libfx.nesting.property; -import java.util.Objects; - -import javafx.beans.property.BooleanProperty; import javafx.beans.property.LongProperty; import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleLongProperty; import org.codefx.libfx.nesting.Nesting; @@ -16,47 +12,70 @@ */ public class NestedLongProperty extends SimpleLongProperty implements NestedProperty { - // #region PROPERTIES - - /** - * The property indicating whether the nesting's inner observable is currently present, i.e. not null. - */ - private final BooleanProperty innerObservablePresent; - - //#end PROPERTIES + private final NestedPropertyInternals internals; - // #region CONSTUCTION + // #begin CONSTUCTION /** - * Creates a new property. Except {@code nesting} all arguments can be null. + * Creates a new property. * * @param nesting * the nesting this property is based on + * @param innerObservableMissingBehavior + * defines the behavior for the case that the inner observable is missing * @param bean * the bean which owns this property; can be null * @param name * this property's name; can be null */ - NestedLongProperty(Nesting> nesting, Object bean, String name) { + NestedLongProperty( + Nesting> nesting, + InnerObservableMissingBehavior innerObservableMissingBehavior, + Object bean, + String name) { + super(bean, name); - Objects.requireNonNull(nesting, "The argument 'nesting' must not be null."); - this.innerObservablePresent = new SimpleBooleanProperty(this, "innerObservablePresent"); + assert nesting != null : "The argument 'nesting' must not be null."; + assert innerObservableMissingBehavior != null : "The argument 'innerObservableMissingBehavior' must not be null."; - PropertyToNestingBinding.bind(this, isPresent -> innerObservablePresent.set(isPresent), nesting); + this.internals = new NestedPropertyInternals<>( + this, nesting, innerObservableMissingBehavior, this::setValueSuper); + internals.initializeBinding(); } //#end CONSTUCTION - // #region IMPLEMENTATION OF 'NestedProperty' + // #begin OVERRIDE SET(VALUE) + + @Override + public void set(long newValue) { + internals.setCheckingMissingInnerObservable(newValue); + } + + @Override + public void setValue(Number newValue) { + internals.setCheckingMissingInnerObservable(newValue); + } + + private void setValueSuper(Number newValue) { + if (newValue == null) + super.set(0); + else + super.set(newValue.longValue()); + } + + // #end OVERRIDE SET(VALUE) + + // #begin IMPLEMENTATION OF 'NestedProperty' @Override public ReadOnlyBooleanProperty innerObservablePresentProperty() { - return innerObservablePresent; + return internals.innerObservablePresentProperty(); } @Override public boolean isInnerObservablePresent() { - return innerObservablePresent.get(); + return internals.innerObservablePresentProperty().get(); } //#end IMPLEMENTATION OF 'NestedProperty' diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedLongPropertyBuilder.java b/src/main/java/org/codefx/libfx/nesting/property/NestedLongPropertyBuilder.java index 88ac180..edf0a30 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/NestedLongPropertyBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedLongPropertyBuilder.java @@ -1,7 +1,6 @@ package org.codefx.libfx.nesting.property; import javafx.beans.property.LongProperty; -import javafx.beans.property.Property; import org.codefx.libfx.nesting.Nesting; @@ -9,9 +8,10 @@ * A builder for a {@link NestedLongProperty} which is bound to the {@link Nesting#innerObservableProperty() * innerObservable} of a {@link Nesting}. */ -public class NestedLongPropertyBuilder extends AbstractNestedPropertyBuilder { +public class NestedLongPropertyBuilder extends + AbstractNestedPropertyBuilder { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new builder which uses the specified nesting. @@ -36,37 +36,11 @@ public static NestedLongPropertyBuilder forNesting(Nesting nesting //#end CONSTRUCTION - // #region METHODS + // #begin METHODS @Override public NestedLongProperty build() { - return new NestedLongProperty(getNesting(), getBean(), getName()); - } - - /** - * Sets the property's future {@link Property#getBean() bean}. - * - * @param bean - * the property's future bean - * @return this builder - */ - @Override - public NestedLongPropertyBuilder setBean(Object bean) { - setTheBean(bean); - return this; - } - - /** - * Sets the property's future {@link Property#getName() name}. - * - * @param name - * the property's future name - * @return this builder - */ - @Override - public NestedLongPropertyBuilder setName(String name) { - setTheName(name); - return this; + return new NestedLongProperty(getNesting(), getInnerObservableMissingBehavior(), getBean(), getName()); } //#end METHODS diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedObjectProperty.java b/src/main/java/org/codefx/libfx/nesting/property/NestedObjectProperty.java index 5215fec..e5524bd 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/NestedObjectProperty.java +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedObjectProperty.java @@ -1,12 +1,8 @@ package org.codefx.libfx.nesting.property; -import java.util.Objects; - -import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import org.codefx.libfx.nesting.Nesting; @@ -19,47 +15,67 @@ */ public class NestedObjectProperty extends SimpleObjectProperty implements NestedProperty { - // #region PROPERTIES - - /** - * The property indicating whether the nesting's inner observable is currently present, i.e. not null. - */ - private final BooleanProperty innerObservablePresent; - - //#end PROPERTIES + private final NestedPropertyInternals internals; - // #region CONSTUCTION + // #begin CONSTUCTION /** - * Creates a new property. Except {@code nesting} all arguments can be null. + * Creates a new property. * * @param nesting * the nesting this property is based on + * @param innerObservableMissingBehavior + * defines the behavior for the case that the inner observable is missing * @param bean * the bean which owns this property; can be null * @param name * this property's name; can be null */ - NestedObjectProperty(Nesting> nesting, Object bean, String name) { + NestedObjectProperty( + Nesting> nesting, + InnerObservableMissingBehavior innerObservableMissingBehavior, + Object bean, + String name) { + super(bean, name); - Objects.requireNonNull(nesting, "The argument 'nesting' must not be null."); - this.innerObservablePresent = new SimpleBooleanProperty(this, "innerObservablePresent"); + assert nesting != null : "The argument 'nesting' must not be null."; + assert innerObservableMissingBehavior != null : "The argument 'innerObservableMissingBehavior' must not be null."; - PropertyToNestingBinding.bind(this, isPresent -> innerObservablePresent.set(isPresent), nesting); + this.internals = new NestedPropertyInternals<>( + this, nesting, innerObservableMissingBehavior, this::setValueSuper); + internals.initializeBinding(); } //#end CONSTUCTION - // #region IMPLEMENTATION OF 'NestedProperty' + // #begin OVERRIDE SET(VALUE) + + @Override + public void set(T newValue) { + internals.setCheckingMissingInnerObservable(newValue); + } + + @Override + public void setValue(T newValue) { + internals.setCheckingMissingInnerObservable(newValue); + } + + private void setValueSuper(T newValue) { + super.set(newValue); + } + + // #end OVERRIDE SET(VALUE) + + // #begin IMPLEMENTATION OF 'NestedProperty' @Override public ReadOnlyBooleanProperty innerObservablePresentProperty() { - return innerObservablePresent; + return internals.innerObservablePresentProperty(); } @Override public boolean isInnerObservablePresent() { - return innerObservablePresent.get(); + return internals.innerObservablePresentProperty().get(); } //#end IMPLEMENTATION OF 'NestedProperty' diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedObjectPropertyBuilder.java b/src/main/java/org/codefx/libfx/nesting/property/NestedObjectPropertyBuilder.java index 9f8044e..0533c8c 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/NestedObjectPropertyBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedObjectPropertyBuilder.java @@ -12,9 +12,9 @@ * the type of the value wrapped by the property which will be build */ public final class NestedObjectPropertyBuilder - extends AbstractNestedPropertyBuilder, NestedProperty> { + extends AbstractNestedPropertyBuilder, NestedProperty, NestedObjectPropertyBuilder> { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new builder which uses the specified nesting. @@ -41,37 +41,11 @@ public static NestedObjectPropertyBuilder forNesting(Nesting> //#end CONSTRUCTION - // #region METHODS + // #begin METHODS @Override public NestedObjectProperty build() { - return new NestedObjectProperty<>(getNesting(), getBean(), getName()); - } - - /** - * Sets the property's future {@link Property#getBean() bean}. - * - * @param bean - * the property's future bean - * @return this builder - */ - @Override - public NestedObjectPropertyBuilder setBean(Object bean) { - setTheBean(bean); - return this; - } - - /** - * Sets the property's future {@link Property#getName() name}. - * - * @param name - * the property's future name - * @return this builder - */ - @Override - public NestedObjectPropertyBuilder setName(String name) { - setTheName(name); - return this; + return new NestedObjectProperty<>(getNesting(), getInnerObservableMissingBehavior(), getBean(), getName()); } //#end METHODS diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedProperty.java b/src/main/java/org/codefx/libfx/nesting/property/NestedProperty.java index cc3df35..9eebfbd 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/NestedProperty.java +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedProperty.java @@ -6,24 +6,32 @@ /** *

- * A {@link Property} which is based on a {@link Nesting}. Simply put, this property is always bound to the nesting's - * inner observable (more precisely, it is bound to the {@link Property} instance contained in the optional value held - * by the nesting's {@link Nesting#innerObservableProperty() innerObservable} property). - *

Inner Observable's Value Changes

This property is bound to the nesting's inner observable. So when that - * observable's value changes, so does this property. + * A {@link Property} which is based on a {@link Nesting}. + *

+ * Simply put, this property is always bound to the nesting's inner observable (more precisely, it is bound to the + * {@link Property} instance contained in the optional value held by the nesting's + * {@link Nesting#innerObservableProperty() innerObservable} property). + *

Value Changes

This property is bidirectionally bound to the nesting's inner observable. So when that + * observable's value changes, so does this property's value and vice versa. *

Inner Observable Is Replaced

When the nesting's inner observable is replaced by a present observable, this * nested property's value changes to the new observable's value. Like all other value changes this one also results in * calling invalidation and change listeners. - *

- * If the new observable is missing, this property stays unbound and keeps its value (and hence no listener is called). - *

Inner Observable is Missing

It is possible that a nesting's inner observable is missing (see comment on + *

Missing Inner Observable

It is possible that a nesting's inner observable is missing (see comment on * {@link Nesting}). In that case the {@link NestedProperty#innerObservablePresentProperty() innerObservablePresent} - * property is false and changes made to this property's value can not be propagated to another property. - *

- * If the inner observable changes back to a present value, everything said above applies. This implies that when the - * nested property's value changed while the inner observable was missing, these changes are replaced by the new - * observable's value when one is set. Since this property's change listeners are called, the replaced value can be - * caught there before it gets lost. + * property is false. How else the nested property behaves depends on its configuration which was determined when it was + * build. + *

When Inner Observable Goes Missing

When the inner observable goes missing, the nested property will either + * keep its value (this is the default behavior) or change to a value which was determined at build time. This can be + * done with the {@code onInnerObservableMissing...} methods on the nested property builder (e.g. + * {@link NestedObjectPropertyBuilder#onInnerObservableMissingSetDefaultValue() onInnerObservableMissingSetDefaultValue} + * ). + *

Update While Inner Observable Is Missing

When a value is set on the nested property while the inner + * observable is missing, it can not be propagated anywhere. For this reason the default behavior is to throw an + * exception. Alternatively the property can hold new values until a new inner observable is found (with + * {@link NestedObjectPropertyBuilder#onUpdateWhenInnerObservableMissingAcceptValues() + * onUpdateWhenInnerObservableMissingAcceptValues}). The property will then be bound to the new observable and hence + * forget the intermediate value. (Since this property's change listeners are called, the replaced value can be caught + * there before it gets lost.) * * @param * the type of the value wrapped by the property diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedPropertyInternals.java b/src/main/java/org/codefx/libfx/nesting/property/NestedPropertyInternals.java new file mode 100644 index 0000000..d9224b4 --- /dev/null +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedPropertyInternals.java @@ -0,0 +1,162 @@ +package org.codefx.libfx.nesting.property; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.Property; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + +import org.codefx.libfx.nesting.Nesting; +import org.codefx.libfx.nesting.property.InnerObservableMissingBehavior.WhenInnerObservableGoesMissing; +import org.codefx.libfx.nesting.property.InnerObservableMissingBehavior.WhenInnerObservableMissingOnUpdate; + +/** + * Contains the internal code of nested properties. + *

+ * This class exists to prevent having the same code in all property implementations for the different types. Instead + * they all delegate their calls to this class. That made it necessary to jump through some hoops and the design is not + * very elegant. + * + * @param + * the type of the value wrapped by the property + */ +final class NestedPropertyInternals { + + private final Nesting> nesting; + private final NestedProperty outerProperty; + private final Consumer setValueDirectly; + private final InnerObservableMissingBehavior missingBehavior; + + private final BooleanProperty innerObservablePresent; + + /** + * Creates a new internals. + * + * @param outerProperty + * the property whose internals are managed here + * @param nesting + * the nesting on which the outer nested property is based + * @param missingBehavior + * the behavior for the case that the inner observable is missing + * @param setValueDirectly + * called to immediately set the value on the outer property; must set the default value if called with + * null on a property which wraps a primitive type + */ + public NestedPropertyInternals( + NestedProperty outerProperty, + Nesting> nesting, + InnerObservableMissingBehavior missingBehavior, + Consumer setValueDirectly) { + + assert nesting != null : "The argument 'nesting' must not be null."; + assert outerProperty != null : "The argument 'outerProperty' must not be null."; + assert setValueDirectly != null : "The argument 'setValueDirectly' must not be null."; + assert missingBehavior != null : "The argument 'missingBehavior' must not be null."; + assert missingBehavior.whenGoesMissing() != WhenInnerObservableGoesMissing.SET_VALUE_FROM_SUPPLIER + || missingBehavior.valueForMissing().isPresent() // + : "When 'missingBehavior' requests 'SET_VALUE_FROM_SUPPLIER', a supplier must be present."; + + this.nesting = nesting; + this.outerProperty = outerProperty; + this.setValueDirectly = setValueDirectly; + this.innerObservablePresent = new SimpleBooleanProperty(outerProperty, "innerObservablePresent"); + this.missingBehavior = missingBehavior; + } + + /** + * Initializes the binding of the nested property to the nesting's inner observable. + */ + public void initializeBinding() { + bindToInnerObservable(nesting.innerObservableProperty().getValue()); + nesting.innerObservableProperty().addListener( + (obs, oldInnerObservable, newInnerObservable) + -> moveBindingToNewInnerObservable(oldInnerObservable, newInnerObservable)); + } + + // #begin BIND TO INNER OBSERVABLE + + private void moveBindingToNewInnerObservable( + Optional> oldInnerObservable, Optional> newInnerObservable) { + unbindFromInnerObservable(oldInnerObservable); + bindToInnerObservable(newInnerObservable); + } + + private void unbindFromInnerObservable(Optional> innerObservable) { + innerObservable.ifPresent(outerProperty::unbindBidirectional); + } + + private void bindToInnerObservable(Optional> innerObservable) { + innerObservablePresent.set(innerObservable.isPresent()); + if (!innerObservable.isPresent()) + handleMissingInnerObservable(); + innerObservable.ifPresent(outerProperty::bindBidirectional); + } + + private void handleMissingInnerObservable() { + WhenInnerObservableGoesMissing whenGoesMissing = missingBehavior.whenGoesMissing(); + switch (whenGoesMissing) { + case KEEP_VALUE: + return; + case SET_DEFAULT_VALUE: + setDefaultValueIgnoringMissingInnerObservable(); + return; + case SET_VALUE_FROM_SUPPLIER: + Supplier supplierForMissingValue = missingBehavior.valueForMissing().get(); + setIgnoringMissingInnerObservable(supplierForMissingValue.get()); + return; + default: + throw new IllegalArgumentException("Unknown procedere for missing inner observable: " + whenGoesMissing); + } + } + + // #end BIND TO INNER OBSERVABLE + + // #begin SET VALUE + + private void set(T newValue, boolean checkMissingInnerObservable) { + if (checkMissingInnerObservable) + maybeThrowExceptionForMissingObservable(); + setValueDirectly.accept(newValue); + } + + private void maybeThrowExceptionForMissingObservable() { + boolean innerObservableMissing = !innerObservablePresent.get(); + boolean throwExceptionConfigured = + missingBehavior.onUpdate() == WhenInnerObservableMissingOnUpdate.THROW_EXCEPTION; + if (innerObservableMissing && throwExceptionConfigured) + throw new IllegalStateException("The inner observable is missing so no value can be set."); + } + + /** + * Sets the specified value on the nested property specified during construction. + *

+ * If the inner observable is missing, the behavior specified during construction is executed. + * + * @param newValue + * the new value to set + */ + public void setCheckingMissingInnerObservable(T newValue) { + set(newValue, true); + } + + private void setIgnoringMissingInnerObservable(T newValue) { + set(newValue, false); + } + + private void setDefaultValueIgnoringMissingInnerObservable() { + set(null, false); + } + + // #end SET VALUE + + /** + * @return whether the nesting's inner observable is present as a property + */ + public final ReadOnlyBooleanProperty innerObservablePresentProperty() { + return innerObservablePresent; + } + +} diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedStringProperty.java b/src/main/java/org/codefx/libfx/nesting/property/NestedStringProperty.java index 84cb10d..a8e5bcf 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/NestedStringProperty.java +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedStringProperty.java @@ -1,11 +1,7 @@ package org.codefx.libfx.nesting.property; -import java.util.Objects; - -import javafx.beans.property.BooleanProperty; import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; @@ -16,47 +12,67 @@ */ public class NestedStringProperty extends SimpleStringProperty implements NestedProperty { - // #region PROPERTIES - - /** - * The property indicating whether the nesting's inner observable is currently present, i.e. not null. - */ - private final BooleanProperty innerObservablePresent; - - //#end PROPERTIES + private final NestedPropertyInternals internals; - // #region CONSTUCTION + // #begin CONSTUCTION /** - * Creates a new property. Except {@code nesting} all arguments can be null. + * Creates a new property. * * @param nesting * the nesting this property is based on + * @param innerObservableMissingBehavior + * defines the behavior for the case that the inner observable is missing * @param bean * the bean which owns this property; can be null * @param name * this property's name; can be null */ - NestedStringProperty(Nesting> nesting, Object bean, String name) { + NestedStringProperty( + Nesting> nesting, + InnerObservableMissingBehavior innerObservableMissingBehavior, + Object bean, + String name) { + super(bean, name); - Objects.requireNonNull(nesting, "The argument 'nesting' must not be null."); - this.innerObservablePresent = new SimpleBooleanProperty(this, "innerObservablePresent"); + assert nesting != null : "The argument 'nesting' must not be null."; + assert innerObservableMissingBehavior != null : "The argument 'innerObservableMissingBehavior' must not be null."; - PropertyToNestingBinding.bind(this, isPresent -> innerObservablePresent.set(isPresent), nesting); + this.internals = new NestedPropertyInternals<>( + this, nesting, innerObservableMissingBehavior, this::setValueSuper); + internals.initializeBinding(); } //#end CONSTUCTION - // #region IMPLEMENTATION OF 'NestedProperty' + // #begin OVERRIDE SET(VALUE) + + @Override + public void set(String newValue) { + internals.setCheckingMissingInnerObservable(newValue); + } + + @Override + public void setValue(String newValue) { + internals.setCheckingMissingInnerObservable(newValue); + } + + private void setValueSuper(String newValue) { + super.set(newValue); + } + + // #end OVERRIDE SET(VALUE) + + // #begin IMPLEMENTATION OF 'NestedProperty' @Override public ReadOnlyBooleanProperty innerObservablePresentProperty() { - return innerObservablePresent; + return internals.innerObservablePresentProperty(); } @Override public boolean isInnerObservablePresent() { - return innerObservablePresent.get(); + return internals.innerObservablePresentProperty().get(); } //#end IMPLEMENTATION OF 'NestedProperty' diff --git a/src/main/java/org/codefx/libfx/nesting/property/NestedStringPropertyBuilder.java b/src/main/java/org/codefx/libfx/nesting/property/NestedStringPropertyBuilder.java index cddc00e..707d520 100644 --- a/src/main/java/org/codefx/libfx/nesting/property/NestedStringPropertyBuilder.java +++ b/src/main/java/org/codefx/libfx/nesting/property/NestedStringPropertyBuilder.java @@ -1,6 +1,5 @@ package org.codefx.libfx.nesting.property; -import javafx.beans.property.Property; import javafx.beans.property.StringProperty; import org.codefx.libfx.nesting.Nesting; @@ -9,9 +8,10 @@ * A builder for a {@link NestedStringProperty} which is bound to the {@link Nesting#innerObservableProperty() * innerObservable} of a {@link Nesting}. */ -public class NestedStringPropertyBuilder extends AbstractNestedPropertyBuilder { +public class NestedStringPropertyBuilder extends + AbstractNestedPropertyBuilder { - // #region CONSTRUCTION + // #begin CONSTRUCTION /** * Creates a new builder which uses the specified nesting. @@ -36,37 +36,11 @@ public static NestedStringPropertyBuilder forNesting(Nesting nes //#end CONSTRUCTION - // #region METHODS + // #begin METHODS @Override public NestedStringProperty build() { - return new NestedStringProperty(getNesting(), getBean(), getName()); - } - - /** - * Sets the property's future {@link Property#getBean() bean}. - * - * @param bean - * the property's future bean - * @return this builder - */ - @Override - public NestedStringPropertyBuilder setBean(Object bean) { - setTheBean(bean); - return this; - } - - /** - * Sets the property's future {@link Property#getName() name}. - * - * @param name - * the property's future name - * @return this builder - */ - @Override - public NestedStringPropertyBuilder setName(String name) { - setTheName(name); - return this; + return new NestedStringProperty(getNesting(), getInnerObservableMissingBehavior(), getBean(), getName()); } //#end METHODS diff --git a/src/main/java/org/codefx/libfx/nesting/property/PropertyToNestingBinding.java b/src/main/java/org/codefx/libfx/nesting/property/PropertyToNestingBinding.java deleted file mode 100644 index c99a54f..0000000 --- a/src/main/java/org/codefx/libfx/nesting/property/PropertyToNestingBinding.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.codefx.libfx.nesting.property; - -import java.util.Objects; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -import javafx.beans.property.Property; - -import org.codefx.libfx.nesting.Nesting; -import org.codefx.libfx.nesting.NestingObserver; - -/** - * Implements the bidirectional binding between a nested property and its nesting's - * {@link Nesting#innerObservableProperty() innerObservable} and updates the binding when the nesting changes. - * - * @param - * the type wrapped by the bound {@link Property Properties} - */ -class PropertyToNestingBinding { - - /** - * Bidirectionally binds the specified nested property to the specified nesting's property. The specified setter is - * used to update the nested property's {@link NestedProperty#innerObservablePresentProperty() - * innerObservablePresent} property. - * - * @param - * the type wrapped by the property - * @param nestedProperty - * the {@link Property} which will be bound to the specified nesting - * @param innerObservablePresentSetter - * the {@link Consumer} which sets the {@link NestedProperty#innerObservablePresentProperty()} property - * @param nesting - * the {@link Nesting} to which the property will be bound - * @throws NullPointerException - * if any of the arguments is null - */ - public static void bind( - NestedProperty nestedProperty, Consumer innerObservablePresentSetter, - Nesting> nesting) { - - Objects.requireNonNull(nestedProperty, "The argument 'property' must not be null."); - Objects.requireNonNull(innerObservablePresentSetter, - "The argument 'innerObservablePresentSetter' must not be null."); - Objects.requireNonNull(nesting, "The argument 'nesting' must not be null."); - - // the 'innerObservablePresentSetter' only accepts one Boolean; create a 'BiConsumer' from it, - // which accepts two and ignores the first - BiConsumer innerObservablePresentBiSetter = - (any, newPropertyPresent) -> innerObservablePresentSetter.accept(newPropertyPresent); - - // use a nesting observer to accomplish the binding/unbinding - NestingObserver - .forNesting(nesting) - .withOldInnerObservable(oldProperty -> nestedProperty.unbindBidirectional(oldProperty)) - .withNewInnerObservable(newProperty -> nestedProperty.bindBidirectional(newProperty)) - .whenInnerObservableChanges(innerObservablePresentBiSetter) - .observe(); - } - -} diff --git a/src/main/java/org/codefx/libfx/serialization/SerializableOptional.java b/src/main/java/org/codefx/libfx/serialization/SerializableOptional.java index c93aba5..dc5475b 100644 --- a/src/main/java/org/codefx/libfx/serialization/SerializableOptional.java +++ b/src/main/java/org/codefx/libfx/serialization/SerializableOptional.java @@ -1,6 +1,5 @@ package org.codefx.libfx.serialization; -import java.io.IOException; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.Serializable; @@ -88,7 +87,6 @@ public final class SerializableOptional implements Seria // FIELDS - @SuppressWarnings("javadoc") private static final long serialVersionUID = -652697447004597911L; /** @@ -188,9 +186,14 @@ private Object writeReplace() { /** * Since this class should never be deserialized directly, this method should not be called. If it is, someone * purposely created a serialization of this class to bypass that mechanism, so throw an exception. + * + * @param in + * the {@link ObjectInputStream} from which the instance should be read + * @throws InvalidObjectException + * always throws this exception */ - @SuppressWarnings({ "static-method", "javadoc", "unused" }) - private void readObject(ObjectInputStream in) throws IOException { + @SuppressWarnings({ "static-method", "unused" }) + private void readObject(ObjectInputStream in) throws InvalidObjectException { throw new InvalidObjectException("Serialization proxy expected."); } @@ -202,7 +205,6 @@ private void readObject(ObjectInputStream in) throws IOException { */ private static class SerializationProxy implements Serializable { - @SuppressWarnings("javadoc") private static final long serialVersionUID = -1326520485869949065L; /** diff --git a/src/main/java/org/codefx/libfx/serialization/package-info.java b/src/main/java/org/codefx/libfx/serialization/package-info.java new file mode 100644 index 0000000..eea4583 --- /dev/null +++ b/src/main/java/org/codefx/libfx/serialization/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides serialization related functionality. + */ +package org.codefx.libfx.serialization; \ No newline at end of file diff --git a/src/test/java/org/codefx/libfx/collection/transform/ElementTypes.java b/src/test/java/org/codefx/libfx/collection/transform/ElementTypes.java new file mode 100644 index 0000000..750658f --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/transform/ElementTypes.java @@ -0,0 +1,111 @@ +package org.codefx.libfx.collection.transform; + +import java.util.Objects; + +/** + * Contains inner classes which are used for {@code Transforming...Tests}. + */ +class ElementTypes { + + /** + * A mammal sits at the top of the inheritance hierarchy. + */ + public static class Mammal { + + private final String name; + + /** + * Creates a new mammal with the specified name. + * + * @param name + * the animal's name + */ + public Mammal(String name) { + Objects.requireNonNull(name, "The argument 'name' must not be null."); + this.name = name; + } + + /** + * @return the animal's name + */ + public String getName() { + return name; + } + + @Override + public String toString() { + return "Mammal [name=" + name + "]"; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public final boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof Mammal)) + return false; + Mammal other = (Mammal) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + + } + + /** + * A feline sits in the middle of the inheritance hierarchy. + */ + public static class Feline extends Mammal { + + /** + * Creates a new feline with the specified name. + * + * @param name + * the animal's name + */ + public Feline(String name) { + super(name); + } + + @Override + public String toString() { + return "Feline [name=" + getName() + "]"; + } + + } + + /** + * A cat sits at the bottom of the inheritance hierarchy. + */ + public static class Cat extends Feline { + + /** + * Creates a new cat with the specified name. + * + * @param name + * the animal's name + */ + public Cat(String name) { + super(name); + } + + @Override + public String toString() { + return "Cat [name=" + getName() + "]"; + } + + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/transform/EqualityTransformingMapTest.java b/src/test/java/org/codefx/libfx/collection/transform/EqualityTransformingMapTest.java new file mode 100644 index 0000000..d8fa505 --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/transform/EqualityTransformingMapTest.java @@ -0,0 +1,185 @@ +package org.codefx.libfx.collection.transform; + +import static org.junit.Assert.assertEquals; + +import java.util.AbstractMap.SimpleEntry; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.BiPredicate; +import java.util.function.ToIntFunction; + +import junit.framework.JUnit4TestAdapter; +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.junit.Before; + +import com.google.common.collect.testing.MapTestSuiteBuilder; +import com.google.common.collect.testing.SampleElements; +import com.google.common.collect.testing.TestMapGenerator; +import com.google.common.collect.testing.features.CollectionFeature; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.collect.testing.features.Feature; +import com.google.common.collect.testing.features.MapFeature; + +/** + * Tests {@link EqualityTransformingMap}. + */ +public class EqualityTransformingMapTest { + + /** + * JUnit-3-style method to create the tests run for this class. + * + * @return the tests to run + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.codefx.libfx.collection.transform.EqualityTransformingMap"); + suite.addTest(originalEquality()); + suite.addTest(lengthBasedEquality()); + return suite; + } + + private static Feature[] features() { + return new Feature[] { + // since 'EqualityTransformingMap' passes all calls along, + // the features are determined by the backing data structure (which is a 'HashMap') + CollectionSize.ANY, + MapFeature.ALLOWS_ANY_NULL_QUERIES, + MapFeature.FAILS_FAST_ON_CONCURRENT_MODIFICATION, + MapFeature.SUPPORTS_PUT, + MapFeature.SUPPORTS_REMOVE, + CollectionFeature.SUPPORTS_ITERATOR_REMOVE, + }; + } + + /** + * Creates a test which uses hashCode and equals of the original keys. + * + * @return the test case + */ + private static Test originalEquality() { + return MapTestSuiteBuilder + .using(new TransformingMapGenerator(String::equals, String::hashCode)) + .named("original equality and hashCode") + .withFeatures(features()) + .createTestSuite(); + } + + /** + * Creates a test which uses hashCode and equals based on the string's lengths. + * + * @return the test case + */ + private static Test lengthBasedEquality() { + BiPredicate equals = (s1, s2) -> s1.length() == s2.length(); + ToIntFunction hash = s -> s.length(); + + Test generalTests = MapTestSuiteBuilder + .using(new TransformingMapGenerator(equals, hash)) + .named("length-based equality and hashCode - general tests") + .withFeatures(features()) + .createTestSuite(); + TestSuite specificTests = new TestSuite("length-based equality and hashCode - specific tests"); + specificTests.addTest(new JUnit4TestAdapter(LengthBasedEqualityAndHashCodeTests.class)); + + TestSuite tests = new TestSuite("length-based equality and hashCode"); + tests.addTest(generalTests); + tests.addTest(specificTests); + return tests; + } + + /** + * Tests {@link EqualityTransformingMap} with a specific set of tests geared towards its special functionality, i.e. + * transforming equals and hashCode. + */ + public static class LengthBasedEqualityAndHashCodeTests { + + private Map testedMap; + + private final BiPredicate equals = (s1, s2) -> s1.length() == s2.length(); + + private final ToIntFunction hash = s -> s.length(); + + @Before + @SuppressWarnings("javadoc") + public void createMap() { + testedMap = EqualityTransformingCollectionBuilder + .forType(String.class) + .withEquals(equals) + .withHash(hash) + .buildMap(); + } + + @org.junit.Test + @SuppressWarnings("javadoc") + public void put_getWithSameLengthKey_exists() { + Integer associatedValue = 1000; + testedMap.put("aaa", associatedValue); + + assertEquals(associatedValue, testedMap.get("bbb")); + } + + } + + private static class TransformingMapGenerator implements TestMapGenerator { + + private final BiPredicate equals; + + private final ToIntFunction hash; + + public TransformingMapGenerator(BiPredicate equals, ToIntFunction hash) { + this.equals = equals; + this.hash = hash; + } + + @Override + public SampleElements> samples() { + return new SampleElements>( + new SimpleEntry<>("A", 1), + new SimpleEntry<>("AA", 2), + new SimpleEntry<>("AAA", 3), + new SimpleEntry<>("AAAA", 4), + new SimpleEntry<>("AAAAA", 5)); + } + + @Override + @SuppressWarnings("unchecked") + public Entry[] createArray(int length) { + return new Entry[length]; + } + + @Override + public String[] createKeyArray(int length) { + return new String[length]; + } + + @Override + public Integer[] createValueArray(int length) { + return new Integer[length]; + } + + @Override + public Iterable> order(List> insertionOrder) { + return insertionOrder; + } + + @Override + @SuppressWarnings("unchecked") + public Map create(Object... entries) { + Map transformingMap = EqualityTransformingCollectionBuilder + .forType(String.class) + .withEquals(equals) + .withHash(hash) + .buildMap(); + + Arrays.stream(entries) + .map(entry -> (Entry) entry) + .forEach(entry -> transformingMap.put(entry.getKey(), entry.getValue())); + + return transformingMap; + } + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/transform/EqualityTransformingSetTest.java b/src/test/java/org/codefx/libfx/collection/transform/EqualityTransformingSetTest.java new file mode 100644 index 0000000..7597912 --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/transform/EqualityTransformingSetTest.java @@ -0,0 +1,162 @@ +package org.codefx.libfx.collection.transform; + +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.ToIntFunction; + +import junit.framework.JUnit4TestAdapter; +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.junit.Before; + +import com.google.common.collect.testing.SampleElements; +import com.google.common.collect.testing.SetTestSuiteBuilder; +import com.google.common.collect.testing.TestSetGenerator; +import com.google.common.collect.testing.features.CollectionFeature; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.collect.testing.features.Feature; + +/** + * Tests {@link EqualityTransformingSet}. + */ +public class EqualityTransformingSetTest { + + /** + * JUnit-3-style method to create the tests run for this class. + * + * @return the tests to run + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.codefx.libfx.collection.transform.TransformingSet"); + suite.addTest(originalEquality()); + suite.addTest(lengthBasedEquality()); + return suite; + } + + private static Feature[] features() { + return new Feature[] { + // since 'EqualityTransformingSet' passes all calls along, + // the features are determined by the backing data structure (which is a 'HashSet') + CollectionSize.ANY, + CollectionFeature.ALLOWS_NULL_VALUES, + CollectionFeature.FAILS_FAST_ON_CONCURRENT_MODIFICATION, + CollectionFeature.SUPPORTS_ADD, + CollectionFeature.SUPPORTS_ITERATOR_REMOVE, + CollectionFeature.SUPPORTS_REMOVE, + }; + } + + /** + * Creates a test which uses hashCode and equals of the original keys. + * + * @return the test case + */ + private static Test originalEquality() { + return SetTestSuiteBuilder + .using(new TransformingSetGenerator(String::equals, String::hashCode)) + .named("original equality and hashCode") + .withFeatures(features()) + .createTestSuite(); + } + + /** + * Creates a test which uses hashCode and equals based on the string's lengths. + * + * @return the test case + */ + private static Test lengthBasedEquality() { + BiPredicate equals = (s1, s2) -> s1.length() == s2.length(); + ToIntFunction hash = s -> s.length(); + + Test generalTests = SetTestSuiteBuilder + .using(new TransformingSetGenerator(equals, hash)) + .named("length-based equality and hashCode - general tests") + .withFeatures(features()) + .createTestSuite(); + TestSuite specificTests = new TestSuite("length-based equality and hashCode - specific tests"); + specificTests.addTest(new JUnit4TestAdapter(LengthBasedEqualityAndHashCodeTests.class)); + + TestSuite tests = new TestSuite("length-based equality and hashCode"); + tests.addTest(generalTests); + tests.addTest(specificTests); + return tests; + } + + /** + * Tests {@link EqualityTransformingSet} with a specific set of tests geared towards its special functionality, i.e. + * transforming equals and hashCode. + */ + public static class LengthBasedEqualityAndHashCodeTests { + + private Set testedSet; + + private final BiPredicate equals = (s1, s2) -> s1.length() == s2.length(); + + private final ToIntFunction hash = s -> s.length(); + + @Before + @SuppressWarnings("javadoc") + public void createSet() { + testedSet = EqualityTransformingCollectionBuilder + .forType(String.class) + .withEquals(equals) + .withHash(hash) + .buildSet(); + } + + @org.junit.Test + @SuppressWarnings("javadoc") + public void add_containsWithSameLengthElement_true() { + testedSet.add("aaa"); + + assertTrue(testedSet.contains("bbb")); + } + + } + + private static class TransformingSetGenerator implements TestSetGenerator { + + private final BiPredicate equals; + private final ToIntFunction hash; + + public TransformingSetGenerator(BiPredicate equals, ToIntFunction hash) { + this.equals = equals; + this.hash = hash; + } + + @Override + public Set create(Object... elements) { + Set transformingSet = EqualityTransformingCollectionBuilder + .forType(String.class) + .withEquals(equals) + .withHash(hash) + .buildSet(); + Arrays.stream(elements) + .map(String.class::cast) + .forEach(transformingSet::add); + return transformingSet; + } + + @Override + public SampleElements samples() { + return new SampleElements("A", "AA", "AAA", "AAAA", "AAAAA"); + } + + @Override + public String[] createArray(int length) { + return new String[length]; + } + + @Override + public Iterable order(List insertionOrder) { + return insertionOrder; + } + + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/transform/OptionalTransformingCollectionTest.java b/src/test/java/org/codefx/libfx/collection/transform/OptionalTransformingCollectionTest.java new file mode 100644 index 0000000..43596d3 --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/transform/OptionalTransformingCollectionTest.java @@ -0,0 +1,124 @@ +package org.codefx.libfx.collection.transform; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import com.google.common.base.Objects; +import com.google.common.collect.testing.CollectionTestSuiteBuilder; +import com.google.common.collect.testing.SampleElements; +import com.google.common.collect.testing.TestCollectionGenerator; +import com.google.common.collect.testing.features.CollectionFeature; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.collect.testing.features.Feature; + +/** + * Tests {@link OptionalTransformingCollection}. + */ +public class OptionalTransformingCollectionTest { + + /** + * JUnit-3-style method to create the tests run for this class. + * + * @return the tests to run + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.codefx.libfx.collection.transform.TransformingCollection"); + suite.addTest(optionalWithNullDefaultValue()); + suite.addTest(optionalWithNonNullDefaultValue()); + return suite; + } + + private static Feature[] features() { + return new Feature[] { + // since 'OptionalTransformingCollection' passes all calls along, + // the features are determined by the backing data structure (which is an 'ArrayList') + CollectionSize.ANY, + // exclude 'CollectionFeature.ALLOWS_NULL_VALUES' because nulls are handled in a special way + CollectionFeature.FAILS_FAST_ON_CONCURRENT_MODIFICATION, + CollectionFeature.KNOWN_ORDER, + CollectionFeature.SUPPORTS_ADD, + CollectionFeature.SUPPORTS_ITERATOR_REMOVE, + CollectionFeature.SUPPORTS_REMOVE, + }; + } + + /** + * Creates a test for a collection which us backed by a collection of {@link Optional Optional<String>}. The + * empty Optional is represented with null. + * + * @return the test case + */ + private static Test optionalWithNullDefaultValue() { + return CollectionTestSuiteBuilder + .using(new OptionalTestGenerator(null)) + .named("Optional with null as default") + .withFeatures(features()) + // if null is the default value, the collection allows null values + .withFeatures(CollectionFeature.ALLOWS_NULL_VALUES) + .createTestSuite(); + } + + /** + * Creates a test for a collection which us backed by a collection of {@link Optional Optional<String>}. The + * empty Optional is represented with "DEFAULT". + * + * @return the test case + */ + private static Test optionalWithNonNullDefaultValue() { + return CollectionTestSuiteBuilder + .using(new OptionalTestGenerator("DEFAULT")) + .named("Optional with 'DEFAULT' as default") + // if null is not the default value, the collection does not allow null values + .withFeatures(features()) + .createTestSuite(); + } + + private static class OptionalTestGenerator implements TestCollectionGenerator { + + private final String defaultValue; + + public OptionalTestGenerator(String defaultValue) { + this.defaultValue = defaultValue; + } + + @Override + public SampleElements samples() { + return new SampleElements("A", "B", "C", "D", "E"); + } + + @Override + public Collection create(Object... elements) { + Collection> optionalStrings = createCollectionOfOptionalStrings(elements); + return new OptionalTransformingCollection<>(optionalStrings, String.class, defaultValue); + } + + private Collection> createCollectionOfOptionalStrings(Object... elements) { + List> optionalStrings = new ArrayList<>(); + for (Object element : elements) { + String string = (String) element; + optionalStrings.add( + Objects.equal(string, defaultValue) + ? Optional.empty() + : Optional.of(string)); + } + return optionalStrings; + } + + @Override + public String[] createArray(int length) { + return new String[length]; + } + + @Override + public Iterable order(List insertionOrder) { + return insertionOrder; + } + + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/transform/OptionalTransformingListTest.java b/src/test/java/org/codefx/libfx/collection/transform/OptionalTransformingListTest.java new file mode 100644 index 0000000..d0e2de0 --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/transform/OptionalTransformingListTest.java @@ -0,0 +1,127 @@ +package org.codefx.libfx.collection.transform; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import com.google.common.base.Objects; +import com.google.common.collect.testing.ListTestSuiteBuilder; +import com.google.common.collect.testing.SampleElements; +import com.google.common.collect.testing.TestListGenerator; +import com.google.common.collect.testing.features.CollectionFeature; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.collect.testing.features.Feature; +import com.google.common.collect.testing.features.ListFeature; + +/** + * Tests {@link OptionalTransformingList}. + */ +public class OptionalTransformingListTest { + + /** + * JUnit-3-style method to create the tests run for this class. + * + * @return the tests to run + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.codefx.libfx.collection.transform.TransformingList"); + suite.addTest(optionalWithNullDefaultValue()); + suite.addTest(optionalWithNonNullDefaultValue()); + return suite; + } + + private static Feature[] features() { + return new Feature[] { + // since 'OptionalTransformingList' passes all calls along, + // the features are determined by the backing data structure (which is an 'ArrayList') + CollectionSize.ANY, + // exclude 'CollectionFeature.ALLOWS_NULL_VALUES' because nulls are handled in a special way + CollectionFeature.FAILS_FAST_ON_CONCURRENT_MODIFICATION, + CollectionFeature.KNOWN_ORDER, + CollectionFeature.SUPPORTS_ADD, + CollectionFeature.SUPPORTS_ITERATOR_REMOVE, + CollectionFeature.SUPPORTS_REMOVE, + ListFeature.SUPPORTS_ADD_WITH_INDEX, + ListFeature.SUPPORTS_SET, + ListFeature.SUPPORTS_REMOVE_WITH_INDEX, + }; + } + + /** + * Creates a test for a set which us backed by a collection of {@link Optional Optional<String>}. The empty + * Optional is represented with null. + * + * @return the test case + */ + private static Test optionalWithNullDefaultValue() { + return ListTestSuiteBuilder + .using(new OptionalTestGenerator(null)) + .named("Optional with null as default") + .withFeatures(features()) + // if null is the default value, the collection allows null values + .withFeatures(CollectionFeature.ALLOWS_NULL_VALUES) + .createTestSuite(); + } + + /** + * Creates a test for a set which us backed by a collection of {@link Optional Optional<String>}. The empty + * Optional is represented with "DEFAULT". + * + * @return the test case + */ + private static Test optionalWithNonNullDefaultValue() { + return ListTestSuiteBuilder + .using(new OptionalTestGenerator("DEFAULT")) + .named("Optional with 'DEFAULT' as default") + // if null is not the default value, the set does not allow null values + .withFeatures(features()) + .createTestSuite(); + } + + private static class OptionalTestGenerator implements TestListGenerator { + + private final String defaultValue; + + public OptionalTestGenerator(String defaultValue) { + this.defaultValue = defaultValue; + } + + @Override + public SampleElements samples() { + return new SampleElements("A", "B", "C", "D", "E"); + } + + @Override + public List create(Object... elements) { + List> optionalStrings = createListOfOptionalStrings(elements); + return new OptionalTransformingList<>(optionalStrings, String.class, defaultValue); + } + + private List> createListOfOptionalStrings(Object... elements) { + List> optionalStrings = new ArrayList<>(); + for (Object element : elements) { + String string = (String) element; + optionalStrings.add( + Objects.equal(string, defaultValue) + ? Optional.empty() + : Optional.of(string)); + } + return optionalStrings; + } + + @Override + public String[] createArray(int length) { + return new String[length]; + } + + @Override + public Iterable order(List insertionOrder) { + return insertionOrder; + } + + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/transform/OptionalTransformingSetTest.java b/src/test/java/org/codefx/libfx/collection/transform/OptionalTransformingSetTest.java new file mode 100644 index 0000000..19621c4 --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/transform/OptionalTransformingSetTest.java @@ -0,0 +1,123 @@ +package org.codefx.libfx.collection.transform; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import com.google.common.base.Objects; +import com.google.common.collect.testing.SampleElements; +import com.google.common.collect.testing.SetTestSuiteBuilder; +import com.google.common.collect.testing.TestSetGenerator; +import com.google.common.collect.testing.features.CollectionFeature; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.collect.testing.features.Feature; + +/** + * Tests {@link OptionalTransformingSet}. + */ +public class OptionalTransformingSetTest { + + /** + * JUnit-3-style method to create the tests run for this class. + * + * @return the tests to run + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.codefx.libfx.collection.transform.TransformingSet"); + suite.addTest(optionalWithNullDefaultValue()); + suite.addTest(optionalWithNonNullDefaultValue()); + return suite; + } + + private static Feature[] features() { + return new Feature[] { + // since 'OptionalTransformingSet' passes all calls along, + // the features are determined by the backing data structure (which is a 'HashSet') + CollectionSize.ANY, + // exclude 'CollectionFeature.ALLOWS_NULL_VALUES' because nulls are handled in a special way + CollectionFeature.FAILS_FAST_ON_CONCURRENT_MODIFICATION, + CollectionFeature.SUPPORTS_ADD, + CollectionFeature.SUPPORTS_ITERATOR_REMOVE, + CollectionFeature.SUPPORTS_REMOVE, + }; + } + + /** + * Creates a test for a set which us backed by a collection of {@link Optional Optional<String>}. The empty + * Optional is represented with null. + * + * @return the test case + */ + private static Test optionalWithNullDefaultValue() { + return SetTestSuiteBuilder + .using(new OptionalTestGenerator(null)) + .named("Optional with null as default") + .withFeatures(features()) + // if null is the default value, the collection allows null values + .withFeatures(CollectionFeature.ALLOWS_NULL_VALUES) + .createTestSuite(); + } + + /** + * Creates a test for a set which us backed by a collection of {@link Optional Optional<String>}. The empty + * Optional is represented with "DEFAULT". + * + * @return the test case + */ + private static Test optionalWithNonNullDefaultValue() { + return SetTestSuiteBuilder + .using(new OptionalTestGenerator("DEFAULT")) + .named("Optional with 'DEFAULT' as default") + // if null is not the default value, the set does not allow null values + .withFeatures(features()) + .createTestSuite(); + } + + private static class OptionalTestGenerator implements TestSetGenerator { + + private final String defaultValue; + + public OptionalTestGenerator(String defaultValue) { + this.defaultValue = defaultValue; + } + + @Override + public SampleElements samples() { + return new SampleElements("A", "B", "C", "D", "E"); + } + + @Override + public Set create(Object... elements) { + Set> optionalStrings = createSetOfOptionalStrings(elements); + return new OptionalTransformingSet<>(optionalStrings, String.class, defaultValue); + } + + private Set> createSetOfOptionalStrings(Object... elements) { + Set> optionalStrings = new HashSet<>(); + for (Object element : elements) { + String string = (String) element; + optionalStrings.add( + Objects.equal(string, defaultValue) + ? Optional.empty() + : Optional.of(string)); + } + return optionalStrings; + } + + @Override + public String[] createArray(int length) { + return new String[length]; + } + + @Override + public Iterable order(List insertionOrder) { + return insertionOrder; + } + + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/transform/TransformingCollectionTest.java b/src/test/java/org/codefx/libfx/collection/transform/TransformingCollectionTest.java new file mode 100644 index 0000000..f7f8fd8 --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/transform/TransformingCollectionTest.java @@ -0,0 +1,152 @@ +package org.codefx.libfx.collection.transform; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.codefx.libfx.collection.transform.ElementTypes.Cat; +import org.codefx.libfx.collection.transform.ElementTypes.Feline; +import org.codefx.libfx.collection.transform.ElementTypes.Mammal; + +import com.google.common.collect.testing.CollectionTestSuiteBuilder; +import com.google.common.collect.testing.SampleElements; +import com.google.common.collect.testing.TestCollectionGenerator; +import com.google.common.collect.testing.features.CollectionFeature; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.collect.testing.features.Feature; + +/** + * Tests {@link TransformingCollection}. + */ +public class TransformingCollectionTest { + + /** + * JUnit-3-style method to create the tests run for this class. + * + * @return the tests to run + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.codefx.libfx.collection.transform.TransformingCollection"); + suite.addTest(backingCollectionHasSupertype()); + suite.addTest(backingCollectionHasSubtype()); + return suite; + } + + private static Feature[] features() { + return new Feature[] { + // since 'TransformedCollection' passes all calls along, + // the features are determined by the backing data structure (which is an 'ArrayList') + CollectionSize.ANY, + CollectionFeature.ALLOWS_NULL_VALUES, // includes ALLOWS_NULL_QUERIES + CollectionFeature.FAILS_FAST_ON_CONCURRENT_MODIFICATION, + CollectionFeature.KNOWN_ORDER, + CollectionFeature.SUPPORTS_ADD, + CollectionFeature.SUPPORTS_ITERATOR_REMOVE, + CollectionFeature.SUPPORTS_REMOVE, + }; + } + + /** + * Creates a test for a feline collection which us backed by a mammal collection (i.e. a supertype). + * + * @return the test case + */ + private static Test backingCollectionHasSupertype() { + return CollectionTestSuiteBuilder + .using(new TransformingCollectionTestGenerator(Mammal.class)) + .named("backed by supertype") + .withFeatures(features()) + .createTestSuite(); + } + + /** + * Creates a test for a feline collection which us backed by a cat collection (i.e. a subtype). + * + * @return the test case + */ + private static Test backingCollectionHasSubtype() { + return CollectionTestSuiteBuilder + .using(new TransformingCollectionTestGenerator(Cat.class)) + .named("backed by subtype") + .withFeatures(features()) + .createTestSuite(); + } + + private static class TransformingCollectionTestGenerator implements TestCollectionGenerator { + + private final Class backingSetGenericType; + + public TransformingCollectionTestGenerator(Class backingSetGenericType) { + this.backingSetGenericType = backingSetGenericType; + } + + @Override + public SampleElements samples() { + return new SampleElements( + new Feline("A"), new Feline("B"), new Feline("C"), new Feline("D"), new Feline("E")); + } + + @Override + public Feline[] createArray(int length) { + return new Feline[length]; + } + + @Override + public Iterable order(List insertionOrder) { + return insertionOrder; + } + + @Override + public Collection create(Object... elements) { + if (backingSetGenericType.equals(Mammal.class)) + return createBackedByMammal(elements); + + if (backingSetGenericType.equals(Cat.class)) + return createBackedByCat(elements); + + throw new UnsupportedOperationException(); + } + + private static Collection createBackedByMammal(Object[] felines) { + List mammals = new ArrayList<>(); + for (Object feline : felines) + if (feline == null) + mammals.add(null); + else { + String name = ((Feline) feline).getName(); + mammals.add(new Mammal(name)); + } + return new TransformingCollection<>( + mammals, + /* + * Because 'Feline' does not uphold the Liskov Substitution Principle (by having its own 'toString' + * method) felines can not masquerade as mammals. Hence create a new mammal for each feline. + */ + Mammal.class, Feline.class, + mammal -> new Feline(mammal.getName()), feline -> new Mammal(feline.getName())); + } + + private static Collection createBackedByCat(Object[] felines) { + List cats = new ArrayList<>(); + for (Object feline : felines) + if (feline == null) + cats.add(null); + else { + String name = ((Feline) feline).getName(); + cats.add(new Cat(name)); + } + return new TransformingCollection<>( + cats, + /* + * Because 'Cat' does not uphold the Liskov Substitution Principle (by having its own 'toString' + * method) cats can not masquerade as felines. Hence create a new feline for each cat. + */ + Cat.class, Feline.class, + cat -> new Feline(cat.getName()), feline -> new Cat(feline.getName())); + } + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/transform/TransformingListTest.java b/src/test/java/org/codefx/libfx/collection/transform/TransformingListTest.java new file mode 100644 index 0000000..2f70672 --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/transform/TransformingListTest.java @@ -0,0 +1,155 @@ +package org.codefx.libfx.collection.transform; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.codefx.libfx.collection.transform.ElementTypes.Cat; +import org.codefx.libfx.collection.transform.ElementTypes.Feline; +import org.codefx.libfx.collection.transform.ElementTypes.Mammal; + +import com.google.common.collect.testing.ListTestSuiteBuilder; +import com.google.common.collect.testing.SampleElements; +import com.google.common.collect.testing.TestListGenerator; +import com.google.common.collect.testing.features.CollectionFeature; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.collect.testing.features.Feature; +import com.google.common.collect.testing.features.ListFeature; + +/** + * Tests {@link TransformingCollection}. + */ +public class TransformingListTest { + + /** + * JUnit-3-style method to create the tests run for this class. + * + * @return the tests to run + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.codefx.libfx.collection.transform.TransformingCollection"); + suite.addTest(backingListHasSupertype()); + suite.addTest(backingListHasSubtype()); + return suite; + } + + private static Feature[] features() { + return new Feature[] { + // since 'TransformedCollection' passes all calls along, + // the features are determined by the backing data structure (which is an 'ArrayList') + CollectionSize.ANY, + CollectionFeature.ALLOWS_NULL_VALUES, // includes ALLOWS_NULL_QUERIES + CollectionFeature.FAILS_FAST_ON_CONCURRENT_MODIFICATION, + CollectionFeature.KNOWN_ORDER, + CollectionFeature.SUPPORTS_ADD, + CollectionFeature.SUPPORTS_ITERATOR_REMOVE, + CollectionFeature.SUPPORTS_REMOVE, + ListFeature.SUPPORTS_ADD_WITH_INDEX, + ListFeature.SUPPORTS_SET, + ListFeature.SUPPORTS_REMOVE_WITH_INDEX, + }; + } + + /** + * Creates a test for a feline list which us backed by a mammal list (i.e. a supertype). + * + * @return the test case + */ + private static Test backingListHasSupertype() { + return ListTestSuiteBuilder + .using(new TransformingListTestGenerator(Mammal.class)) + .named("backed by supertype") + .withFeatures(features()) + .createTestSuite(); + } + + /** + * Creates a test for a feline list which us backed by a cat list (i.e. a subtype). + * + * @return the test case + */ + private static Test backingListHasSubtype() { + return ListTestSuiteBuilder + .using(new TransformingListTestGenerator(Cat.class)) + .named("backed by subtype") + .withFeatures(features()) + .createTestSuite(); + } + + private static class TransformingListTestGenerator implements TestListGenerator { + + private final Class backingSetGenericType; + + public TransformingListTestGenerator(Class backingSetGenericType) { + this.backingSetGenericType = backingSetGenericType; + } + + @Override + public SampleElements samples() { + return new SampleElements( + new Feline("A"), new Feline("B"), new Feline("C"), new Feline("D"), new Feline("E")); + } + + @Override + public Feline[] createArray(int length) { + return new Feline[length]; + } + + @Override + public Iterable order(List insertionOrder) { + return insertionOrder; + } + + @Override + public List create(Object... elements) { + if (backingSetGenericType.equals(Mammal.class)) + return createBackedByMammal(elements); + + if (backingSetGenericType.equals(Cat.class)) + return createBackedByCat(elements); + + throw new UnsupportedOperationException(); + } + + private static List createBackedByMammal(Object[] felines) { + List mammals = new ArrayList<>(); + for (Object feline : felines) + if (feline == null) + mammals.add(null); + else { + String name = ((Feline) feline).getName(); + mammals.add(new Mammal(name)); + } + return new TransformingList<>( + mammals, + /* + * Because 'Feline' does not uphold the Liskov Substitution Principle (by having its own 'toString' + * method) felines can not masquerade as mammals. Hence create a new mammal for each feline. + */ + Mammal.class, Feline.class, + mammal -> new Feline(mammal.getName()), feline -> new Mammal(feline.getName())); + } + + private static List createBackedByCat(Object[] felines) { + List cats = new ArrayList<>(); + for (Object feline : felines) + if (feline == null) + cats.add(null); + else { + String name = ((Feline) feline).getName(); + cats.add(new Cat(name)); + } + return new TransformingList<>( + cats, + /* + * Because 'Cat' does not uphold the Liskov Substitution Principle (by having its own 'toString' + * method) cats can not masquerade as felines. Hence create a new feline for each cat. + */ + Cat.class, Feline.class, + cat -> new Feline(cat.getName()), feline -> new Cat(feline.getName())); + } + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/transform/TransformingMapTest.java b/src/test/java/org/codefx/libfx/collection/transform/TransformingMapTest.java new file mode 100644 index 0000000..e9b6af5 --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/transform/TransformingMapTest.java @@ -0,0 +1,167 @@ +package org.codefx.libfx.collection.transform; + +import java.util.AbstractMap.SimpleEntry; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.codefx.libfx.collection.transform.ElementTypes.Cat; +import org.codefx.libfx.collection.transform.ElementTypes.Feline; +import org.codefx.libfx.collection.transform.ElementTypes.Mammal; + +import com.google.common.collect.testing.MapTestSuiteBuilder; +import com.google.common.collect.testing.SampleElements; +import com.google.common.collect.testing.TestMapGenerator; +import com.google.common.collect.testing.features.CollectionFeature; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.collect.testing.features.Feature; +import com.google.common.collect.testing.features.MapFeature; + +/** + * Tests {@link TransformingMap}. + */ +public class TransformingMapTest { + + /** + * JUnit-3-style method to create the tests run for this class. + * + * @return the tests to run + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.codefx.libfx.collection.transform.TransformingMap"); + suite.addTest(backingMapHasSupertype()); + suite.addTest(backingMapHasSubtype()); + return suite; + } + + private static Feature[] features() { + return new Feature[] { + // since 'TransformedMap' passes all calls along, + // the features are determined by the backing data structure (which is a 'HashMap') + CollectionSize.ANY, + MapFeature.ALLOWS_ANY_NULL_QUERIES, + MapFeature.FAILS_FAST_ON_CONCURRENT_MODIFICATION, + MapFeature.SUPPORTS_PUT, + MapFeature.SUPPORTS_REMOVE, + CollectionFeature.SUPPORTS_ITERATOR_REMOVE, + }; + } + + /** + * Creates a test for a feline map which us backed by a mammal map (i.e. a supertype). + * + * @return the test case + */ + private static Test backingMapHasSupertype() { + return MapTestSuiteBuilder + .using(new TransformingMapGenerator(Mammal.class)) + .named("backed by supertype") + .withFeatures(features()) + .createTestSuite(); + } + + /** + * Creates a test for a feline map which us backed by a cat map (i.e. a subtype). + * + * @return the test case + */ + private static Test backingMapHasSubtype() { + return MapTestSuiteBuilder + .using(new TransformingMapGenerator(Cat.class)) + .named("backed by subtype") + .withFeatures(features()) + .createTestSuite(); + } + + private static class TransformingMapGenerator implements TestMapGenerator { + + private final Class backingMapGenericType; + + public TransformingMapGenerator(Class backingMapGenericType) { + this.backingMapGenericType = backingMapGenericType; + } + + @Override + public SampleElements> samples() { + return new SampleElements>( + new SimpleEntry<>(new Feline("A"), new Feline("1")), + new SimpleEntry<>(new Feline("B"), new Feline("2")), + new SimpleEntry<>(new Feline("C"), new Feline("3")), + new SimpleEntry<>(new Feline("D"), new Feline("4")), + new SimpleEntry<>(new Feline("E"), new Feline("5"))); + } + + @Override + @SuppressWarnings("unchecked") + public Entry[] createArray(int length) { + return new Entry[length]; + } + + @Override + public Feline[] createKeyArray(int length) { + return new Feline[length]; + } + + @Override + public Feline[] createValueArray(int length) { + return new Feline[length]; + } + + @Override + public Iterable> order(List> insertionOrder) { + return insertionOrder; + } + + @Override + public Map create(Object... entries) { + if (backingMapGenericType.equals(Mammal.class)) + return createBackedByMammalMap(entries); + + if (backingMapGenericType.equals(Cat.class)) + return createBackedByCatMap(entries); + + throw new UnsupportedOperationException(); + } + + private static Map createBackedByMammalMap(Object[] entries) { + Map mammals = new HashMap<>(); + for (Object entry : entries) { + String keyName = ((Feline) ((Entry) entry).getKey()).getName(); + String valueName = ((Feline) ((Entry) entry).getValue()).getName(); + mammals.put(new Mammal(keyName), new Mammal(valueName)); + } + // Because 'Feline' does not uphold the Liskov Substitution Principle (by having its own 'toString' + // method) felines can not masquerade as mammals. Hence create a new mammal for each feline. + return TransformingMapBuilder + .forTypes(Mammal.class, Feline.class, Mammal.class, Feline.class) + .toOuterKey(mammal -> new Feline(mammal.getName())) + .toInnerKey(feline -> new Cat(feline.getName())) + .toOuterValue(mammal -> new Feline(mammal.getName())) + .toInnerValue(feline -> new Cat(feline.getName())) + .transformMap(mammals); + } + + private static Map createBackedByCatMap(Object[] entries) { + Map cats = new HashMap<>(); + for (Object entry : entries) { + String keyName = ((Feline) ((Entry) entry).getKey()).getName(); + String valueName = ((Feline) ((Entry) entry).getValue()).getName(); + cats.put(new Cat(keyName), new Cat(valueName)); + } + // Because 'Cat' does not uphold the Liskov Substitution Principle (by having its own 'toString' + // method) cats can not masquerade as felines. Hence create a new feline for each cat. + return TransformingMapBuilder + .forTypes(Cat.class, Feline.class, Cat.class, Feline.class) + .toOuterKey(cat -> new Feline(cat.getName())) + .toInnerKey(feline -> new Cat(feline.getName())) + .toOuterValue(cat -> new Feline(cat.getName())) + .toInnerValue(feline -> new Cat(feline.getName())) + .transformMap(cats); + } + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/transform/TransformingSetTest.java b/src/test/java/org/codefx/libfx/collection/transform/TransformingSetTest.java new file mode 100644 index 0000000..3fff04b --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/transform/TransformingSetTest.java @@ -0,0 +1,151 @@ +package org.codefx.libfx.collection.transform; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.codefx.libfx.collection.transform.ElementTypes.Cat; +import org.codefx.libfx.collection.transform.ElementTypes.Feline; +import org.codefx.libfx.collection.transform.ElementTypes.Mammal; + +import com.google.common.collect.testing.SampleElements; +import com.google.common.collect.testing.SetTestSuiteBuilder; +import com.google.common.collect.testing.TestSetGenerator; +import com.google.common.collect.testing.features.CollectionFeature; +import com.google.common.collect.testing.features.CollectionSize; +import com.google.common.collect.testing.features.Feature; + +/** + * Tests {@link TransformingSet}. + */ +public class TransformingSetTest { + + /** + * JUnit-3-style method to create the tests run for this class. + * + * @return the tests to run + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.codefx.libfx.collection.transform.TransformingSet"); + suite.addTest(backingSetHasSupertype()); + suite.addTest(backingSetHasSubtype()); + return suite; + } + + private static Feature[] features() { + return new Feature[] { + // since 'TransformedSet' passes all calls along, + // the features are determined by the backing data structure (which is a 'HashSet') + CollectionSize.ANY, + CollectionFeature.ALLOWS_NULL_VALUES, // includes ALLOWS_NULL_QUERIES + CollectionFeature.FAILS_FAST_ON_CONCURRENT_MODIFICATION, + CollectionFeature.SUPPORTS_ADD, + CollectionFeature.SUPPORTS_ITERATOR_REMOVE, + CollectionFeature.SUPPORTS_REMOVE, + }; + } + + /** + * Creates a test for a feline set which us backed by a mammal set (i.e. a supertype). + * + * @return the test case + */ + private static Test backingSetHasSupertype() { + return SetTestSuiteBuilder + .using(new TransformingSetGenerator(Mammal.class)) + .named("backed by supertype") + .withFeatures(features()) + .createTestSuite(); + } + + /** + * Creates a test for a feline set which us backed by a cat set (i.e. a subtype). + * + * @return the test case + */ + private static Test backingSetHasSubtype() { + return SetTestSuiteBuilder + .using(new TransformingSetGenerator(Cat.class)) + .named("backed by subtype") + .withFeatures(features()) + .createTestSuite(); + } + + private static class TransformingSetGenerator implements TestSetGenerator { + + private final Class backingSetGenericType; + + public TransformingSetGenerator(Class backingSetGenericType) { + this.backingSetGenericType = backingSetGenericType; + } + + @Override + public SampleElements samples() { + return new SampleElements( + new Feline("A"), new Feline("B"), new Feline("C"), new Feline("D"), new Feline("E")); + } + + @Override + public Feline[] createArray(int length) { + return new Feline[length]; + } + + @Override + public Iterable order(List insertionOrder) { + return insertionOrder; + } + + @Override + public Set create(Object... elements) { + if (backingSetGenericType.equals(Mammal.class)) + return createBackedByMammalSet(elements); + + if (backingSetGenericType.equals(Cat.class)) + return createBackedByCatSet(elements); + + throw new UnsupportedOperationException(); + } + + private static Set createBackedByMammalSet(Object[] felines) { + Set mammals = new HashSet<>(); + for (Object feline : felines) + if (feline == null) + mammals.add(null); + else { + String name = ((Feline) feline).getName(); + mammals.add(new Mammal(name)); + } + return new TransformingSet<>( + mammals, + /* + * Because 'Feline' does not uphold the Liskov Substitution Principle (by having its own 'toString' + * method) felines can not masquerade as mammals. Hence create a new mammal for each feline. + */ + Mammal.class, Feline.class, + mammal -> new Feline(mammal.getName()), feline -> new Mammal(feline.getName())); + } + + private static Set createBackedByCatSet(Object[] felines) { + Set cats = new HashSet<>(); + for (Object feline : felines) + if (feline == null) + cats.add(null); + else { + String name = ((Feline) feline).getName(); + cats.add(new Cat(name)); + } + return new TransformingSet<>( + cats, + /* + * Because 'Cat' does not uphold the Liskov Substitution Principle (by having its own 'toString' + * method) cats can not masquerade as felines. Hence create a new feline for each cat. + */ + Cat.class, Feline.class, + cat -> new Feline(cat.getName()), feline -> new Cat(feline.getName())); + } + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/tree/navigate/AbstractTreeNavigatorTest.java b/src/test/java/org/codefx/libfx/collection/tree/navigate/AbstractTreeNavigatorTest.java new file mode 100644 index 0000000..e8023ef --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/tree/navigate/AbstractTreeNavigatorTest.java @@ -0,0 +1,182 @@ +package org.codefx.libfx.collection.tree.navigate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; + +import java.util.Optional; +import java.util.OptionalInt; + +import org.junit.Before; +import org.junit.Test; + +/** + * Abstract superclass for tests of {@link TreeNavigator} implementations. + * + * @param + * the type of elements in the tree which is navigated by the tested navigator + */ +public abstract class AbstractTreeNavigatorTest { + + private TreeNavigator navigator; + + @Before + @SuppressWarnings("javadoc") + public void setUp() { + navigator = createNavigator(); + } + + // getParent + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void getParent_childNull_throwsNullPointerException() throws Exception { + navigator.getParent(null); + } + + @Test + @SuppressWarnings("javadoc") + public void getParent_nodeWithoutParent_returnsEmptyOptional() throws Exception { + E parentlessNode = createSingletonNode(); + + Optional parent = navigator.getParent(parentlessNode); + + assertFalse(parent.isPresent()); + } + + @Test + @SuppressWarnings("javadoc") + public void getParent_nodeWithParent_returnsParent() throws Exception { + E parent = createNodeWithChildren(1); + E child = getChildOfParent(parent, 0); + + Optional proclaimedParent = navigator.getParent(child); + + assertSame(parent, proclaimedParent.get()); + } + + // getChidlIndex + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void getChildIndex_nodeNull_throwsNullPointerException() throws Exception { + navigator.getChildIndex(null); + } + + @Test + @SuppressWarnings("javadoc") + public void getChildIndex_nodeWithoutParent_emptyOptional() throws Exception { + E parentlessNode = createSingletonNode(); + + OptionalInt childIndex = navigator.getChildIndex(parentlessNode); + + assertFalse(childIndex.isPresent()); + } + + @Test + @SuppressWarnings("javadoc") + public void getChildIndex_nodeWithParent_returnsCorrectIndex() throws Exception { + E parent = createNodeWithChildren(5); + + for (int childIndex = 0; childIndex < 5; childIndex++) { + E child = getChildOfParent(parent, childIndex); + OptionalInt proclaimedChildIndex = navigator.getChildIndex(child); + assertEquals(childIndex, proclaimedChildIndex.getAsInt()); + } + } + + // getChildrenCount + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void getChildrenCount_parentNull_throwsNullPointerException() throws Exception { + navigator.getChildrenCount(null); + } + + @Test + @SuppressWarnings("javadoc") + public void getChildCount_nodeWithoutChildren_returns0() throws Exception { + E node = createSingletonNode(); + + int proclaimedChildCount = navigator.getChildrenCount(node); + + assertEquals(0, proclaimedChildCount); + } + + @Test + @SuppressWarnings("javadoc") + public void getChildrenCount_nodeWithChildren_returnsCorrectCount() throws Exception { + for (int childCount = 1; childCount < 5; childCount++) { + E parent = createNodeWithChildren(childCount); + int proclaimedChildCount = navigator.getChildrenCount(parent); + assertEquals(childCount, proclaimedChildCount); + } + } + + // getChild + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void getChild_parentNull_throwsNullPointerException() throws Exception { + navigator.getChild(null, 0); + } + + @Test(expected = IllegalArgumentException.class) + @SuppressWarnings("javadoc") + public void getChild_childIndexNegative_throwsIllegalArgumentException() throws Exception { + navigator.getChild(createSingletonNode(), -1); + } + + @Test + @SuppressWarnings("javadoc") + public void getChild_noChildWithThatIndex_returnsEmptyOptional() throws Exception { + for (int childCount = 0; childCount < 5; childCount++) { + E parent = createNodeWithChildren(childCount); + Optional child = navigator.getChild(parent, childCount); + assertFalse(child.isPresent()); + } + } + + @Test + @SuppressWarnings("javadoc") + public void getChild_existingChild_returnsChild() throws Exception { + E parent = createNodeWithChildren(5); + + for (int childIndex = 0; childIndex < 5; childIndex++) { + Optional proclaimedChild = navigator.getChild(parent, childIndex); + E child = getChildOfParent(parent, childIndex); + assertSame(child, proclaimedChild.get()); + } + } + + // #begin ABSTRACT METHODS + + /** + * @return the tested navigator + */ + protected abstract TreeNavigator createNavigator(); + + /** + * @return a node which as neither parents nor children + */ + protected abstract E createSingletonNode(); + + /** + * @param nrOfChildren + * the exact number of children the created node will have + * @return a node with the specified number of children + */ + protected abstract E createNodeWithChildren(int nrOfChildren); + + /** + * @param parent + * a node which was created with {@link #createNodeWithChildren(int)} + * @param childIndex + * the index of the requested child with {@code 0 < childIndex < argFor_createNodeWithChildren} + * @return the node which is the child with the specified index of the specified parent + */ + protected abstract E getChildOfParent(E parent, int childIndex); + + // #end ABSTRACT METHODS + +} diff --git a/src/test/java/org/codefx/libfx/collection/tree/navigate/ComponentHierarchyNavigatorTest.java b/src/test/java/org/codefx/libfx/collection/tree/navigate/ComponentHierarchyNavigatorTest.java new file mode 100644 index 0000000..f0127bb --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/tree/navigate/ComponentHierarchyNavigatorTest.java @@ -0,0 +1,35 @@ +package org.codefx.libfx.collection.tree.navigate; + +import java.awt.Component; +import java.awt.Panel; +import java.awt.TextArea; + +/** + * Tests {@link ComponentHierarchyNavigator}. + */ +public class ComponentHierarchyNavigatorTest extends AbstractTreeNavigatorTest { + + @Override + protected TreeNavigator createNavigator() { + return new ComponentHierarchyNavigator(); + } + + @Override + protected Component createSingletonNode() { + return new TextArea("A component without parent and children."); + } + + @Override + protected Component createNodeWithChildren(int nrOfChildren) { + Panel panel = new Panel(); + for (int childIndex = 0; childIndex < nrOfChildren; childIndex++) + panel.add(new TextArea("Child #" + childIndex)); + return panel; + } + + @Override + protected Component getChildOfParent(Component parent, int childIndex) { + return ((Panel) parent).getComponent(childIndex); + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/tree/navigate/JComponentHierarchyNavigatorTest.java b/src/test/java/org/codefx/libfx/collection/tree/navigate/JComponentHierarchyNavigatorTest.java new file mode 100644 index 0000000..68edafc --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/tree/navigate/JComponentHierarchyNavigatorTest.java @@ -0,0 +1,35 @@ +package org.codefx.libfx.collection.tree.navigate; + +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JTextArea; + +/** + * Tests {@link JComponentHierarchyNavigator}. + */ +public class JComponentHierarchyNavigatorTest extends AbstractTreeNavigatorTest { + + @Override + protected TreeNavigator createNavigator() { + return new JComponentHierarchyNavigator(); + } + + @Override + protected JComponent createSingletonNode() { + return new JTextArea("A jComponent without parent and children."); + } + + @Override + protected JComponent createNodeWithChildren(int nrOfChildren) { + JPanel panel = new JPanel(); + for (int childIndex = 0; childIndex < nrOfChildren; childIndex++) + panel.add(new JTextArea("Child #" + childIndex)); + return panel; + } + + @Override + protected JComponent getChildOfParent(JComponent parent, int childIndex) { + return (JTextArea) ((JPanel) parent).getComponent(childIndex); + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/tree/navigate/SceneGraphNavigatorTest.java b/src/test/java/org/codefx/libfx/collection/tree/navigate/SceneGraphNavigatorTest.java new file mode 100644 index 0000000..6d31f16 --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/tree/navigate/SceneGraphNavigatorTest.java @@ -0,0 +1,44 @@ +package org.codefx.libfx.collection.tree.navigate; + +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.control.TextArea; + +import org.codefx.tarkastus.JavaFXRule; +import org.junit.Rule; + +/** + * Tests {@link SceneGraphNavigator}. + */ +public class SceneGraphNavigatorTest extends AbstractTreeNavigatorTest { + + /** + * Runs all tests in the JavaFX platform thread. + */ + @Rule + public JavaFXRule javaFXRule = new JavaFXRule(); + + @Override + protected TreeNavigator createNavigator() { + return new SceneGraphNavigator(); + } + + @Override + protected Node createSingletonNode() { + return new TextArea("A node without parents and children."); + } + + @Override + protected Node createNodeWithChildren(int nrOfChildren) { + Group parent = new Group(); + for (int i = 0; i < nrOfChildren; i++) + parent.getChildren().add(new TextArea("Child #" + nrOfChildren)); + return parent; + } + + @Override + protected Node getChildOfParent(Node parent, int childIndex) { + return ((Group) parent).getChildren().get(childIndex); + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/tree/stream/AbstractTreePathTest.java b/src/test/java/org/codefx/libfx/collection/tree/stream/AbstractTreePathTest.java new file mode 100644 index 0000000..dc2d6b8 --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/tree/stream/AbstractTreePathTest.java @@ -0,0 +1,165 @@ +package org.codefx.libfx.collection.tree.stream; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.util.NoSuchElementException; +import java.util.Optional; + +import org.codefx.libfx.collection.tree.stream.TreePath; +import org.junit.Before; +import org.junit.Test; + +/** + * Abstract super class to tests of {@link TreePath} implementations. + */ +public abstract class AbstractTreePathTest { + + private TreePath path; + + @Before + @SuppressWarnings("javadoc") + public void createPath() { + path = createEmptyPath(); + } + + // is empty + + @Test + @SuppressWarnings("javadoc") + public void isEmpty_emptyPath_returnsTrue() throws Exception { + assertTrue(path.isEmpty()); + } + + @Test + @SuppressWarnings("javadoc") + public void isEmpty_nonEmptyPath_false() throws Exception { + path = createPath("element"); + + assertFalse(path.isEmpty()); + } + + // get + + @Test + @SuppressWarnings("javadoc") + public void getEnd_emptyPath_returnsEmptyOptional() throws Exception { + Optional end = path.getEnd(); + + assertFalse(end.isPresent()); + } + + @Test + @SuppressWarnings("javadoc") + public void getEnd_nonEmptyPath_returnsLastElement() throws Exception { + String lastElement = "last"; + path = createPath("first", "some", lastElement); + + Optional end = path.getEnd(); + + assertSame(lastElement, end.get()); + } + + // remove + + @Test(expected = NoSuchElementException.class) + @SuppressWarnings("javadoc") + public void removeEnd_emptyPath_throwsNoSuchElementException() throws Exception { + path.removeEnd(); + } + + @Test + @SuppressWarnings("javadoc") + public void removetEnd_nonEmptyPath_returnsElement() throws Exception { + String lastElement = "last"; + path = createPath("first", "some", lastElement); + + String end = path.removeEnd(); + + assertSame(lastElement, end); + } + + @Test + @SuppressWarnings("javadoc") + public void removetEnd_nonEmptyPath_elementIsRemoved() throws Exception { + String onlyElement = "only"; + path = createPath(onlyElement); + + path.removeEnd(); + + assertTrue(path.isEmpty()); + } + + // append + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void append_nullNode_throwsNullPointerException() throws Exception { + path.append(null); + } + + @Test + @SuppressWarnings("javadoc") + public void append_toEmptyPath_isEmptyReturnsFalse() throws Exception { + path.append("element"); + + assertFalse(path.isEmpty()); + } + + @Test + @SuppressWarnings("javadoc") + public void append_toEmptyPath_getReturnsElement() throws Exception { + String element = "element"; + path.append(element); + + assertSame(element, path.getEnd().get()); + } + + @Test + @SuppressWarnings("javadoc") + public void append_toEmptyPath_removeReturnsElement() throws Exception { + String element = "element"; + path.append(element); + + assertSame(element, path.removeEnd()); + } + + @Test + @SuppressWarnings("javadoc") + public void append_toNonEmptyPath_getReturnsElement() throws Exception { + path = createPath("first", "some", "last"); + + String addedElement = "added"; + path.append(addedElement); + + assertSame(addedElement, path.getEnd().get()); + } + + @Test + @SuppressWarnings("javadoc") + public void append_toNonEmptyPath_removeReturnsElement() throws Exception { + path = createPath("first", "some", "last"); + + String addedElement = "added"; + path.append(addedElement); + + assertSame(addedElement, path.removeEnd()); + } + + // #begin ABSTRACT FACTORY METHODS + + /** + * @return an empty path + */ + protected abstract TreePath createEmptyPath(); + + /** + * @param elements + * the initial elements of the path ordered from start to end + * @return a path with the specified elements + */ + protected abstract TreePath createPath(String... elements); + + // #end ABSTRACT FACTORY METHODS +} diff --git a/src/test/java/org/codefx/libfx/collection/tree/stream/BackwardsDfsTreeIterationStrategyTest.java b/src/test/java/org/codefx/libfx/collection/tree/stream/BackwardsDfsTreeIterationStrategyTest.java new file mode 100644 index 0000000..8ebdfa0 --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/tree/stream/BackwardsDfsTreeIterationStrategyTest.java @@ -0,0 +1,177 @@ +package org.codefx.libfx.collection.tree.stream; + +import static org.codefx.libfx.collection.tree.stream.TreePathFactory.createFromNodeToDescendant; +import static org.codefx.libfx.collection.tree.stream.TreePathFactory.createWithSingleNode; +import static org.codefx.libfx.collection.tree.stream.TreeTestHelper.NAVIGATOR; +import static org.codefx.libfx.collection.tree.stream.TreeTestHelper.createDeepBinaryTree; +import static org.codefx.libfx.collection.tree.stream.TreeTestHelper.createSimpleBinaryTree; +import static org.codefx.libfx.collection.tree.stream.TreeTestHelper.createSingletonTree; +import static org.codefx.libfx.collection.tree.stream.TreeTestHelper.iterateTreeContent; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; + +import java.util.Arrays; + +import org.codefx.libfx.collection.tree.navigate.TreeNavigator; +import org.codefx.libfx.collection.tree.stream.TreeTestHelper.Node; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests {@link BackwardsDfsTreeIterationStrategy}. + */ +public class BackwardsDfsTreeIterationStrategyTest { + + private TreeNavigator treeNavigator; + + @Before + @SuppressWarnings({ "unchecked", "javadoc" }) + public void setUp() { + treeNavigator = mock(TreeNavigator.class); + } + + // construction + + @Test(expected = NullPointerException.class) + @SuppressWarnings({ "javadoc", "unchecked", "unused" }) + public void create_nullNavigator_throwsNullPointerException() throws Exception { + TreePath> initialPath = mock(TreePath.class); + new BackwardsDfsTreeIterationStrategy<>(null, initialPath); + } + + @Test(expected = NullPointerException.class) + @SuppressWarnings({ "javadoc", "unused" }) + public void create_nullInitialTreePath_throwsNullPointerException() throws Exception { + new BackwardsDfsTreeIterationStrategy<>(treeNavigator, null); + } + + @Test + @SuppressWarnings("javadoc") + public void create_withInitialTreePath_returnPathEndOnFirstCallToGoToNext() throws Exception { + String pathEnd = "end"; + TreePath> treePath = new StackTreePath<>(Arrays.asList( + SimpleTreeNode.root("root"), + SimpleTreeNode.innerNode("inner", 0), + SimpleTreeNode.innerNode(pathEnd, 1) + )); + BackwardsDfsTreeIterationStrategy strategy = + new BackwardsDfsTreeIterationStrategy<>(treeNavigator, treePath); + + String firstNode = strategy.goToNextNode().get(); + + assertSame(pathEnd, firstNode); + } + + // iterate through trees + + /* + * These tests are not very fine grained. They create a tree (from 'TreeTestHelper') and use a navigator to iterate + * over all the nodes. The resulting order of nodes is compared to what is expected. + */ + + @Test + @SuppressWarnings("javadoc") + public void iterateOverSingletonTree_returnsCorrectElements() { + Node singleton = createSingletonTree(); + TreePath> initialPath = createWithSingleNode(singleton); + TreeIterationStrategy strategy = new BackwardsDfsTreeIterationStrategy<>(NAVIGATOR, initialPath); + + String[] treeContent = iterateTreeContent(strategy); + + assertArrayEquals(new String[] { "singleton" }, treeContent); + } + + @Test + @SuppressWarnings("javadoc") + public void iterateOverSimpleBinaryTree_startAtRoot_returnsRootOnly() { + Node singleton = createSimpleBinaryTree(); + TreePath> initialPath = createWithSingleNode(singleton); + TreeIterationStrategy strategy = new BackwardsDfsTreeIterationStrategy<>(NAVIGATOR, initialPath); + + String[] treeContent = iterateTreeContent(strategy); + + assertArrayEquals(new String[] { "root" }, treeContent); + } + + @Test + @SuppressWarnings("javadoc") + public void iterateOverSimpleBinaryTree_startAtLeftChild_returnsChildAndRoot() { + Node root = createSimpleBinaryTree(); + Node leftChild = root.children.get(0); + TreePath> initialPath = createFromNodeToDescendant(NAVIGATOR, root, leftChild); + TreeIterationStrategy strategy = new BackwardsDfsTreeIterationStrategy<>(NAVIGATOR, initialPath); + + String[] treeContent = iterateTreeContent(strategy); + + assertArrayEquals(new String[] { "leftLeaf", "root" }, treeContent); + } + + @Test + @SuppressWarnings("javadoc") + public void iterateOverSimpleBinaryTree_startAtLastNode_returnsWholeTreeBackwards() { + Node root = createSimpleBinaryTree(); + Node rightLeaf = root.children.get(1); + TreePath> initialPath = createFromNodeToDescendant(NAVIGATOR, root, rightLeaf); + TreeIterationStrategy strategy = new BackwardsDfsTreeIterationStrategy<>(NAVIGATOR, initialPath); + + String[] treeContent = iterateTreeContent(strategy); + + assertArrayEquals(new String[] { "rightLeaf", "leftLeaf", "root", }, treeContent); + } + + @Test + @SuppressWarnings("javadoc") + public void iterateOverDeepBinaryTree_returnsCorrectElements() { + Node root = createDeepBinaryTree(); + Node rightmostLeaf = root + .children.get(1) // returns the tree rooted in "9" + .children.get(1) // returns the tree rooted in "13" + .children.get(1); // returns the tree rooted in "15" + TreePath> initialPath = createFromNodeToDescendant(NAVIGATOR, root, rightmostLeaf); + TreeIterationStrategy strategy = new BackwardsDfsTreeIterationStrategy(NAVIGATOR, initialPath); + + String[] treeContent = iterateTreeContent(strategy); + + assertArrayEquals( + new String[] { "15", "14", "13", "12", "11", "10", "9", "8", "7", "6", "5", "4", "3", "2", "1" }, + treeContent); + } + + @Test + @SuppressWarnings("javadoc") + public void iterateOverSubTreeOfDeepBinaryTree_returnsCorrectElements() { + Node root = createDeepBinaryTree(); + Node subtreeRoot = root + .children.get(0) // returns the tree rooted in "2" + .children.get(1); // returns the tree rooted in "6" + Node leftChild = subtreeRoot.children.get(0); // returns the tree rooted in "7" + TreePath> initialPath = createFromNodeToDescendant(NAVIGATOR, subtreeRoot, leftChild); + TreeIterationStrategy strategy = new BackwardsDfsTreeIterationStrategy<>(NAVIGATOR, initialPath); + + String[] treeContent = iterateTreeContent(strategy); + + // the strategy should recurse to the subtree root, but no further + assertArrayEquals( + new String[] { "7", "6" }, + treeContent); + } + + @Test + @SuppressWarnings("javadoc") + public void iterateOverDeepBinaryTree_startFromWithin_returnsCorrectElements() { + Node root = createDeepBinaryTree(); + Node descendant = root + .children.get(1) // returns the tree rooted in "9" + .children.get(1); // returns the tree rooted in "13" + TreePath> initialPath = createFromNodeToDescendant(NAVIGATOR, root, descendant); + TreeIterationStrategy strategy = new BackwardsDfsTreeIterationStrategy<>(NAVIGATOR, initialPath); + + String[] treeContent = iterateTreeContent(strategy); + + assertArrayEquals( + new String[] { "13", "12", "11", "10", "9", "8", "7", "6", "5", "4", "3", "2", "1" }, + treeContent); + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/tree/stream/DfsTreeIterationStrategyTest.java b/src/test/java/org/codefx/libfx/collection/tree/stream/DfsTreeIterationStrategyTest.java new file mode 100644 index 0000000..52e8082 --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/tree/stream/DfsTreeIterationStrategyTest.java @@ -0,0 +1,146 @@ +package org.codefx.libfx.collection.tree.stream; + +import static org.codefx.libfx.collection.tree.stream.TreePathFactory.createFromNodeToDescendant; +import static org.codefx.libfx.collection.tree.stream.TreePathFactory.createWithSingleNode; +import static org.codefx.libfx.collection.tree.stream.TreeTestHelper.NAVIGATOR; +import static org.codefx.libfx.collection.tree.stream.TreeTestHelper.createDeepBinaryTree; +import static org.codefx.libfx.collection.tree.stream.TreeTestHelper.createSimpleBinaryTree; +import static org.codefx.libfx.collection.tree.stream.TreeTestHelper.createSingletonTree; +import static org.codefx.libfx.collection.tree.stream.TreeTestHelper.iterateTreeContent; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; + +import java.util.Arrays; + +import org.codefx.libfx.collection.tree.navigate.TreeNavigator; +import org.codefx.libfx.collection.tree.stream.TreeTestHelper.Node; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests {@link DfsTreeIterationStrategy}. + */ +public class DfsTreeIterationStrategyTest { + + private TreeNavigator treeNavigator; + + @Before + @SuppressWarnings({ "unchecked", "javadoc" }) + public void setUp() { + treeNavigator = mock(TreeNavigator.class); + } + + // construction + + @Test(expected = NullPointerException.class) + @SuppressWarnings({ "javadoc", "unchecked", "unused" }) + public void create_nullNavigator_throwsNullPointerException() throws Exception { + TreePath> initialPath = mock(TreePath.class); + new DfsTreeIterationStrategy<>(null, initialPath); + } + + @Test(expected = NullPointerException.class) + @SuppressWarnings({ "javadoc", "unused" }) + public void create_nullInitialTreePath_throwsNullPointerException() throws Exception { + new DfsTreeIterationStrategy<>(treeNavigator, null); + } + + @Test + @SuppressWarnings("javadoc") + public void create_withInitialTreePath_returnPathEndOnFirstCallToGoToNext() throws Exception { + String pathEnd = "end"; + TreePath> treePath = new StackTreePath<>(Arrays.asList( + SimpleTreeNode.root("root"), + SimpleTreeNode.innerNode("inner", 0), + SimpleTreeNode.innerNode(pathEnd, 1) + )); + DfsTreeIterationStrategy strategy = new DfsTreeIterationStrategy<>(treeNavigator, treePath); + + String firstNode = strategy.goToNextNode().get(); + + assertSame(pathEnd, firstNode); + } + + // iterate through trees + + /* + * These tests are not very fine grained. They create a tree (from 'TreeTestHelper') and use a navigator to iterate + * over all the nodes. The resulting order of nodes is compared to what is expected. + */ + + @Test + @SuppressWarnings("javadoc") + public void iterateOverSingletonTree_returnsCorrectElements() { + Node singleton = createSingletonTree(); + TreePath> initialPath = createWithSingleNode(singleton); + TreeIterationStrategy strategy = new DfsTreeIterationStrategy<>(NAVIGATOR, initialPath); + + String[] treeContent = iterateTreeContent(strategy); + + assertArrayEquals(new String[] { "singleton" }, treeContent); + } + + @Test + @SuppressWarnings("javadoc") + public void iterateOverSimpleBinaryTree_returnsCorrectElements() { + Node singleton = createSimpleBinaryTree(); + TreePath> initialPath = createWithSingleNode(singleton); + TreeIterationStrategy strategy = new DfsTreeIterationStrategy<>(NAVIGATOR, initialPath); + + String[] treeContent = iterateTreeContent(strategy); + + assertArrayEquals(new String[] { "root", "leftLeaf", "rightLeaf" }, treeContent); + } + + @Test + @SuppressWarnings("javadoc") + public void iterateOverDeepBinaryTree_returnsCorrectElements() { + Node root = createDeepBinaryTree(); + TreePath> initialPath = createWithSingleNode(root); + TreeIterationStrategy strategy = new DfsTreeIterationStrategy<>(NAVIGATOR, initialPath); + + String[] treeContent = iterateTreeContent(strategy); + + assertArrayEquals( + new String[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15" }, + treeContent); + } + + @Test + @SuppressWarnings("javadoc") + public void iterateOverSubTreeOfDeepBinaryTree_returnsCorrectElements() { + Node root = createDeepBinaryTree(); + Node subtreeRoot = root + .children.get(0) // returns the tree rooted in "2" + .children.get(1); // returns the tree rooted in "6" + TreePath> initialPath = createWithSingleNode(subtreeRoot); + TreeIterationStrategy strategy = new DfsTreeIterationStrategy<>(NAVIGATOR, initialPath); + + String[] treeContent = iterateTreeContent(strategy); + + // the strategy should recurse to the subtree root, but no further + assertArrayEquals( + new String[] { "6", "7", "8" }, + treeContent); + } + + @Test + @SuppressWarnings("javadoc") + public void iterateOverDeepBinaryTree_startFromWithin_returnsCorrectElements() { + Node root = createDeepBinaryTree(); + Node descendant = root + .children.get(0) // returns the tree rooted in "2" + .children.get(1); // returns the tree rooted in "6" + TreePath> initialPath = createFromNodeToDescendant(NAVIGATOR, root, descendant); + TreeIterationStrategy strategy = new DfsTreeIterationStrategy<>(NAVIGATOR, initialPath); + + String[] treeContent = iterateTreeContent(strategy); + + // the strategy must recurse which are no descendants of "6" + assertArrayEquals( + new String[] { "6", "7", "8", "9", "10", "11", "12", "13", "14", "15" }, + treeContent); + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/tree/stream/SimpleTreeNodeTest.java b/src/test/java/org/codefx/libfx/collection/tree/stream/SimpleTreeNodeTest.java new file mode 100644 index 0000000..ebb5082 --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/tree/stream/SimpleTreeNodeTest.java @@ -0,0 +1,106 @@ +package org.codefx.libfx.collection.tree.stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; + +import java.util.OptionalInt; + +import org.codefx.libfx.collection.tree.stream.SimpleTreeNode; +import org.junit.Test; + +/** + * Tests the class {@link SimpleTreeNode}. + */ +public class SimpleTreeNodeTest { + + // node + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void createNode_elementNull_throwsNullPointerException() throws Exception { + SimpleTreeNode.node(null, OptionalInt.empty()); + } + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void createNode_childIndexNull_throwsNullPointerException() throws Exception { + SimpleTreeNode.node("element", null); + } + + @Test + @SuppressWarnings("javadoc") + public void createNode_elementNonNull_canRetrieveElement() throws Exception { + String element = "element"; + SimpleTreeNode node = SimpleTreeNode.node(element, OptionalInt.empty()); + + assertSame(element, node.getElement()); + } + + @Test + @SuppressWarnings("javadoc") + public void createNode_canRetrieveChildIndex() throws Exception { + int childIndex = 4; + SimpleTreeNode node = SimpleTreeNode.node("element", OptionalInt.of(childIndex)); + + assertEquals(childIndex, node.getChildIndex().getAsInt()); + } + + // root + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void createRoot_elementNull_throwsNullPointerException() throws Exception { + SimpleTreeNode.root(null); + } + + @Test + @SuppressWarnings("javadoc") + public void createRoot_elementNonNull_canRetrieveElement() throws Exception { + String element = "element"; + SimpleTreeNode node = SimpleTreeNode.root(element); + + assertSame(element, node.getElement()); + } + + @Test + @SuppressWarnings("javadoc") + public void createRoot_hasNoChildIndex() throws Exception { + SimpleTreeNode node = SimpleTreeNode.root("element"); + + assertFalse(node.getChildIndex().isPresent()); + } + + // inner node + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void createInnerNode_elementNull_throwsNullPointerException() throws Exception { + SimpleTreeNode.innerNode(null, 0); + } + + @Test(expected = IllegalArgumentException.class) + @SuppressWarnings("javadoc") + public void createInnerNode_childIndexNegative_throwsIllegalArgumentException() throws Exception { + SimpleTreeNode.innerNode("element", -1); + } + + @Test + @SuppressWarnings("javadoc") + public void createInnerNode_elementNonNull_canRetrieveElement() throws Exception { + String element = "element"; + SimpleTreeNode node = SimpleTreeNode.innerNode(element, 0); + + assertSame(element, node.getElement()); + } + + @Test + @SuppressWarnings("javadoc") + public void createInnerNode_canRetrieveChildIndex() throws Exception { + int childIndex = 4; + SimpleTreeNode node = SimpleTreeNode.innerNode("element", childIndex); + + assertEquals(childIndex, node.getChildIndex().getAsInt()); + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/tree/stream/StackTreePathTest.java b/src/test/java/org/codefx/libfx/collection/tree/stream/StackTreePathTest.java new file mode 100644 index 0000000..6ad131f --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/tree/stream/StackTreePathTest.java @@ -0,0 +1,69 @@ +package org.codefx.libfx.collection.tree.stream; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.nitorcreations.junit.runners.NestedRunner; + +/** + * Tests {@link StackTreePath}. + */ +@RunWith(NestedRunner.class) +public class StackTreePathTest { + + /** + * Tests whether the constructors work. + */ + public static class Construction { + + @Test + @SuppressWarnings("javadoc") + public void create_emptyConstructor_throwsNoException() throws Exception { + @SuppressWarnings("unused") + StackTreePath stackTreePath = new StackTreePath<>(); + } + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void create_nullElementList_throwsNullPointerException() throws Exception { + @SuppressWarnings("unused") + StackTreePath stackTreePath = new StackTreePath<>(null); + } + + @Test + @SuppressWarnings("javadoc") + public void create_elementList_containsElementrs() throws Exception { + List elements = Arrays.asList("4", "3", "2", "1", "0"); + StackTreePath stackTreePath = new StackTreePath<>(elements); + + for (int i = 0; i < 5; i++) { + String lastElement = stackTreePath.removeEnd(); + assertEquals("" + i, lastElement); + } + } + + } + + /** + * Tests whether the class fulfills the {@link TreePath} contract. + */ + public static class TreePathContract extends AbstractTreePathTest { + + @Override + protected TreePath createEmptyPath() { + return new StackTreePath<>(); + } + + @Override + protected TreePath createPath(String... elements) { + return new StackTreePath<>(Arrays.asList(elements)); + } + + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/tree/stream/TreeIteratorTest.java b/src/test/java/org/codefx/libfx/collection/tree/stream/TreeIteratorTest.java new file mode 100644 index 0000000..0b67b2c --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/tree/stream/TreeIteratorTest.java @@ -0,0 +1,164 @@ +package org.codefx.libfx.collection.tree.stream; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import java.util.NoSuchElementException; +import java.util.Optional; + +import org.codefx.libfx.collection.tree.stream.TreeIterationStrategy; +import org.codefx.libfx.collection.tree.stream.TreeIterator; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests {@link TreeIterator}. + */ +public class TreeIteratorTest { + + private TreeIterationStrategy strategy; + + private TreeIterator treeIterator; + + @Before + @SuppressWarnings({ "unchecked", "javadoc" }) + public void setUp() { + strategy = mock(TreeIterationStrategy.class); + treeIterator = new TreeIterator<>(strategy); + } + + // create + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void create_nullStrategy_throwsNullPointerExeption() throws Exception { + @SuppressWarnings("unused") + TreeIterator treeIterator = new TreeIterator<>(null); + } + + // has next + + @Test + @SuppressWarnings("javadoc") + public void hasNext_strategyReturnsEmptyOptional_returnsFalse() throws Exception { + when(strategy.goToNextNode()).thenReturn(Optional.empty()); + + boolean hasNext = treeIterator.hasNext(); + + assertFalse(hasNext); + } + + @Test + @SuppressWarnings("javadoc") + public void hasNext_strategyReturnsNonEmptyOptional_returnsTrue() throws Exception { + when(strategy.goToNextNode()).thenReturn(Optional.of("A")); + + boolean hasNext = treeIterator.hasNext(); + + assertTrue(hasNext); + } + + // next + + @Test(expected = NoSuchElementException.class) + @SuppressWarnings("javadoc") + public void next_strategyReturnsEmptyOptional_throwsNoSuchElementException() throws Exception { + when(strategy.goToNextNode()).thenReturn(Optional.empty()); + + treeIterator.next(); + } + + @Test + @SuppressWarnings("javadoc") + public void next_strategyReturnsNonEmptyOptional_returnsThatElement() throws Exception { + String element = "element"; + when(strategy.goToNextNode()).thenReturn(Optional.of(element)); + + String nextElement = treeIterator.next(); + + assertSame(element, nextElement); + } + + // calls to strategy + + @Test + @SuppressWarnings("javadoc") + public void create_noCallsToGoToNext() throws Exception { + verifyZeroInteractions(strategy); + } + + @Test + @SuppressWarnings("javadoc") + public void hasNext_manyCallsToExhaustedStrategy_callsGoToNextOnlyOnce() throws Exception { + // strategy is "exhausted" so it always returns 'empty' + when(strategy.goToNextNode()).thenReturn(Optional.empty()); + + treeIterator.hasNext(); + treeIterator.hasNext(); + treeIterator.hasNext(); + + verify(strategy, times(1)).goToNextNode(); + } + + @Test + @SuppressWarnings("javadoc") + public void hasNext_manyCallsToNonExhaustedStrategy_callsGoToNextOnlyOnce() throws Exception { + // strategy is not "exhausted" so it returns non-empty Optionals + when(strategy.goToNextNode()).thenReturn(Optional.of("A")); + + treeIterator.hasNext(); + treeIterator.hasNext(); + treeIterator.hasNext(); + + verify(strategy, times(1)).goToNextNode(); + } + + @Test + @SuppressWarnings("javadoc") + public void hasNextThenNext_callsGoToNextOnlyOnce() throws Exception { + when(strategy.goToNextNode()).thenReturn(Optional.of("A")); + + treeIterator.hasNext(); + treeIterator.hasNext(); + treeIterator.next(); + + verify(strategy).goToNextNode(); + } + + @Test + @SuppressWarnings("javadoc") + public void next_manyCalls_callsGoToNextExactlyAsOften() throws Exception { + when(strategy.goToNextNode()).thenReturn(Optional.of("A")); + + treeIterator.next(); + treeIterator.next(); + treeIterator.next(); + + verify(strategy, times(3)).goToNextNode(); + } + + @Test + @SuppressWarnings("javadoc") + public void hasNextThenNext_manyCalls_callsGoToNextCorrectly() throws Exception { + when(strategy.goToNextNode()).thenReturn(Optional.of("A")); + + verify(strategy, times(0)).goToNextNode(); + treeIterator.next(); // must move forward so calls 'goToNext' + verify(strategy, times(1)).goToNextNode(); + treeIterator.hasNext(); // must move forward so calls 'goToNext' + verify(strategy, times(2)).goToNextNode(); + treeIterator.hasNext(); // no additional information required so no more calls + treeIterator.hasNext(); + treeIterator.hasNext(); + verify(strategy, times(2)).goToNextNode(); + treeIterator.next(); // no additional information required so no more calls + verify(strategy, times(2)).goToNextNode(); + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/tree/stream/TreePathFactoryTest.java b/src/test/java/org/codefx/libfx/collection/tree/stream/TreePathFactoryTest.java new file mode 100644 index 0000000..501af24 --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/tree/stream/TreePathFactoryTest.java @@ -0,0 +1,191 @@ +package org.codefx.libfx.collection.tree.stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import org.codefx.libfx.collection.tree.navigate.TreeNavigator; +import org.codefx.libfx.collection.tree.stream.TreeNode; +import org.codefx.libfx.collection.tree.stream.TreePath; +import org.codefx.libfx.collection.tree.stream.TreePathFactory; +import org.junit.Test; + +/** + * Tests {@link TreePathFactory}. + */ +public class TreePathFactoryTest { + + // createWithSingletonNode + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void createWithSingletonNode_nodeNull_throwNullPointerException() throws Exception { + TreePathFactory.createWithSingleNode(null); + } + + @Test + @SuppressWarnings("javadoc") + public void createWithSingletonNode_someNode_pathContainsOnlyThatNode() throws Exception { + String node = "Node"; + + TreePath> path = TreePathFactory.createWithSingleNode(node); + + assertSame(node, path.removeEnd().getElement()); + assertTrue(path.isEmpty()); + } + + // createFromElementList + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void createFromElementList_listNavigator_throwNullPointerException() throws Exception { + TreePathFactory.createFromElementList(null, Collections.emptyList()); + } + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void createFromElementList_listNull_throwNullPointerException() throws Exception { + TreePathFactory.createFromElementList(mockTreeNavigator(), null); + } + + @Test + @SuppressWarnings({ "javadoc", "unchecked" }) + public void createFromElementList_singletonList_pathOnlyContainsThatElement() throws Exception { + String element = "node"; + List singletonList = Collections.singletonList(element); + TreeNavigator navigator = mock(TreeNavigator.class); + when(navigator.getChildIndex(element)).thenReturn(OptionalInt.empty()); + + TreePath> path = TreePathFactory.createFromElementList(navigator, singletonList); + + TreeNode end = path.removeEnd(); + assertSame(element, end.getElement()); + assertFalse(end.getChildIndex().isPresent()); + assertTrue(path.isEmpty()); + } + + @Test + @SuppressWarnings("javadoc") + public void createFromElementList_longerList_pathContainsListElementsInCorrectOrder() throws Exception { + String first = "first node"; + String second = "second node"; + String third = "third node"; + List nodes = Arrays.asList(new String[] { first, second, third }); + TreeNavigator navigator = mockTreeNavigatorWithNodesAndIndices(first, 2, second, 5, third, 8); + + TreePath> path = TreePathFactory.createFromElementList(navigator, nodes); + + TreeNode thirdNode = path.removeEnd(); + assertSame(third, thirdNode.getElement()); + assertEquals(8, thirdNode.getChildIndex().getAsInt()); + + TreeNode secondNode = path.removeEnd(); + assertSame(second, secondNode.getElement()); + assertEquals(5, secondNode.getChildIndex().getAsInt()); + + TreeNode firstNode = path.removeEnd(); + assertSame(first, firstNode.getElement()); + assertEquals(2, firstNode.getChildIndex().getAsInt()); + + assertTrue(path.isEmpty()); + } + + // createFromNodeToDescendant + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void createFromNodeToDescendant_nullNavigator_throwNullPointerException() throws Exception { + TreePathFactory.createFromNodeToDescendant(null, "node", "descendant"); + } + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void createFromNodeToDescendant_nullNode_throwNullPointerException() throws Exception { + TreePathFactory.createFromNodeToDescendant(mockTreeNavigator(), null, "descendant"); + } + + @Test(expected = NullPointerException.class) + @SuppressWarnings("javadoc") + public void createFromNodeToDescendant_nullDescendant_throwNullPointerException() throws Exception { + TreePathFactory.createFromNodeToDescendant(mockTreeNavigator(), "node", null); + } + + @Test + @SuppressWarnings({ "javadoc", "unchecked" }) + public void createFromNodeToDescendant_nodeAndDescendantAreSame_pathOnlyContainsThatNode() throws Exception { + String node = "node"; + String descendant = node; + TreeNavigator navigator = mock(TreeNavigator.class); + when(navigator.getChildIndex(node)).thenReturn(OptionalInt.empty()); + + TreePath> path = TreePathFactory.createFromNodeToDescendant(navigator, node, descendant); + + TreeNode end = path.removeEnd(); + assertSame(node, end.getElement()); + assertFalse(end.getChildIndex().isPresent()); + assertTrue(path.isEmpty()); + } + + @Test + @SuppressWarnings("javadoc") + public void createFromNodeToDescendant_longerPath_pathContainsNodesInCorrectOrder() throws Exception { + String first = "first node"; + String second = "second node"; + String third = "third node"; + TreeNavigator navigator = mockTreeNavigatorWithNodesAndIndices(first, 2, second, 5, third, 8); + + TreePath> path = TreePathFactory.createFromNodeToDescendant(navigator, first, third); + + TreeNode thirdNode = path.removeEnd(); + assertSame(third, thirdNode.getElement()); + assertEquals(8, thirdNode.getChildIndex().getAsInt()); + + TreeNode secondNode = path.removeEnd(); + assertSame(second, secondNode.getElement()); + assertEquals(5, secondNode.getChildIndex().getAsInt()); + + TreeNode firstNode = path.removeEnd(); + assertSame(first, firstNode.getElement()); + assertEquals(2, firstNode.getChildIndex().getAsInt()); + + assertTrue(path.isEmpty()); + } + + // HELPER + + @SuppressWarnings("unchecked") + private static TreeNavigator mockTreeNavigator() { + return mock(TreeNavigator.class); + } + + @SuppressWarnings("unchecked") + private static TreeNavigator mockTreeNavigatorWithNodesAndIndices( + String firstNode, int firstNodeIndex, + String secondNode, int secondNodeIndex, + String thirdNode, int thirdNodeIndex) { + + TreeNavigator navigator = mock(TreeNavigator.class); + + // return child indices + when(navigator.getChildIndex(firstNode)).thenReturn(OptionalInt.of(firstNodeIndex)); + when(navigator.getChildIndex(secondNode)).thenReturn(OptionalInt.of(secondNodeIndex)); + when(navigator.getChildIndex(thirdNode)).thenReturn(OptionalInt.of(thirdNodeIndex)); + + // return parents + when(navigator.getParent(thirdNode)).thenReturn(Optional.of(secondNode)); + when(navigator.getParent(secondNode)).thenReturn(Optional.of(firstNode)); + when(navigator.getParent(firstNode)).thenReturn(Optional.empty()); + + return navigator; + } + +} diff --git a/src/test/java/org/codefx/libfx/collection/tree/stream/TreeTestHelper.java b/src/test/java/org/codefx/libfx/collection/tree/stream/TreeTestHelper.java new file mode 100644 index 0000000..5f36f55 --- /dev/null +++ b/src/test/java/org/codefx/libfx/collection/tree/stream/TreeTestHelper.java @@ -0,0 +1,209 @@ +package org.codefx.libfx.collection.tree.stream; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import org.codefx.libfx.collection.tree.navigate.TreeNavigator; +import org.codefx.libfx.collection.tree.stream.TreeIterationStrategy; + +/** + * Supports testing by providing sample trees and a {@link Navigator} for them. + */ +class TreeTestHelper { + + /** + * The singleton instance of a stateless navigator which can be used to iterate over the test trees. + */ + public static final Navigator NAVIGATOR = new Navigator(); + + // #begin TREES + + /** + * @return a tree with the single node [singleton]. + */ + public static Node createSingletonTree() { + Node tree = Node.singleton("singleton"); + return tree; + } + + /** + * @return a small binary tree [root] -> { [leftLeaf], [rightLeaf] }. + */ + public static Node createSimpleBinaryTree() { + Node tree = Node.root("root", + Node.leaf("leftLeaf"), + Node.leaf("rightLeaf") + ); + return tree; + } + + /** + * @return a perfect binary tree with depth 4 (see types of trees); a septh-first + * search yields [1], [2], ..., [15] + */ + public static Node createDeepBinaryTree() { + Node tree = Node.root("1", + Node.node("2", + Node.node("3", + Node.leaf("4"), + Node.leaf("5") + ), + Node.node("6", + Node.leaf("7"), + Node.leaf("8") + ) + ), + Node.node("9", + Node.node("10", + Node.leaf("11"), + Node.leaf("12") + ), + Node.node("13", + Node.leaf("14"), + Node.leaf("15") + ) + ) + ); + return tree; + } + + // #end TREES + + // #begin UTIL + + /** + * @param strategy + * the strategy to iterate the nodes + * @return an array which contains each node the strategy returns + */ + public static String[] iterateTreeContent(TreeIterationStrategy strategy) { + List treeContent = new ArrayList<>(); + + Optional nextNode = strategy.goToNextNode(); + while (nextNode.isPresent()) { + treeContent.add(nextNode.get().content); + nextNode = strategy.goToNextNode(); + } + + return treeContent.toArray(new String[0]); + } + + // #end UTIL + + // #begin INNER CLASSES + + /** + * A {@link TreeNavigator} for the {@link Node}-based trees available in this class. + */ + public static class Navigator implements TreeNavigator { + + @Override + public Optional getParent(Node child) { + return child.parent; + } + + @Override + public OptionalInt getChildIndex(Node node) { + Optional parent = node.parent; + if (parent.isPresent()) + return OptionalInt.of(parent.get().children.indexOf(node)); + else + return OptionalInt.empty(); + } + + @Override + public int getChildrenCount(Node parent) { + return parent.children.size(); + } + + @Override + public Optional getChild(Node parent, int childIndex) { + return (0 <= childIndex && childIndex < parent.children.size()) + ? Optional.of(parent.children.get(childIndex)) + : Optional.empty(); + } + + } + + /** + * A node in the trees returned by this class. + */ + public static class Node { + + /** + * The nodes actual content. + */ + public final String content; + + /** + * The node's children. + */ + public final List children; + + /** + * The node's parent. + */ + public Optional parent; + + private Node(String content, Node[] children) { + this.content = content; + this.parent = Optional.empty(); + this.children = new ArrayList<>(Arrays.asList(children)); + + Arrays.stream(children).forEach(node -> node.parent = Optional.of(this)); + } + + /** + * @param content + * the node's content + * @return a singleton tree + */ + public static Node singleton(String content) { + return new Node(content, new Node[0]); + } + + /** + * @param content + * the root node's content + * @param children + * the child nodes + * @return a tree with the specified child nodes + */ + public static Node root(String content, Node... children) { + return new Node(content, children); + } + + /** + * @param content + * the node's content + * @param children + * the child nodes + * @return a node with the specified child nodes + */ + public static Node node(String content, Node... children) { + return new Node(content, children); + } + + /** + * @param content + * the leaf's content + * @return a leaf node + */ + public static Node leaf(String content) { + return new Node(content, new Node[0]); + } + + @Override + public String toString() { + return "Node [" + content + "]"; + } + + } + + // #end INNER CLASSES + +} diff --git a/src/test/java/org/codefx/libfx/concurrent/when/ExecuteAlwaysWhenTest.java b/src/test/java/org/codefx/libfx/concurrent/when/ExecuteAlwaysWhenTest.java index b27b00d..5227e27 100644 --- a/src/test/java/org/codefx/libfx/concurrent/when/ExecuteAlwaysWhenTest.java +++ b/src/test/java/org/codefx/libfx/concurrent/when/ExecuteAlwaysWhenTest.java @@ -18,7 +18,7 @@ */ public class ExecuteAlwaysWhenTest { - // #region FIELDS & INITIALIZATION + // #begin FIELDS & INITIALIZATION /** * The string which passes the {@link #ACTION_GATEWAY}. @@ -62,7 +62,7 @@ public void setUp() { // #end FIELDS & INITIALIZATION - // #region SINGLE-THREADED TESTS + // #begin SINGLE-THREADED TESTS /** * Tests whether an {@link IllegalStateException} is thrown when {@link ExecuteAlwaysWhen#executeWhen() @@ -203,7 +203,7 @@ public void testCancelAfterCorrectValueWasObserved() { // #end SINGLE-THREADED TESTS - // #region MULTI-THREADED TESTS + // #begin MULTI-THREADED TESTS /* * Unfortunately I could not come up with multi-threaded tests... :( The problem is that the only interesting part diff --git a/src/test/java/org/codefx/libfx/concurrent/when/ExecuteOnceWhenTest.java b/src/test/java/org/codefx/libfx/concurrent/when/ExecuteOnceWhenTest.java index c1ae9a4..b892076 100644 --- a/src/test/java/org/codefx/libfx/concurrent/when/ExecuteOnceWhenTest.java +++ b/src/test/java/org/codefx/libfx/concurrent/when/ExecuteOnceWhenTest.java @@ -22,7 +22,7 @@ */ public class ExecuteOnceWhenTest { - // #region FIELDS & INITIALIZATION + // #begin FIELDS & INITIALIZATION /** * The string which passes the {@link #ACTION_CONDITION}. @@ -66,7 +66,7 @@ public void setUp() { // #end FIELDS & INITIALIZATION - // #region SINGLE-THREADED TESTS + // #begin SINGLE-THREADED TESTS /** * Tests whether an {@link IllegalStateException} is thrown when {@link ExecuteOnceWhen#executeWhen() executeWhen()} @@ -169,7 +169,7 @@ public void testCancel() { // #end SINGLE-THREADED TESTS - // #region MULTI-THREADED TESTS + // #begin MULTI-THREADED TESTS /** * Creates a number of threads which repeatedly change the {@link #observable}'s value and a number of threads which diff --git a/src/test/java/org/codefx/libfx/control/properties/AbstractControlPropertyListenerHandleTest.java b/src/test/java/org/codefx/libfx/control/properties/AbstractControlPropertyListenerHandleTest.java index 3efc09b..eb859fa 100644 --- a/src/test/java/org/codefx/libfx/control/properties/AbstractControlPropertyListenerHandleTest.java +++ b/src/test/java/org/codefx/libfx/control/properties/AbstractControlPropertyListenerHandleTest.java @@ -24,7 +24,7 @@ */ public abstract class AbstractControlPropertyListenerHandleTest { - // #region FIELDS + // #begin FIELDS /** * A key to which the created listeners listen. @@ -53,7 +53,7 @@ public abstract class AbstractControlPropertyListenerHandleTest { // #end FIELDS - // #region SETUP + // #begin SETUP /** * Initializes fields for tests. @@ -115,7 +115,7 @@ private ControlPropertyListenerHandle createAttachedDefaultListener(Consumer on(properties) } - // #region TESTS CREATED LISTENERS + // #begin TESTS CREATED LISTENERS /** * Tests the created {@link CastingControlPropertyListenerHandle}. diff --git a/src/test/java/org/codefx/libfx/dom/AbstractDomEventConverterTest.java b/src/test/java/org/codefx/libfx/dom/AbstractDomEventConverterTest.java index 4d74f22..eddfa6b 100644 --- a/src/test/java/org/codefx/libfx/dom/AbstractDomEventConverterTest.java +++ b/src/test/java/org/codefx/libfx/dom/AbstractDomEventConverterTest.java @@ -28,7 +28,7 @@ */ public abstract class AbstractDomEventConverterTest { - // #region FIELDS & INITIALIZATION + // #begin FIELDS & INITIALIZATION /** * The URL used for all links. @@ -76,11 +76,11 @@ public void setUp() throws Exception { // #end FIELDS & INITIALIZATION - // #region TESTS + // #begin TESTS // #end FIELDS & INITIALIZATION - // #region TESTS + // #begin TESTS /** * Tests whether all DOM events[1] which have a corresponding {@link EventType HyperlinkEventType} are correctly @@ -201,7 +201,7 @@ public void testSourceElement() { // #end TESTS - // #region ABSTRACT METHODS + // #begin ABSTRACT METHODS /** * Implemented by subclasses to check whether the specified event can be converted. @@ -225,7 +225,7 @@ public void testSourceElement() { // #end ABSTRACT METHODS - // #region HELPER METHODS + // #begin HELPER METHODS /** * Creates a DOM event and dispatches it with the specified target. A listener on the same target catches any event diff --git a/src/test/java/org/codefx/libfx/dom/DomEventConverterTest.java b/src/test/java/org/codefx/libfx/dom/DomEventConverterTest.java index 722a7b4..53e5671 100644 --- a/src/test/java/org/codefx/libfx/dom/DomEventConverterTest.java +++ b/src/test/java/org/codefx/libfx/dom/DomEventConverterTest.java @@ -9,7 +9,7 @@ */ public class DomEventConverterTest extends AbstractDomEventConverterTest { - // #region FIELDS & INITIALIZATION + // #begin FIELDS & INITIALIZATION /** * The tested {@link DomEventConverter}. diff --git a/src/test/java/org/codefx/libfx/listener/handle/GenericListenerHandleTest.java b/src/test/java/org/codefx/libfx/listener/handle/GenericListenerHandleTest.java index 3060db5..0d34521 100644 --- a/src/test/java/org/codefx/libfx/listener/handle/GenericListenerHandleTest.java +++ b/src/test/java/org/codefx/libfx/listener/handle/GenericListenerHandleTest.java @@ -18,7 +18,7 @@ */ public class GenericListenerHandleTest { - // #region INSTANCES + // #begin INSTANCES /** * The tested handle. @@ -47,7 +47,7 @@ public class GenericListenerHandleTest { // #end INSTANCES - // #region SETUP + // #begin SETUP /** * Creates the tested instances. @@ -65,7 +65,7 @@ public void setUp() { // #end SETUP - // #region TESTS + // #begin TESTS /** * Tests whether the construction of the handle does not cause any calls to {@link #add} and {@link #remove}. diff --git a/src/test/java/org/codefx/libfx/listener/handle/ListenerHandleBuilderTest.java b/src/test/java/org/codefx/libfx/listener/handle/ListenerHandleBuilderTest.java index 2234c1e..4ca7e8b 100644 --- a/src/test/java/org/codefx/libfx/listener/handle/ListenerHandleBuilderTest.java +++ b/src/test/java/org/codefx/libfx/listener/handle/ListenerHandleBuilderTest.java @@ -25,7 +25,7 @@ public class ListenerHandleBuilderTest { */ private static final BiConsumer NOT_NULL_CONSUMER = (o, l) -> { /* do nothing */}; - // #region TESTS + // #begin TESTS // construction diff --git a/src/test/java/org/codefx/libfx/nesting/AbstractDeepNestingTest.java b/src/test/java/org/codefx/libfx/nesting/AbstractDeepNestingTest.java index a1fecf9..91b39b2 100644 --- a/src/test/java/org/codefx/libfx/nesting/AbstractDeepNestingTest.java +++ b/src/test/java/org/codefx/libfx/nesting/AbstractDeepNestingTest.java @@ -22,7 +22,7 @@ public abstract class AbstractDeepNestingTest extends AbstractNestingTest { - // #region TESTS + // #begin TESTS // construction @@ -121,7 +121,7 @@ public void testWhenSettingOuterValueToNull() { //#end TESTS - // #region ABSTRACT METHODS + // #begin ABSTRACT METHODS /** * Creates a new outer observable with a null value. The returned instances must be new for each call. @@ -153,7 +153,7 @@ public void testWhenSettingOuterValueToNull() { //#end ABSTRACT METHODS - // #region INNER CLASSES + // #begin INNER CLASSES /** * Indicates on which level of the nesting hierarchy a new value will be set by diff --git a/src/test/java/org/codefx/libfx/nesting/AbstractNestingTest.java b/src/test/java/org/codefx/libfx/nesting/AbstractNestingTest.java index 764f52b..5693f0b 100644 --- a/src/test/java/org/codefx/libfx/nesting/AbstractNestingTest.java +++ b/src/test/java/org/codefx/libfx/nesting/AbstractNestingTest.java @@ -19,7 +19,7 @@ */ public abstract class AbstractNestingTest { - // #region INSTANCES USED FOR TESTING + // #begin INSTANCES USED FOR TESTING /** * The outer observable of the nesting hierarchy contained in {@link #nesting}. @@ -42,7 +42,7 @@ public void setUp() { nesting = createNewNestingFromOuterObservable(outerObservable); } - // #region TESTS + // #begin TESTS // construction @@ -65,7 +65,7 @@ public void testCorrectAfterConstruction() { //#end TESTS - // #region ABSTRACT METHODS + // #begin ABSTRACT METHODS /** * Creates a new nesting hierarchy and returns its outer observable. All returned instances must be new for each diff --git a/src/test/java/org/codefx/libfx/nesting/DeepNestingTest.java b/src/test/java/org/codefx/libfx/nesting/DeepNestingTest.java index 3ba7d83..06a0157 100644 --- a/src/test/java/org/codefx/libfx/nesting/DeepNestingTest.java +++ b/src/test/java/org/codefx/libfx/nesting/DeepNestingTest.java @@ -8,26 +8,18 @@ import javafx.beans.property.Property; import javafx.beans.value.ObservableValue; -import org.codefx.libfx.nesting.DeepNesting; -import org.codefx.libfx.nesting.Nesting; -import org.codefx.libfx.nesting.NestingStep; import org.codefx.libfx.nesting.testhelper.InnerValue; import org.codefx.libfx.nesting.testhelper.NestingAccess; import org.codefx.libfx.nesting.testhelper.OuterValue; import org.codefx.libfx.nesting.testhelper.SomeValue; import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; + +import com.nitorcreations.junit.runners.NestedRunner; /** * Tests the {@link Nesting} implementation {@link DeepNesting}. */ -@RunWith(Suite.class) -@SuiteClasses({ - DeepNestingTest.WithObservable.class, - DeepNestingTest.WithProperty.class, - DeepNestingTest.WithIntegerProperty.class, -}) +@RunWith(NestedRunner.class) public class DeepNestingTest { /** diff --git a/src/test/java/org/codefx/libfx/nesting/NestingObserverTest.java b/src/test/java/org/codefx/libfx/nesting/NestingObserverTest.java index 6c87a8c..e499e06 100644 --- a/src/test/java/org/codefx/libfx/nesting/NestingObserverTest.java +++ b/src/test/java/org/codefx/libfx/nesting/NestingObserverTest.java @@ -20,7 +20,7 @@ */ public class NestingObserverTest { - // #region INSTANCES USED FOR TESTING + // #begin INSTANCES USED FOR TESTING /** * The nesting's initial {@link Nesting#innerObservableProperty() innerObservable}. @@ -39,7 +39,7 @@ public class NestingObserverTest { //#end INSTANCES USED FOR TESTING - // #region INITIALIZATION + // #begin INITIALIZATION /** * Creates instances of {@link #initialInnerObservable}, {@link #nesting}, {@link #verifier} and a @@ -75,7 +75,7 @@ public void setUpObservation(INNER_OBSERVABLE_INITIALLY_PRESENT initiallyPresent //#end INITIALIZATION - // #region TESTS + // #begin TESTS /** * Tests whether the observer's construction leads to a correct initialization of the methods if the nesting's @@ -185,7 +185,7 @@ public void testReplacingPresentWithPresentInnerObservable() { //#end TESTS - // #region INNER CLASSES + // #begin INNER CLASSES /** * The {@link NestingObserver NestingObservers} created in this test call this class' methods when the nesting diff --git a/src/test/java/org/codefx/libfx/nesting/ShallowNestingTest.java b/src/test/java/org/codefx/libfx/nesting/ShallowNestingTest.java index 3efc9c0..6730703 100644 --- a/src/test/java/org/codefx/libfx/nesting/ShallowNestingTest.java +++ b/src/test/java/org/codefx/libfx/nesting/ShallowNestingTest.java @@ -10,18 +10,13 @@ import org.codefx.libfx.nesting.testhelper.SomeValue; import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; + +import com.nitorcreations.junit.runners.NestedRunner; /** * Tests the {@link Nesting} implementation {@link ShallowNesting}. */ -@RunWith(Suite.class) -@SuiteClasses({ - ShallowNestingTest.OnObservable.class, - ShallowNestingTest.OnObservableValue.class, - ShallowNestingTest.OnProperty.class, -}) +@RunWith(NestedRunner.class) public class ShallowNestingTest { /** diff --git a/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedChangeListenerBuilderTest.java b/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedChangeListenerBuilderTest.java index 6d550c0..f6a9056 100644 --- a/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedChangeListenerBuilderTest.java +++ b/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedChangeListenerBuilderTest.java @@ -12,7 +12,7 @@ */ public abstract class AbstractNestedChangeListenerBuilderTest { - // #region TESTED INSTANCES + // #begin TESTED INSTANCES /** * The tested builder. @@ -21,7 +21,7 @@ public abstract class AbstractNestedChangeListenerBuilderTest { //#end TESTED INSTANCES - // #region SETUP + // #begin SETUP /** * Creates a new builder before each test. @@ -43,7 +43,7 @@ public void setUp() { // #end SETUP - // #region TESTS + // #begin TESTS /** * Tests whether the builder can be created with a null {@link Nesting}. diff --git a/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedChangeListenerHandleTest.java b/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedChangeListenerHandleTest.java index 296420b..038b98d 100644 --- a/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedChangeListenerHandleTest.java +++ b/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedChangeListenerHandleTest.java @@ -27,7 +27,7 @@ */ public abstract class AbstractNestedChangeListenerHandleTest { - // #region INSTANCES USED FOR TESTING + // #begin INSTANCES USED FOR TESTING /** * The nesting's inner observable. @@ -56,7 +56,7 @@ public abstract class AbstractNestedChangeListenerHandleTest { //#end INSTANCES USED FOR TESTING - // #region SETUP + // #begin SETUP /** * Creates a new instance of {@link #nesting} and {@link #listener}. @@ -130,7 +130,7 @@ protected abstract NestedChangeListenerHandle createNestedListenerHandle( //end SETUP - // #region TESTS + // #begin TESTS // construction diff --git a/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedInvalidationListenerBuilderTest.java b/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedInvalidationListenerBuilderTest.java index 63a3bbd..b2d2c5f 100644 --- a/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedInvalidationListenerBuilderTest.java +++ b/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedInvalidationListenerBuilderTest.java @@ -11,7 +11,7 @@ */ public abstract class AbstractNestedInvalidationListenerBuilderTest { - // #region TESTED INSTANCES + // #begin TESTED INSTANCES /** * The tested builder. @@ -20,7 +20,7 @@ public abstract class AbstractNestedInvalidationListenerBuilderTest { //#end TESTED INSTANCES - // #region SETUP + // #begin SETUP /** * Creates a new builder before each test. @@ -39,7 +39,7 @@ public void setUp() { // #end SETUP - // #region TESTS + // #begin TESTS /** * Tests whether the builder can be created with a null {@link Nesting}. diff --git a/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedInvalidationListenerHandleTest.java b/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedInvalidationListenerHandleTest.java index 89060eb..186feb2 100644 --- a/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedInvalidationListenerHandleTest.java +++ b/src/test/java/org/codefx/libfx/nesting/listener/AbstractNestedInvalidationListenerHandleTest.java @@ -28,7 +28,7 @@ */ public abstract class AbstractNestedInvalidationListenerHandleTest { - // #region INSTANCES USED FOR TESTING + // #begin INSTANCES USED FOR TESTING /** * The nesting's inner observable. @@ -57,7 +57,7 @@ public abstract class AbstractNestedInvalidationListenerHandleTest { //#end INSTANCES USED FOR TESTING - // #region SETUP + // #begin SETUP /** * Creates a new instance of {@link #nesting} and {@link #listener}. @@ -121,7 +121,7 @@ protected abstract NestedInvalidationListenerHandle createNestedListenerHandle( //end SETUP - // #region TESTS + // #begin TESTS // construction diff --git a/src/test/java/org/codefx/libfx/nesting/listener/NestedChangeListenerBuilderTest.java b/src/test/java/org/codefx/libfx/nesting/listener/NestedChangeListenerBuilderTest.java index ced9098..fe089e0 100644 --- a/src/test/java/org/codefx/libfx/nesting/listener/NestedChangeListenerBuilderTest.java +++ b/src/test/java/org/codefx/libfx/nesting/listener/NestedChangeListenerBuilderTest.java @@ -9,17 +9,13 @@ import org.codefx.libfx.nesting.Nesting; import org.codefx.libfx.nesting.testhelper.NestingAccess.EditableNesting; import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; + +import com.nitorcreations.junit.runners.NestedRunner; /** * Tests the class {@link NestedChangeListenerBuilder}. */ -@RunWith(Suite.class) -@SuiteClasses({ - NestedChangeListenerBuilderTest.Builder.class, - NestedChangeListenerBuilderTest.CreatedListenerHandles.class, -}) +@RunWith(NestedRunner.class) public class NestedChangeListenerBuilderTest { /** diff --git a/src/test/java/org/codefx/libfx/nesting/listener/NestedInvalidationListenerBuilderTest.java b/src/test/java/org/codefx/libfx/nesting/listener/NestedInvalidationListenerBuilderTest.java index 653ff9a..4426f2d 100644 --- a/src/test/java/org/codefx/libfx/nesting/listener/NestedInvalidationListenerBuilderTest.java +++ b/src/test/java/org/codefx/libfx/nesting/listener/NestedInvalidationListenerBuilderTest.java @@ -9,17 +9,13 @@ import org.codefx.libfx.nesting.Nesting; import org.codefx.libfx.nesting.testhelper.NestingAccess.EditableNesting; import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; + +import com.nitorcreations.junit.runners.NestedRunner; /** * Tests the class {@link NestedInvalidationListenerBuilder}. */ -@RunWith(Suite.class) -@SuiteClasses({ - NestedInvalidationListenerBuilderTest.Builder.class, - NestedInvalidationListenerBuilderTest.CreatedListeners.class, -}) +@RunWith(NestedRunner.class) public class NestedInvalidationListenerBuilderTest { /** diff --git a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedBooleanPropertyTest.java b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedBooleanPropertyTest.java index 7f8ebaf..5c7500e 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedBooleanPropertyTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedBooleanPropertyTest.java @@ -1,33 +1,32 @@ package org.codefx.libfx.nesting.property; -import static org.codefx.libfx.nesting.testhelper.NestingAccess.getNestingObservable; -import static org.codefx.libfx.nesting.testhelper.NestingAccess.getNestingValue; import static org.codefx.libfx.nesting.testhelper.NestingAccess.setNestingObservable; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertTrue; +import static org.codefx.tarkastus.AssertFX.assertSameOrEqual; +import static org.junit.Assert.assertNotEquals; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import org.codefx.libfx.nesting.Nesting; +import org.codefx.libfx.nesting.property.InnerObservableMissingBehavior.WhenInnerObservableMissingOnUpdate; import org.junit.Test; /** * Abstract superclass to tests for {@link NestedBooleanProperty NestedBooleanProperty} which only leaves the creation - * of the tested properties (by {@link #createNestedPropertyFromNesting(Nesting)}) to the subclasses. + * of the tested properties (by {@link #createNestedPropertyFromNesting(Nesting, InnerObservableMissingBehavior)}) to + * the subclasses. */ -public abstract class AbstractNestedBooleanPropertyTest extends AbstractNestedPropertyTest { +public abstract class AbstractNestedBooleanPropertyTest extends + AbstractNestedPropertyTest { /* * Since Boolean has only two values, 'createNewValue' can not fulfill its contract. Instead it always returns - * 'true' whereas 'createNewObservableWithSomeValue' uses false. All tests where this might come into play are - * overridden below (for better readability or just to make them work). + * 'true' whereas 'createNewObservableWithSomeValue' uses false. All tests where this leads to a failing test are + * overridden below. */ @Override - protected boolean allowsNullValues() { - return false; + protected boolean wrapsPrimitive() { + return true; } @Override @@ -45,56 +44,33 @@ protected BooleanProperty createNewObservableWithSomeValue() { return createNewObservableWithValue(false); } - // #region OVERRIDDEN TEST METHODS + // #begin OVERRIDDEN TEST METHODS - @Override - @Test - public void testChangingNewObservablesValue() { - // set a new observable whose value is 'false'... - BooleanProperty newObservable = createNewObservableWithValue(false); - setNestingObservable(getNesting(), newObservable); - // (assert that setting the observable worked) - assertEquals(newObservable, getNestingObservable(getNesting())); - - // ... and change its value to 'true' - newObservable.setValue(true); - - // assert that nesting and property hold the new value - assertTrue(getNestingValue(getNesting())); - assertTrue(getPropertyValue()); - } + // #begin TESTS @Override @Test - public void testChangingOldObservablesValue() { - // store the old observable which has the value 'false' (see 'createNewObservableWithSomeValue') ... - BooleanProperty oldObservable = getNestingObservable(getNesting()); - - // ... set a new observable with value 'false' ... - BooleanProperty newObservableWithFalse = createNewObservableWithValue(false); - setNestingObservable(getNesting(), newObservableWithFalse); - // (assert that setting the observable worked) - assertNotSame(oldObservable, getNestingObservable(getNesting())); - - // ... and change the old observable's value - oldObservable.setValue(true); - - // assert that nesting and property hold the new observable's value (i.e. 'false') instead of the old observable's new value (i.e. 'true') - assertFalse(getNestingValue(getNesting())); - assertFalse(getPropertyValue()); - } - - @Override - @Test - public void testChangedValueNotPropagatedAfterObservableWasMissing() { - // set the nesting observable and change the nested property's value to 'true' + public void newInnerObservableAfterSetValueOnMissingInnerObservable_acceptUntilNext_newInnerObservableKeepsValue() { + boolean valueWhileMissing = true; + boolean valueOfNewInnerObservable = false; + + MissingBehavior missingBehavior = MissingBehavior + . defaults() + .onUpdate(WhenInnerObservableMissingOnUpdate.ACCEPT_VALUE_UNTIL_NEXT_INNER_OBSERVABLE); + NestedProperty property = createNestedPropertyFromNesting(getNesting(), missingBehavior); setNestingObservable(getNesting(), null); - getProperty().setValue(true); + BooleanProperty newObservable = createNewObservableWithValue(valueOfNewInnerObservable); + + // change the nested property's value (which can not be written to the nesting's observable as none is present); + property.setValue(valueWhileMissing); + // the values of the nested property and the new observable are not equal + assertNotEquals(newObservable.getValue(), property.getValue()); - // set the new observable and assert that the property reflects its value, i.e. holds 'false' - BooleanProperty newObservable = createNewObservableWithValue(false); + // set the new observable and assert that it kept its value and the nested property was updated setNestingObservable(getNesting(), newObservable); - assertFalse(getPropertyValue()); + + assertSameOrEqual(valueOfNewInnerObservable, newObservable.getValue(), wrapsPrimitive()); + assertSameOrEqual(valueOfNewInnerObservable, property.getValue(), wrapsPrimitive()); } //#end OVERRIDDEN TEST METHODS diff --git a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedDoublePropertyTest.java b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedDoublePropertyTest.java index 325a576..ef93545 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedDoublePropertyTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedDoublePropertyTest.java @@ -7,9 +7,11 @@ /** * Abstract superclass to tests for {@link NestedDoubleProperty NestedDoubleProperty} which only leaves the creation of - * the tested properties (by {@link #createNestedPropertyFromNesting(Nesting)}) to the subclasses. + * the tested properties (by {@link #createNestedPropertyFromNesting(Nesting, InnerObservableMissingBehavior)}) to the + * subclasses. */ -public abstract class AbstractNestedDoublePropertyTest extends AbstractNestedPropertyTest { +public abstract class AbstractNestedDoublePropertyTest extends + AbstractNestedPropertyTest { /** * The last value returned by {@link #createNewValue()}. @@ -17,12 +19,12 @@ public abstract class AbstractNestedDoublePropertyTest extends AbstractNestedPro private double lastValue = 1.5; @Override - protected boolean allowsNullValues() { - return false; + protected boolean wrapsPrimitive() { + return true; } @Override - protected Number createNewValue() { + protected Double createNewValue() { lastValue += 1; return lastValue; } diff --git a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedFloatPropertyTest.java b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedFloatPropertyTest.java index 94458ba..64780e7 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedFloatPropertyTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedFloatPropertyTest.java @@ -7,9 +7,11 @@ /** * Abstract superclass to tests for {@link NestedFloatProperty NestedFloatProperty} which only leaves the creation of - * the tested properties (by {@link #createNestedPropertyFromNesting(Nesting)}) to the subclasses. + * the tested properties (by {@link #createNestedPropertyFromNesting(Nesting, InnerObservableMissingBehavior)}) to the + * subclasses. */ -public abstract class AbstractNestedFloatPropertyTest extends AbstractNestedPropertyTest { +public abstract class AbstractNestedFloatPropertyTest extends + AbstractNestedPropertyTest { /** * The last value returned by {@link #createNewValue()}. @@ -17,12 +19,12 @@ public abstract class AbstractNestedFloatPropertyTest extends AbstractNestedProp private float lastValue = 1.5f; @Override - protected boolean allowsNullValues() { - return false; + protected boolean wrapsPrimitive() { + return true; } @Override - protected Number createNewValue() { + protected Float createNewValue() { lastValue += 1; return lastValue; } diff --git a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedIntegerPropertyTest.java b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedIntegerPropertyTest.java index 8fa9166..d018234 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedIntegerPropertyTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedIntegerPropertyTest.java @@ -7,9 +7,11 @@ /** * Abstract superclass to tests for {@link NestedIntegerProperty NestedIntegerProperty} which only leaves the creation - * of the tested properties (by {@link #createNestedPropertyFromNesting(Nesting)}) to the subclasses. + * of the tested properties (by {@link #createNestedPropertyFromNesting(Nesting, InnerObservableMissingBehavior)}) to + * the subclasses. */ -public abstract class AbstractNestedIntegerPropertyTest extends AbstractNestedPropertyTest { +public abstract class AbstractNestedIntegerPropertyTest extends + AbstractNestedPropertyTest { /** * The last value returned by {@link #createNewValue()}. @@ -17,12 +19,12 @@ public abstract class AbstractNestedIntegerPropertyTest extends AbstractNestedPr private int lastValue = 0; @Override - protected boolean allowsNullValues() { - return false; + protected boolean wrapsPrimitive() { + return true; } @Override - protected Number createNewValue() { + protected Integer createNewValue() { return lastValue++; } diff --git a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedLongPropertyTest.java b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedLongPropertyTest.java index 1a10f49..3ffe790 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedLongPropertyTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedLongPropertyTest.java @@ -7,9 +7,11 @@ /** * Abstract superclass to tests for {@link NestedLongProperty NestedLongProperty} which only leaves the creation of the - * tested properties (by {@link #createNestedPropertyFromNesting(Nesting)}) to the subclasses. + * tested properties (by {@link #createNestedPropertyFromNesting(Nesting, InnerObservableMissingBehavior)}) to the + * subclasses. */ -public abstract class AbstractNestedLongPropertyTest extends AbstractNestedPropertyTest { +public abstract class AbstractNestedLongPropertyTest extends + AbstractNestedPropertyTest { /** * The last value returned by {@link #createNewValue()}. @@ -17,12 +19,12 @@ public abstract class AbstractNestedLongPropertyTest extends AbstractNestedPrope private long lastValue = 0; @Override - protected boolean allowsNullValues() { - return false; + protected boolean wrapsPrimitive() { + return true; } @Override - protected Number createNewValue() { + protected Long createNewValue() { lastValue += 1; return lastValue; } diff --git a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedObjectPropertyTest.java b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedObjectPropertyTest.java index 6051ce2..570c88f 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedObjectPropertyTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedObjectPropertyTest.java @@ -4,19 +4,19 @@ import javafx.beans.property.SimpleObjectProperty; import org.codefx.libfx.nesting.Nesting; -import org.codefx.libfx.nesting.property.NestedObjectProperty; import org.codefx.libfx.nesting.testhelper.SomeValue; /** * Abstract superclass to tests for {@link NestedObjectProperty NestedObjectProperties} which only leaves the creation - * of the tested properties (by {@link #createNestedPropertyFromNesting(Nesting)}) to the subclasses. + * of the tested properties (by {@link #createNestedPropertyFromNesting(Nesting, InnerObservableMissingBehavior)}) to + * the subclasses. */ public abstract class AbstractNestedObjectPropertyTest - extends AbstractNestedPropertyTest> { + extends AbstractNestedPropertyTest> { @Override - protected final boolean allowsNullValues() { - return true; + protected final boolean wrapsPrimitive() { + return false; } @Override diff --git a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedPropertyBuilderTest.java b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedPropertyBuilderTest.java index fa3fc23..6cb1e6b 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedPropertyBuilderTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedPropertyBuilderTest.java @@ -9,39 +9,38 @@ /** * Abstract superclass to tests for nested property builders. + *

+ * Some behavior of the builders is already tested in the property tests (e.g. the behavior when the inner observable is + * missing). This test cover the rest of the functionality. * * @param * the nesting hierarchy's inner type of {@link Property} * @param

* the type of {@link Property} which will be built */ +@SuppressWarnings("javadoc") public abstract class AbstractNestedPropertyBuilderTest, P extends NestedProperty> { - // #region TESTED INSTANCES + // #begin TESTED INSTANCES - /** - * The tested builder. - */ - private AbstractNestedPropertyBuilder builder; + private AbstractNestedPropertyBuilder builder; //#end TESTED INSTANCES - /** - * Creates a new builder before each test. - */ @Before public void setUp() { builder = createBuilder(); } - // #region TESTS + // #begin TESTS + + @Test(expected = NullPointerException.class) + public void setBean_nullBean_throwsException() { + builder.setBean(null); + } - /** - * Tests whether calling {@link AbstractNestedPropertyBuilder#setBean(Object)} sets the bean for the created nested - * property. - */ @Test - public void testSetBean() { + public void setBean_validBean_builtPropertyBelongsToThatBean() { // set a bean on the builder and let it build the property Object bean = "Mr. Bean"; builder.setBean(bean); @@ -50,21 +49,13 @@ public void testSetBean() { assertEquals(bean, nestedProperty.getBean()); } - /** - * Tests whether calling {@link AbstractNestedPropertyBuilder#setBean(Object)} with null causes a - * {@link NullPointerException}. - */ @Test(expected = NullPointerException.class) - public void testSetBeanToNull() { - builder.setBean(null); + public void setName_nullName_throwsException() { + builder.setName(null); } - /** - * Tests whether calling {@link AbstractNestedPropertyBuilder#setName(String)} sets the name for the created nested - * property. - */ @Test - public void testSetName() { + public void setName_validName_builrPropertyHasThatName() { // set a name on the builder and let it build the property String name = "The Name"; builder.setName(name); @@ -73,21 +64,8 @@ public void testSetName() { assertEquals(name, nestedProperty.getName()); } - /** - * Tests whether calling {@link AbstractNestedPropertyBuilder#setName(String)} with null causes a - * {@link NullPointerException}. - */ - @Test(expected = NullPointerException.class) - public void testSetNameToNull() { - builder.setName(null); - } - - /** - * Tests whether repeatedly calling {@link AbstractNestedPropertyBuilder#build()} returns different instances of - * nested properties. - */ @Test - public void testBuildCreatesNewInstances() { + public void callBuildRepeatedly_createsNewInstances() { P firstNestedProperty = builder.build(); P secondNestedProperty = builder.build(); @@ -96,14 +74,14 @@ public void testBuildCreatesNewInstances() { //#end TESTS - // #region ABSTRACT METHODS + // #begin ABSTRACT METHODS /** * Creates the tested builder. Each call must return a new instance * * @return an {@link AbstractNestedPropertyBuilder} */ - protected abstract AbstractNestedPropertyBuilder createBuilder(); + protected abstract AbstractNestedPropertyBuilder createBuilder(); //#end ABSTRACT METHODS diff --git a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedPropertyTest.java b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedPropertyTest.java index bb454a9..7967b96 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedPropertyTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedPropertyTest.java @@ -1,20 +1,26 @@ package org.codefx.libfx.nesting.property; import static org.codefx.libfx.nesting.testhelper.NestingAccess.getNestingObservable; -import static org.codefx.libfx.nesting.testhelper.NestingAccess.getNestingValue; import static org.codefx.libfx.nesting.testhelper.NestingAccess.setNestingObservable; import static org.codefx.libfx.nesting.testhelper.NestingAccess.setNestingValue; -import static org.junit.Assert.assertEquals; +import static org.codefx.libfx.nesting.testhelper.NestingAccess.EditableNesting.createWithInnerObservable; +import static org.codefx.tarkastus.AssertFX.assertDefault; +import static org.codefx.tarkastus.AssertFX.assertSameOrEqual; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Optional; +import java.util.function.Supplier; + import javafx.beans.property.Property; import org.codefx.libfx.nesting.Nesting; -import org.codefx.libfx.nesting.testhelper.NestingAccess; +import org.codefx.libfx.nesting.property.InnerObservableMissingBehavior.WhenInnerObservableGoesMissing; +import org.codefx.libfx.nesting.property.InnerObservableMissingBehavior.WhenInnerObservableMissingOnUpdate; +import org.codefx.libfx.nesting.testhelper.NestingAccess.EditableNesting; import org.junit.Before; import org.junit.Test; @@ -22,19 +28,23 @@ * Abstract superclass to tests of nested properties. By implementing the few abstract methods subclasses can run all * tests which apply to all nested property implementations. * + * @param + * the type of the instances contained in the nested property; e.g. {@link Integer} for + * {@link NestedIntegerProperty} * @param - * the type wrapped by the nested property + * the type wrapped by the nested property; e.g. {@link Number} for {@link NestedIntegerProperty} * @param

* the type of property wrapped by the nesting */ -public abstract class AbstractNestedPropertyTest> { +@SuppressWarnings("javadoc") +public abstract class AbstractNestedPropertyTest> { - // #region INSTANCES USED FOR TESTING + // #begin INSTANCES USED FOR TESTING /** * The nesting on which the tested property is based. */ - private NestingAccess.EditableNesting

nesting; + private EditableNesting

nesting; /** * The tested property. @@ -49,206 +59,265 @@ public abstract class AbstractNestedPropertyTest> { @Before public void setUp() { P innerObservable = createNewObservableWithSomeValue(); - nesting = NestingAccess.EditableNesting.createWithInnerObservable(innerObservable); - property = createNestedPropertyFromNesting(nesting); + nesting = createWithInnerObservable(innerObservable); + property = createNestedPropertyFromNesting(nesting, MissingBehavior.defaults()); } - // #region TESTS + // #begin TESTS - /** - * Tests whether the properties the tested property owns have the correct bean. - */ - public void testPropertyBean() { + @Test + public void innerObservablePresentProperty_getBean_returnsNestedProperty() { assertSame(property, property.innerObservablePresentProperty().getBean()); } - /** - * Tests whether the property's initial value (i.e. after construction) is the one held by the nesting's inner - * observable. - */ @Test - public void testInnerValueAfterConstruction() { - assertEquals(getNestingValue(nesting), property.getValue()); + public void getValue_afterConstruction_returnsInnerObservablesValue() { + // create a nesting with a non-default value for this + T initialValue = createNewValue(); + nesting = createWithInnerObservable(createNewObservableWithValue(initialValue)); + + property = createNestedPropertyFromNesting(nesting, MissingBehavior.defaults()); + + assertSameOrEqual(initialValue, nesting.getInnerObservable().get().getValue(), wrapsPrimitive()); + assertSameOrEqual(initialValue, property.getValue(), wrapsPrimitive()); assertTrue(property.isInnerObservablePresent()); } - /** - * Tests whether the property's value is correctly updated when the nesting's observable changes its value. - */ @Test - public void testChangingValue() { + public void setNestingValue_nestedPropertyHoldsSameValue() { T newValue = createNewValue(); setNestingValue(nesting, newValue); - // assert that setting the value worked - assertEquals(newValue, getNestingValue(nesting)); - // assert that nesting and property hold the new value - assertEquals(getNestingValue(nesting), property.getValue()); - assertEquals(newValue, property.getValue()); + assertSameOrEqual(newValue, property.getValue(), wrapsPrimitive()); } - /** - * Tests whether the property's value is correctly updated when the nesting's observable changes its value to null. - */ @Test - public void testChangingValueToNull() { - if (!allowsNullValues()) - return; - + public void setNestingValueToNull_nestedPropertyHoldsNull() { setNestingValue(nesting, null); - // assert that setting the value worked - assertNull(getNestingValue(nesting)); - // assert that the property holds null - assertNull(property.getValue()); + assertDefault(property.getValue()); } - /** - * Tests whether the property's value is correctly updated when the nesting gets a new observable. - */ @Test - public void testChangingObservable() { + public void setInnerObservable_nestedPropertyHoldsNewObservablesValue() { T newValue = createNewValue(); P newObservable = createNewObservableWithValue(newValue); setNestingObservable(nesting, newObservable); - // assert that setting the observable worked - assertEquals(newObservable, getNestingObservable(nesting)); - // assert that nesting and property hold the new value - assertEquals(getNestingValue(nesting), property.getValue()); - assertEquals(newValue, property.getValue()); - // assert that the inner observable is present + assertSameOrEqual(newValue, property.getValue(), wrapsPrimitive()); assertTrue(property.isInnerObservablePresent()); } - /** - * Tests whether the property's value is not updated when the nesting gets null as a new observable. - */ + // inner observable goes missing + @Test - public void testChangingObservableToNull() { + public void setInnerObservableToNull_defaultBehavior_propertyKeepsOldValue() { T oldValue = property.getValue(); setNestingObservable(nesting, null); - // assert that setting the null observable worked - assertNull(getNestingObservable(nesting)); - // assert that the nesting still holds the old value - assertEquals(oldValue, property.getValue()); - // assert that the inner observable is now missing, i.e. not present + assertSameOrEqual(oldValue, property.getValue(), wrapsPrimitive()); + assertFalse(property.isInnerObservablePresent()); + } + + @Test + public void setInnerObservableToNull_keepValue_propertyKeepsOldValue() { + MissingBehavior missingBehavior = MissingBehavior + . defaults() + .whenGoesMissing(WhenInnerObservableGoesMissing.KEEP_VALUE); + property = createNestedPropertyFromNesting(nesting, missingBehavior); + + T oldValue = property.getValue(); + setNestingObservable(nesting, null); + + assertSameOrEqual(oldValue, property.getValue(), wrapsPrimitive()); assertFalse(property.isInnerObservablePresent()); } - /** - * Tests whether changing the nested property's value while the nesting's observable is missing works. - */ @Test - public void testChangingValueWhileObservableIsMissing() { - // set the nesting observable to null + public void setInnerObservableToNull_setDefaultValue_propertyHoldsDefaultValue() { + MissingBehavior missingBehavior = MissingBehavior + . defaults() + .whenGoesMissing(WhenInnerObservableGoesMissing.SET_DEFAULT_VALUE); + property = createNestedPropertyFromNesting(nesting, missingBehavior); + + setNestingObservable(nesting, null); + + assertDefault(property.getValue()); + } + + @Test + public void setInnerObservableToNull_setValueFromSupplier_propertyHoldsThatValue() { + S newValue = createNewValue(); + MissingBehavior missingBehavior = MissingBehavior + . defaults() + .whenGoesMissing(WhenInnerObservableGoesMissing.SET_VALUE_FROM_SUPPLIER) + .valueForMissing(() -> newValue); + property = createNestedPropertyFromNesting(nesting, missingBehavior); + + setNestingObservable(nesting, null); + + assertSameOrEqual(newValue, property.getValue(), wrapsPrimitive()); + } + + @Test + public void setInnerObservableToNull_setNullFromSupplier_propertyHoldsDefaultValue() { + MissingBehavior missingBehavior = MissingBehavior + . defaults() + .whenGoesMissing(WhenInnerObservableGoesMissing.SET_VALUE_FROM_SUPPLIER) + .valueForMissing(() -> null); + property = createNestedPropertyFromNesting(nesting, missingBehavior); + + setNestingObservable(nesting, null); + + assertDefault(property.getValue()); + } + + // update when inner observable missing + + @Test(expected = IllegalStateException.class) + public void setValueOnInnerObservableMissing_defaulBehavior_throwException() { + setNestingObservable(nesting, null); + + property.setValue(createNewValue()); + } + + @Test(expected = IllegalStateException.class) + public void setValueOnInnerObservableMissing_throw_throwException() { + MissingBehavior missingBehavior = MissingBehavior + . defaults() + .onUpdate(WhenInnerObservableMissingOnUpdate.THROW_EXCEPTION); + property = createNestedPropertyFromNesting(nesting, missingBehavior); + setNestingObservable(nesting, null); + + property.setValue(createNewValue()); + } + + @Test + public void setValueOnInnerObservableMissing_acceptUntilNext_nestedPropertyHoldsNewValue() { + MissingBehavior missingBehavior = MissingBehavior + . defaults() + .onUpdate(WhenInnerObservableMissingOnUpdate.ACCEPT_VALUE_UNTIL_NEXT_INNER_OBSERVABLE); + property = createNestedPropertyFromNesting(nesting, missingBehavior); setNestingObservable(nesting, null); // set a new value (which can not be written to the nesting's observable as none is present) T newValue = createNewValue(); property.setValue(newValue); - // assert that the property indeed holds the new value - assertEquals(newValue, property.getValue()); + assertSameOrEqual(newValue, property.getValue(), wrapsPrimitive()); } - /** - * Tests whether the nested property's value, which was changed while the nesting's observable was missing, will not - * propagate to an observable which will be set thereafter. - */ @Test - public void testChangedValueNotPropagatedAfterObservableWasMissing() { - // set the nesting observable to null and create the new observable + public void newInnerObservableAfterSetValueOnMissingInnerObservable_acceptUntilNext_newInnerObservableKeepsValue() { + MissingBehavior missingBehavior = MissingBehavior + . defaults() + .onUpdate(WhenInnerObservableMissingOnUpdate.ACCEPT_VALUE_UNTIL_NEXT_INNER_OBSERVABLE); + property = createNestedPropertyFromNesting(nesting, missingBehavior); setNestingObservable(nesting, null); - P newObservable = createNewObservableWithValue(createNewValue()); + T newInnerObservablesValue = createNewValue(); + P newObservable = createNewObservableWithValue(newInnerObservablesValue); // change the nested property's value (which can not be written to the nesting's observable as none is present); - // due to the contract of 'createNewValue' the nested property has currently another value than the new observable property.setValue(createNewValue()); + // due to the contract of 'createNewValue' the nested property has currently another value than the new observable assertNotEquals(newObservable.getValue(), property.getValue()); - // set the new observable and assert that the property reflects its value + // set the new observable and assert that it kept its value and the nested property was updated setNestingObservable(nesting, newObservable); - assertEquals(newObservable.getValue(), property.getValue()); + + assertSameOrEqual(newInnerObservablesValue, newObservable.getValue(), wrapsPrimitive()); + assertSameOrEqual(newInnerObservablesValue, property.getValue(), wrapsPrimitive()); } - /** - * Tests whether the property's value is correctly updated when the nesting's new observable gets a new value. - */ + // binding to new inner observable + @Test - public void testChangingNewObservablesValue() { - // set a new observable ... + public void setValueOnNewInnerObservable_nestedPropertyHoldsThatValue() { P newObservable = createNewObservableWithSomeValue(); setNestingObservable(nesting, newObservable); - // (assert that setting the observable worked) - assertEquals(newObservable, getNestingObservable(nesting)); - // ... and change its value + // change the new observable's value T newValue = createNewValue(); newObservable.setValue(newValue); - // assert that nesting and property hold the new value - assertEquals(getNestingValue(nesting), property.getValue()); - assertEquals(newValue, property.getValue()); + assertSameOrEqual(newValue, property.getValue(), wrapsPrimitive()); } - /** - * Tests whether the property's value is not updated when the nesting's old observable gets a new value. - */ @Test - public void testChangingOldObservablesValue() { - // store the old observable ... + public void setValueOnNestedProperty_newInnerObservableHoldsThatValue() { + P newObservable = createNewObservableWithSomeValue(); + setNestingObservable(nesting, newObservable); + + // change the nested property's value + T newValue = createNewValue(); + property.setValue(newValue); + + assertSameOrEqual(newValue, newObservable.getValue(), wrapsPrimitive()); + } + + // unbinding from replaced inner observable + + @Test + public void setValueOnOldInnerObservable_nestedPropertyDoesNotChange() { Property oldObservable = getNestingObservable(nesting); + setNestingObservable(nesting, createNewObservableWithValue(createNewValue())); - // ... set a new observable ... - T newValueInNewObservable = createNewValue(); - P newObservable = createNewObservableWithValue(newValueInNewObservable); - setNestingObservable(nesting, newObservable); - // (assert that setting the observable worked) - assertNotSame(oldObservable, getNestingObservable(nesting)); - - // ... and change the old observable's value - T newValueInOldObservable = createNewValue(); - oldObservable.setValue(newValueInOldObservable); - - // assert that nesting and property do not hold the old observable's new value ... - assertNotEquals(newValueInOldObservable, property.getValue()); - // ... but the new observable's value - assertEquals(getNestingValue(nesting), property.getValue()); - assertEquals(newValueInNewObservable, property.getValue()); + // let the test fail when the nested property changes + property.addListener((obs, oldValue, newValue) -> fail()); + + // change the old observable's value + oldObservable.setValue(createNewValue()); + } + + @Test + public void setValueOnNestedProperty_oldInnerObservableDoesNotChange() { + Property oldObservable = getNestingObservable(nesting); + setNestingObservable(nesting, createNewObservableWithValue(createNewValue())); + + // let the test fail when the old observable changes + oldObservable.addListener((obs, oldValue, newValue) -> fail()); + + // change the nested property's value + property.setValue(createNewValue()); } - //#end TESTS + // #end TESTS - // #region ABSTRACT METHODS + // #begin ABSTRACT METHODS /** - * Indicates whether the tested nested property allows null values. + * Indicates whether the tested nested property wraps primitive values (e.g. ints). * - * @return true if the nested properties allows null values + * @return true if the nested properties wraps primitive values */ - protected abstract boolean allowsNullValues(); + protected abstract boolean wrapsPrimitive(); /** - * Creates the property, which will be tested, from the specified nesting. + * Creates the property which will be tested from the specified nesting. * * @param nesting * the nesting from which the nested property is created + * @param missingBehavior + * the behavior for the case that the inner observable is missing * @return a new {@link NestedProperty} instance */ - protected abstract NestedProperty createNestedPropertyFromNesting(Nesting

nesting); + protected abstract NestedProperty createNestedPropertyFromNesting( + Nesting

nesting, InnerObservableMissingBehavior missingBehavior); /** - * Creates a new value. Each call must return an instance which is not equal to any of those returned before and to - * that contained in the observable returned by {@link #createNewObservableWithSomeValue()}. + * Creates a new value. + *

+ * Each call must return an instance which is not equal to any of those returned before and to that contained in the + * observable returned by {@link #createNewObservableWithSomeValue()}. * - * @return a new instance of type {@code T} + * @return a new instance of type {@code S} */ - protected abstract T createNewValue(); + protected abstract S createNewValue(); /** - * Creates a new observable which holds the specified value. Each call must return a new instance. + * Creates a new observable which holds the specified value. + *

+ * Each call must return a new instance. * * @param value * the new observable's value @@ -257,38 +326,166 @@ public void testChangingOldObservablesValue() { protected abstract P createNewObservableWithValue(T value); /** - * Creates a new observable which holds some arbitrary value (there are no constraints for this value). Each call - * must return a new instance. + * Creates a new observable which holds some arbitrary value. + *

+ * Each call must return a new instance. * * @return a new {@link Property} instance with the specified value */ protected abstract P createNewObservableWithSomeValue(); - //#end ABSTRACT METHODS + // #end ABSTRACT METHODS - // #region ACCESSORS + // #begin HELPERS /** * @return the nesting on which the tested property is based */ - public NestingAccess.EditableNesting

getNesting() { + protected final EditableNesting

getNesting() { return nesting; } /** * @return the tested property */ - public NestedProperty getProperty() { + protected final NestedProperty getProperty() { return property; } /** * @return the {@link #getProperty tested property}'s current value */ - public T getPropertyValue() { + protected final T getPropertyValue() { return property.getValue(); } - //#end ACCESSORS + /** + * Sets the specified behavior for missing inner observables on the specified builder. + * + * @param behavior + * the behavior to set on the builder + * @param builder + * the mutated builder + */ + protected final void setBehaviorOnBuilder( + InnerObservableMissingBehavior behavior, AbstractNestedPropertyBuilder builder) { + // on goes missing + switch (behavior.whenGoesMissing()) { + case KEEP_VALUE: + builder.onInnerObservableMissingKeepValue(); + break; + case SET_DEFAULT_VALUE: + builder.onInnerObservableMissingSetDefaultValue(); + break; + case SET_VALUE_FROM_SUPPLIER: + builder.onInnerObservableMissingComputeValue(behavior.valueForMissing().get()); + break; + default: + throw new IllegalArgumentException(); + } + + // on update + switch (behavior.onUpdate()) { + case ACCEPT_VALUE_UNTIL_NEXT_INNER_OBSERVABLE: + builder.onUpdateWhenInnerObservableMissingAcceptValues(); + break; + case THROW_EXCEPTION: + builder.onUpdateWhenInnerObservableMissingThrowException(); + break; + default: + throw new IllegalArgumentException(); + } + } + + // #end HELPERS + + // #begin NESTED CLASSES + + /** + * Mutable implementation of {@link InnerObservableMissingBehavior}. + * + * @param + * the type contained in the nested property, e.g. {@link Integer} for {@link NestedIntegerProperty} + */ + protected static class MissingBehavior implements InnerObservableMissingBehavior { + + private WhenInnerObservableGoesMissing whenGoesMissing; + private Optional> valueForMissing; + private WhenInnerObservableMissingOnUpdate onUpdate; + + private MissingBehavior() {} + + /** + * Creates the default specification for the behavior when the inner observable is missing. + *

+ * The "production code" defines default behavior as well and it could be referenced here. Instead the defaults + * are explicitly specified (again) to ensure that changing them in some other place does not happen without + * breaking some tests. + * + * @param + * the type contained in the nested property + * @return the default behavior + */ + public static MissingBehavior defaults() { + MissingBehavior behavior = new MissingBehavior<>(); + behavior.whenGoesMissing = WhenInnerObservableGoesMissing.KEEP_VALUE; + behavior.valueForMissing = Optional.empty(); + behavior.onUpdate = WhenInnerObservableMissingOnUpdate.THROW_EXCEPTION; + return behavior; + } + + @Override + public WhenInnerObservableGoesMissing whenGoesMissing() { + return whenGoesMissing; + } + + /** + * Determines what happens the inner observable goes missing. + * + * @param whenGoesMissing + * the desired behavior + * @return this behavior + */ + public MissingBehavior whenGoesMissing(WhenInnerObservableGoesMissing whenGoesMissing) { + this.whenGoesMissing = whenGoesMissing; + return this; + } + + @Override + public Optional> valueForMissing() { + return valueForMissing; + } + + /** + * Sets a supplier which is called when the inner observable goes missing and a new value should be set. + * + * @param valueForMissing + * the supplier for the new value + * @return this behavior + */ + public MissingBehavior valueForMissing(Supplier valueForMissing) { + this.valueForMissing = Optional.of(valueForMissing); + return this; + } + + @Override + public WhenInnerObservableMissingOnUpdate onUpdate() { + return onUpdate; + } + + /** + * Determines what happens when the property is updated while the inner observable is missing. + * + * @param onUpdate + * the desired behavior + * @return this behavior + */ + public MissingBehavior onUpdate(WhenInnerObservableMissingOnUpdate onUpdate) { + this.onUpdate = onUpdate; + return this; + } + + } + // #end NESTED CLASSES } diff --git a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedStringPropertyTest.java b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedStringPropertyTest.java index 2e80295..996ef76 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedStringPropertyTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/AbstractNestedStringPropertyTest.java @@ -7,9 +7,11 @@ /** * Abstract superclass to tests for {@link NestedStringProperty NestedStringProperty} which only leaves the creation of - * the tested properties (by {@link #createNestedPropertyFromNesting(Nesting)}) to the subclasses. + * the tested properties (by {@link #createNestedPropertyFromNesting(Nesting, InnerObservableMissingBehavior)}) to the + * subclasses. */ -public abstract class AbstractNestedStringPropertyTest extends AbstractNestedPropertyTest { +public abstract class AbstractNestedStringPropertyTest extends + AbstractNestedPropertyTest { /** * The last value returned by {@link #createNewValue()}. @@ -17,8 +19,8 @@ public abstract class AbstractNestedStringPropertyTest extends AbstractNestedPro private String lastValue = ""; @Override - protected boolean allowsNullValues() { - return true; + protected boolean wrapsPrimitive() { + return false; } @Override diff --git a/src/test/java/org/codefx/libfx/nesting/property/NestedBooleanPropertyBuilderTest.java b/src/test/java/org/codefx/libfx/nesting/property/NestedBooleanPropertyBuilderTest.java index 625f741..ab4dd8c 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/NestedBooleanPropertyBuilderTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/NestedBooleanPropertyBuilderTest.java @@ -6,17 +6,13 @@ import org.codefx.libfx.nesting.Nesting; import org.codefx.libfx.nesting.testhelper.NestingAccess.EditableNesting; import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; + +import com.nitorcreations.junit.runners.NestedRunner; /** * Tests the class {@link NestedBooleanPropertyBuilder}. */ -@RunWith(Suite.class) -@SuiteClasses({ - NestedBooleanPropertyBuilderTest.AbstractBuilderContract.class, - NestedBooleanPropertyBuilderTest.CreatedProperties.class, -}) +@RunWith(NestedRunner.class) public class NestedBooleanPropertyBuilderTest { /** @@ -26,7 +22,7 @@ public static class AbstractBuilderContract extends AbstractNestedPropertyBuilderTest { @Override - protected AbstractNestedPropertyBuilder createBuilder() { + protected AbstractNestedPropertyBuilder createBuilder() { BooleanProperty innerObservable = new SimpleBooleanProperty(false); EditableNesting nesting = EditableNesting.createWithInnerObservable(innerObservable); return NestedBooleanPropertyBuilder.forNesting(nesting); @@ -40,9 +36,11 @@ protected AbstractNestedPropertyBuilder public static class CreatedProperties extends AbstractNestedBooleanPropertyTest { @Override - protected NestedProperty createNestedPropertyFromNesting(Nesting nesting) { + protected NestedProperty createNestedPropertyFromNesting( + Nesting nesting, InnerObservableMissingBehavior missingBehavior) { // use the builder to create the property NestedBooleanPropertyBuilder builder = NestedBooleanPropertyBuilder.forNesting(nesting); + setBehaviorOnBuilder(missingBehavior, builder); return builder.build(); } diff --git a/src/test/java/org/codefx/libfx/nesting/property/NestedBooleanPropertyTest.java b/src/test/java/org/codefx/libfx/nesting/property/NestedBooleanPropertyTest.java index 095329d..8523ba2 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/NestedBooleanPropertyTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/NestedBooleanPropertyTest.java @@ -10,8 +10,9 @@ public class NestedBooleanPropertyTest extends AbstractNestedBooleanPropertyTest { @Override - protected NestedProperty createNestedPropertyFromNesting(Nesting nesting) { - return new NestedBooleanProperty(nesting, null, null); + protected NestedProperty createNestedPropertyFromNesting( + Nesting nesting, InnerObservableMissingBehavior missingBehavior) { + return new NestedBooleanProperty(nesting, missingBehavior, null, null); } } diff --git a/src/test/java/org/codefx/libfx/nesting/property/NestedDoublePropertyBuilderTest.java b/src/test/java/org/codefx/libfx/nesting/property/NestedDoublePropertyBuilderTest.java index 3aff198..e2e14fc 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/NestedDoublePropertyBuilderTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/NestedDoublePropertyBuilderTest.java @@ -6,17 +6,13 @@ import org.codefx.libfx.nesting.Nesting; import org.codefx.libfx.nesting.testhelper.NestingAccess.EditableNesting; import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; + +import com.nitorcreations.junit.runners.NestedRunner; /** * Tests the class {@link NestedDoublePropertyBuilder}. */ -@RunWith(Suite.class) -@SuiteClasses({ - NestedDoublePropertyBuilderTest.AbstractBuilderContract.class, - NestedDoublePropertyBuilderTest.CreatedProperties.class, -}) +@RunWith(NestedRunner.class) public class NestedDoublePropertyBuilderTest { /** @@ -26,7 +22,7 @@ public static class AbstractBuilderContract extends AbstractNestedPropertyBuilderTest { @Override - protected AbstractNestedPropertyBuilder createBuilder() { + protected AbstractNestedPropertyBuilder createBuilder() { DoubleProperty innerObservable = new SimpleDoubleProperty(0); EditableNesting nesting = EditableNesting.createWithInnerObservable(innerObservable); return NestedDoublePropertyBuilder.forNesting(nesting); @@ -40,9 +36,11 @@ protected AbstractNestedPropertyBuilder cr public static class CreatedProperties extends AbstractNestedDoublePropertyTest { @Override - protected NestedProperty createNestedPropertyFromNesting(Nesting nesting) { + protected NestedProperty createNestedPropertyFromNesting( + Nesting nesting, InnerObservableMissingBehavior missingBehavior) { // use the builder to create the property NestedDoublePropertyBuilder builder = NestedDoublePropertyBuilder.forNesting(nesting); + setBehaviorOnBuilder(missingBehavior, builder); return builder.build(); } diff --git a/src/test/java/org/codefx/libfx/nesting/property/NestedDoublePropertyTest.java b/src/test/java/org/codefx/libfx/nesting/property/NestedDoublePropertyTest.java index d7ca4ea..2fde40c 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/NestedDoublePropertyTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/NestedDoublePropertyTest.java @@ -10,8 +10,9 @@ public class NestedDoublePropertyTest extends AbstractNestedDoublePropertyTest { @Override - protected NestedProperty createNestedPropertyFromNesting(Nesting nesting) { - return new NestedDoubleProperty(nesting, null, null); + protected NestedProperty createNestedPropertyFromNesting( + Nesting nesting, InnerObservableMissingBehavior missingBehavior) { + return new NestedDoubleProperty(nesting, missingBehavior, null, null); } } diff --git a/src/test/java/org/codefx/libfx/nesting/property/NestedFloatPropertyBuilderTest.java b/src/test/java/org/codefx/libfx/nesting/property/NestedFloatPropertyBuilderTest.java index 1c717fa..44cc416 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/NestedFloatPropertyBuilderTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/NestedFloatPropertyBuilderTest.java @@ -6,17 +6,13 @@ import org.codefx.libfx.nesting.Nesting; import org.codefx.libfx.nesting.testhelper.NestingAccess.EditableNesting; import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; + +import com.nitorcreations.junit.runners.NestedRunner; /** * Tests the class {@link NestedFloatPropertyBuilder}. */ -@RunWith(Suite.class) -@SuiteClasses({ - NestedFloatPropertyBuilderTest.AbstractBuilderContract.class, - NestedFloatPropertyBuilderTest.CreatedProperties.class, -}) +@RunWith(NestedRunner.class) public class NestedFloatPropertyBuilderTest { /** @@ -26,7 +22,7 @@ public static class AbstractBuilderContract extends AbstractNestedPropertyBuilderTest { @Override - protected AbstractNestedPropertyBuilder createBuilder() { + protected AbstractNestedPropertyBuilder createBuilder() { FloatProperty innerObservable = new SimpleFloatProperty(0); EditableNesting nesting = EditableNesting.createWithInnerObservable(innerObservable); return NestedFloatPropertyBuilder.forNesting(nesting); @@ -40,9 +36,11 @@ protected AbstractNestedPropertyBuilder crea public static class CreatedProperties extends AbstractNestedFloatPropertyTest { @Override - protected NestedProperty createNestedPropertyFromNesting(Nesting nesting) { + protected NestedProperty createNestedPropertyFromNesting( + Nesting nesting, InnerObservableMissingBehavior missingBehavior) { // use the builder to create the property NestedFloatPropertyBuilder builder = NestedFloatPropertyBuilder.forNesting(nesting); + setBehaviorOnBuilder(missingBehavior, builder); return builder.build(); } diff --git a/src/test/java/org/codefx/libfx/nesting/property/NestedFloatPropertyTest.java b/src/test/java/org/codefx/libfx/nesting/property/NestedFloatPropertyTest.java index c5be6fe..e4ad306 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/NestedFloatPropertyTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/NestedFloatPropertyTest.java @@ -10,8 +10,9 @@ public class NestedFloatPropertyTest extends AbstractNestedFloatPropertyTest { @Override - protected NestedProperty createNestedPropertyFromNesting(Nesting nesting) { - return new NestedFloatProperty(nesting, null, null); + protected NestedProperty createNestedPropertyFromNesting( + Nesting nesting, InnerObservableMissingBehavior missingBehavior) { + return new NestedFloatProperty(nesting, missingBehavior, null, null); } } diff --git a/src/test/java/org/codefx/libfx/nesting/property/NestedIntegerPropertyBuilderTest.java b/src/test/java/org/codefx/libfx/nesting/property/NestedIntegerPropertyBuilderTest.java index 1727c62..d7f1ecd 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/NestedIntegerPropertyBuilderTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/NestedIntegerPropertyBuilderTest.java @@ -6,17 +6,13 @@ import org.codefx.libfx.nesting.Nesting; import org.codefx.libfx.nesting.testhelper.NestingAccess.EditableNesting; import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; + +import com.nitorcreations.junit.runners.NestedRunner; /** * Tests the class {@link NestedIntegerPropertyBuilder}. */ -@RunWith(Suite.class) -@SuiteClasses({ - NestedIntegerPropertyBuilderTest.AbstractBuilderContract.class, - NestedIntegerPropertyBuilderTest.CreatedProperties.class, -}) +@RunWith(NestedRunner.class) public class NestedIntegerPropertyBuilderTest { /** @@ -26,7 +22,7 @@ public static class AbstractBuilderContract extends AbstractNestedPropertyBuilderTest { @Override - protected AbstractNestedPropertyBuilder createBuilder() { + protected AbstractNestedPropertyBuilder createBuilder() { IntegerProperty innerObservable = new SimpleIntegerProperty(0); EditableNesting nesting = EditableNesting.createWithInnerObservable(innerObservable); return NestedIntegerPropertyBuilder.forNesting(nesting); @@ -40,9 +36,11 @@ protected AbstractNestedPropertyBuilder public static class CreatedProperties extends AbstractNestedIntegerPropertyTest { @Override - protected NestedProperty createNestedPropertyFromNesting(Nesting nesting) { + protected NestedProperty createNestedPropertyFromNesting( + Nesting nesting, InnerObservableMissingBehavior missingBehavior) { // use the builder to create the property NestedIntegerPropertyBuilder builder = NestedIntegerPropertyBuilder.forNesting(nesting); + setBehaviorOnBuilder(missingBehavior, builder); return builder.build(); } diff --git a/src/test/java/org/codefx/libfx/nesting/property/NestedIntegerPropertyTest.java b/src/test/java/org/codefx/libfx/nesting/property/NestedIntegerPropertyTest.java index 0f3ed33..b607cf9 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/NestedIntegerPropertyTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/NestedIntegerPropertyTest.java @@ -3,8 +3,6 @@ import javafx.beans.property.IntegerProperty; import org.codefx.libfx.nesting.Nesting; -import org.codefx.libfx.nesting.property.NestedIntegerProperty; -import org.codefx.libfx.nesting.property.NestedProperty; /** * Tests the class {@link NestedIntegerProperty}. @@ -12,8 +10,9 @@ public class NestedIntegerPropertyTest extends AbstractNestedIntegerPropertyTest { @Override - protected NestedProperty createNestedPropertyFromNesting(Nesting nesting) { - return new NestedIntegerProperty(nesting, null, null); + protected NestedProperty createNestedPropertyFromNesting( + Nesting nesting, InnerObservableMissingBehavior missingBehavior) { + return new NestedIntegerProperty(nesting, missingBehavior, null, null); } } diff --git a/src/test/java/org/codefx/libfx/nesting/property/NestedLongPropertyBuilderTest.java b/src/test/java/org/codefx/libfx/nesting/property/NestedLongPropertyBuilderTest.java index ee15ea7..364e58c 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/NestedLongPropertyBuilderTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/NestedLongPropertyBuilderTest.java @@ -6,17 +6,13 @@ import org.codefx.libfx.nesting.Nesting; import org.codefx.libfx.nesting.testhelper.NestingAccess.EditableNesting; import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; + +import com.nitorcreations.junit.runners.NestedRunner; /** * Tests the class {@link NestedLongPropertyBuilder}. */ -@RunWith(Suite.class) -@SuiteClasses({ - NestedLongPropertyBuilderTest.AbstractBuilderContract.class, - NestedLongPropertyBuilderTest.CreatedProperties.class, -}) +@RunWith(NestedRunner.class) public class NestedLongPropertyBuilderTest { /** @@ -26,7 +22,7 @@ public static class AbstractBuilderContract extends AbstractNestedPropertyBuilderTest { @Override - protected AbstractNestedPropertyBuilder createBuilder() { + protected AbstractNestedPropertyBuilder createBuilder() { LongProperty innerObservable = new SimpleLongProperty(0); EditableNesting nesting = EditableNesting.createWithInnerObservable(innerObservable); return NestedLongPropertyBuilder.forNesting(nesting); @@ -40,9 +36,11 @@ protected AbstractNestedPropertyBuilder create public static class CreatedProperties extends AbstractNestedLongPropertyTest { @Override - protected NestedProperty createNestedPropertyFromNesting(Nesting nesting) { + protected NestedProperty createNestedPropertyFromNesting( + Nesting nesting, InnerObservableMissingBehavior missingBehavior) { // use the builder to create the property NestedLongPropertyBuilder builder = NestedLongPropertyBuilder.forNesting(nesting); + setBehaviorOnBuilder(missingBehavior, builder); return builder.build(); } diff --git a/src/test/java/org/codefx/libfx/nesting/property/NestedLongPropertyTest.java b/src/test/java/org/codefx/libfx/nesting/property/NestedLongPropertyTest.java index f610e23..158fb54 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/NestedLongPropertyTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/NestedLongPropertyTest.java @@ -10,8 +10,9 @@ public class NestedLongPropertyTest extends AbstractNestedLongPropertyTest { @Override - protected NestedProperty createNestedPropertyFromNesting(Nesting nesting) { - return new NestedLongProperty(nesting, null, null); + protected NestedProperty createNestedPropertyFromNesting( + Nesting nesting, InnerObservableMissingBehavior missingBehavior) { + return new NestedLongProperty(nesting, missingBehavior, null, null); } } diff --git a/src/test/java/org/codefx/libfx/nesting/property/NestedObjectPropertyBuilderTest.java b/src/test/java/org/codefx/libfx/nesting/property/NestedObjectPropertyBuilderTest.java index 3ef4e50..72e3f7c 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/NestedObjectPropertyBuilderTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/NestedObjectPropertyBuilderTest.java @@ -4,23 +4,16 @@ import javafx.beans.property.SimpleObjectProperty; import org.codefx.libfx.nesting.Nesting; -import org.codefx.libfx.nesting.property.AbstractNestedPropertyBuilder; -import org.codefx.libfx.nesting.property.NestedObjectPropertyBuilder; -import org.codefx.libfx.nesting.property.NestedProperty; -import org.codefx.libfx.nesting.testhelper.SomeValue; import org.codefx.libfx.nesting.testhelper.NestingAccess.EditableNesting; +import org.codefx.libfx.nesting.testhelper.SomeValue; import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; + +import com.nitorcreations.junit.runners.NestedRunner; /** * Tests the class {@link NestedObjectPropertyBuilder}. */ -@RunWith(Suite.class) -@SuiteClasses({ - NestedObjectPropertyBuilderTest.AbstractBuilderContract.class, - NestedObjectPropertyBuilderTest.CreatedProperties.class, -}) +@RunWith(NestedRunner.class) public class NestedObjectPropertyBuilderTest { /** @@ -30,7 +23,7 @@ public static class AbstractBuilderContract extends AbstractNestedPropertyBuilderTest, NestedProperty> { @Override - protected AbstractNestedPropertyBuilder, NestedProperty> createBuilder() { + protected AbstractNestedPropertyBuilder, NestedProperty, ?> createBuilder() { Property innerObservable = new SimpleObjectProperty<>(new SomeValue()); EditableNesting> nesting = EditableNesting.createWithInnerObservable(innerObservable); return NestedObjectPropertyBuilder.forNesting(nesting); @@ -44,9 +37,11 @@ protected AbstractNestedPropertyBuilder, NestedProperty createNestedPropertyFromNesting(Nesting> nesting) { + protected NestedProperty createNestedPropertyFromNesting( + Nesting> nesting, InnerObservableMissingBehavior missingBehavior) { // use the builder to create the property NestedObjectPropertyBuilder builder = NestedObjectPropertyBuilder.forNesting(nesting); + setBehaviorOnBuilder(missingBehavior, builder); return builder.build(); } diff --git a/src/test/java/org/codefx/libfx/nesting/property/NestedObjectPropertyTest.java b/src/test/java/org/codefx/libfx/nesting/property/NestedObjectPropertyTest.java index d438f2c..411b00f 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/NestedObjectPropertyTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/NestedObjectPropertyTest.java @@ -3,8 +3,6 @@ import javafx.beans.property.Property; import org.codefx.libfx.nesting.Nesting; -import org.codefx.libfx.nesting.property.NestedObjectProperty; -import org.codefx.libfx.nesting.property.NestedProperty; import org.codefx.libfx.nesting.testhelper.SomeValue; /** @@ -13,8 +11,9 @@ public class NestedObjectPropertyTest extends AbstractNestedObjectPropertyTest { @Override - protected NestedProperty createNestedPropertyFromNesting(Nesting> nesting) { - return new NestedObjectProperty<>(nesting, null, null); + protected NestedProperty createNestedPropertyFromNesting( + Nesting> nesting, InnerObservableMissingBehavior missingBehavior) { + return new NestedObjectProperty<>(nesting, missingBehavior, null, null); } } diff --git a/src/test/java/org/codefx/libfx/nesting/property/NestedStringPropertyBuilderTest.java b/src/test/java/org/codefx/libfx/nesting/property/NestedStringPropertyBuilderTest.java index 54b0a01..6b3099f 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/NestedStringPropertyBuilderTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/NestedStringPropertyBuilderTest.java @@ -6,27 +6,23 @@ import org.codefx.libfx.nesting.Nesting; import org.codefx.libfx.nesting.testhelper.NestingAccess.EditableNesting; import org.junit.runner.RunWith; -import org.junit.runners.Suite; -import org.junit.runners.Suite.SuiteClasses; + +import com.nitorcreations.junit.runners.NestedRunner; /** * Tests the class {@link NestedStringPropertyBuilder}. */ -@RunWith(Suite.class) -@SuiteClasses({ - NestedStringPropertyBuilderTest.AbstractBuilderContract.class, - NestedStringPropertyBuilderTest.CreatedProperties.class, -}) +@RunWith(NestedRunner.class) public class NestedStringPropertyBuilderTest { /** * Tests whether the builder fulfills the contract defined by {@link AbstractNestedPropertyBuilder}. */ public static class AbstractBuilderContract - extends AbstractNestedPropertyBuilderTest { + extends AbstractNestedPropertyBuilderTest { @Override - protected AbstractNestedPropertyBuilder createBuilder() { + protected AbstractNestedPropertyBuilder createBuilder() { StringProperty innerObservable = new SimpleStringProperty(""); EditableNesting nesting = EditableNesting.createWithInnerObservable(innerObservable); return NestedStringPropertyBuilder.forNesting(nesting); @@ -40,9 +36,11 @@ protected AbstractNestedPropertyBuilder cr public static class CreatedProperties extends AbstractNestedStringPropertyTest { @Override - protected NestedProperty createNestedPropertyFromNesting(Nesting nesting) { + protected NestedProperty createNestedPropertyFromNesting( + Nesting nesting, InnerObservableMissingBehavior missingBehavior) { // use the builder to create the property NestedStringPropertyBuilder builder = NestedStringPropertyBuilder.forNesting(nesting); + setBehaviorOnBuilder(missingBehavior, builder); return builder.build(); } diff --git a/src/test/java/org/codefx/libfx/nesting/property/NestedStringPropertyTest.java b/src/test/java/org/codefx/libfx/nesting/property/NestedStringPropertyTest.java index d8a586d..ebd9c0d 100644 --- a/src/test/java/org/codefx/libfx/nesting/property/NestedStringPropertyTest.java +++ b/src/test/java/org/codefx/libfx/nesting/property/NestedStringPropertyTest.java @@ -10,8 +10,9 @@ public class NestedStringPropertyTest extends AbstractNestedStringPropertyTest { @Override - protected NestedProperty createNestedPropertyFromNesting(Nesting nesting) { - return new NestedStringProperty(nesting, null, null); + protected NestedProperty createNestedPropertyFromNesting( + Nesting nesting, InnerObservableMissingBehavior missingBehavior) { + return new NestedStringProperty(nesting, missingBehavior, null, null); } } diff --git a/src/test/java/org/codefx/libfx/nesting/testhelper/InnerValue.java b/src/test/java/org/codefx/libfx/nesting/testhelper/InnerValue.java index 4c498c5..3fd893a 100644 --- a/src/test/java/org/codefx/libfx/nesting/testhelper/InnerValue.java +++ b/src/test/java/org/codefx/libfx/nesting/testhelper/InnerValue.java @@ -11,7 +11,7 @@ */ public class InnerValue { -// #region PROPERTIES +// #begin PROPERTIES /** * An observable. @@ -30,7 +30,7 @@ public class InnerValue { // #end PROPERTIES -// #region CONSTRUCTOR +// #begin CONSTRUCTOR /** * Creates a new inner value with the specified observables. @@ -68,7 +68,7 @@ public static InnerValue createWithObservables() { // #end CONSTRUCTOR -// #region ACCESSORS +// #begin ACCESSORS /** * An observable. diff --git a/src/test/java/org/codefx/libfx/nesting/testhelper/NestingAccess.java b/src/test/java/org/codefx/libfx/nesting/testhelper/NestingAccess.java index 90ba5af..bf94fde 100644 --- a/src/test/java/org/codefx/libfx/nesting/testhelper/NestingAccess.java +++ b/src/test/java/org/codefx/libfx/nesting/testhelper/NestingAccess.java @@ -19,7 +19,7 @@ */ public class NestingAccess { - // #region NESTING + // #begin NESTING /** * @param @@ -32,12 +32,8 @@ public class NestingAccess { */ public static O getNestingObservable(Nesting nesting) { Objects.requireNonNull(nesting, "The argument 'nesting' must not be null."); - Optional nestingObservable = nesting.innerObservableProperty().getValue(); - if (nestingObservable.isPresent()) - return nestingObservable.get(); - else - return null; + return nestingObservable.orElse(null); } /** @@ -52,7 +48,6 @@ public static O getNestingObservable(Nesting nesting) */ public static void setNestingObservable(EditableNesting nesting, O newObservable) { Objects.requireNonNull(nesting, "The argument 'nesting' must not be null."); - nesting.setInnerObservable(Optional.ofNullable(newObservable)); } @@ -71,7 +66,6 @@ public static void setNestingObservable(EditableNesting> T getNestingValue(Nesting nesting) { Objects.requireNonNull(nesting, "The argument 'nesting' must not be null."); - return getNestingObservable(nesting).getValue(); } @@ -91,13 +85,12 @@ public static > T getNestingValue(Nesting nes */ public static > void setNestingValue(Nesting nesting, T newValue) { Objects.requireNonNull(nesting, "The argument 'nesting' must not be null."); - getNestingObservable(nesting).setValue(newValue); } //#end NESTING - // #region NESTED HIERARCHY + // #begin NESTED HIERARCHY /** * @param outerObservable @@ -106,7 +99,6 @@ public static > void setNestingValue(Nesting nesting */ public static OuterValue getOuterValue(ObservableValue outerObservable) { Objects.requireNonNull(outerObservable, "The argument 'outerObservable' must not be null."); - return outerObservable.getValue(); } @@ -120,7 +112,6 @@ public static OuterValue getOuterValue(ObservableValue outerObservab */ public static void setOuterValue(Property outerObservable, OuterValue outerValue) { Objects.requireNonNull(outerObservable, "The argument 'outerObservable' must not be null."); - outerObservable.setValue(outerValue); } @@ -133,7 +124,6 @@ public static void setOuterValue(Property outerObservable, OuterValu */ public static InnerValue getInnerValue(ObservableValue outerObservable) { Objects.requireNonNull(outerObservable, "The argument 'outerObservable' must not be null."); - return outerObservable.getValue().getInnerValue(); } @@ -149,7 +139,6 @@ public static InnerValue getInnerValue(ObservableValue outerObservab */ public static void setInnerValue(ObservableValue outerObservable, InnerValue innerValue) { Objects.requireNonNull(outerObservable, "The argument 'outerObservable' must not be null."); - outerObservable.getValue().setInnerValue(innerValue); } @@ -162,7 +151,6 @@ public static void setInnerValue(ObservableValue outerObservable, In */ public static Observable getInnerObservable(ObservableValue outerObservable) { Objects.requireNonNull(outerObservable, "The argument 'outerObservable' must not be null."); - return getInnerValue(outerObservable).observable(); } @@ -175,7 +163,6 @@ public static Observable getInnerObservable(ObservableValue outerObs */ public static Property getInnerProperty(ObservableValue outerObservable) { Objects.requireNonNull(outerObservable, "The argument 'outerObservable' must not be null."); - return getInnerValue(outerObservable).property(); } @@ -192,7 +179,7 @@ public static IntegerProperty getInnerIntegerProperty(ObservableValue + * the type of the compared instances + * @param expected + * expected value + * @param actual + * the value to check against expected + * @param isPrimitive + * indicates whether the compared type is a primitive + */ + public static void assertSameOrEqual(T expected, T actual, boolean isPrimitive) { + if (isPrimitive) + assertEquals(expected, actual); + else + assertSame(expected, actual); + } + + /** + * If the specified value is of a primitive wrapping type (e.g. {@link Integer}), a call asserts that it is the + * default value for that primitive; otherwise the value must be null. + * + * @param value + * the value to check + */ + public static void assertDefault(Object value) { + if (value instanceof Boolean) + assertFalse((Boolean) value); + else if (value instanceof Integer) + assertEquals(0, ((Integer) value).intValue()); + else if (value instanceof Long) + assertEquals(0, ((Long) value).longValue()); + else if (value instanceof Float) + assertEquals(0, ((Float) value).floatValue(), 0); + else if (value instanceof Double) + assertEquals(0, ((Double) value).doubleValue(), 0); + else + assertNull(value); + } + +} diff --git a/src/test/java/org/codefx/tarkastus/JavaFXRule.java b/src/test/java/org/codefx/tarkastus/JavaFXRule.java new file mode 100644 index 0000000..f798954 --- /dev/null +++ b/src/test/java/org/codefx/tarkastus/JavaFXRule.java @@ -0,0 +1,150 @@ +package org.codefx.tarkastus; + +import java.util.Optional; +import java.util.concurrent.CountDownLatch; + +import javafx.application.Platform; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; +import javafx.embed.swing.JFXPanel; + +import javax.swing.SwingUtilities; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * A rule which evaluates all statements (i.e. runs all tests) on the JavaFX platform thread. + *

+ * The platform thread is created on demand. + *

+ * TODO: superficial tests + */ +public class JavaFXRule implements TestRule { + + @Override + public Statement apply(Statement statement, Description description) { + return new JavaFXStatement(statement); + } + + /** + * Evaluates statements on the JavaFX platform thread. + */ + private static class JavaFXStatement extends Statement { + + private final StatementOnPlatformThread statement; + + /** + * Creates a new JavaFX statement. + * + * @param statement + * the statement which will be evaluated on the platform thread + */ + public JavaFXStatement(Statement statement) { + this.statement = new StatementOnPlatformThread(statement); + } + + @Override + public void evaluate() throws Throwable { + JavaFXInitializer.ensureInitialized(); + statement.evaluate(); + } + + } + + /** + * Ensures that JavaFX is initialized. + */ + private static class JavaFXInitializer { + + private static boolean initialized = false; + + /** + * Ensures that JavaFX is initialized. + * + * @throws InterruptedException + * when waiting for the initialization (which happens in another thread) to complete is interrupted + */ + public static void ensureInitialized() throws InterruptedException { + if (initialized) + return; + + initializeOnce(); + } + + private static synchronized void initializeOnce() throws InterruptedException { + if (initialized) + return; + + initialize(); + initialized = true; + } + + @SuppressWarnings("unused") + private static void initialize() throws InterruptedException { + + /* + * To initialize JavaFX, create a JFXPanel on the Swing EDT. + */ + + final CountDownLatch initialized = new CountDownLatch(1); + SwingUtilities.invokeLater(() -> { + new JFXPanel(); + initialized.countDown(); + }); + initialized.await(); + } + + } + + /** + * Evaluates a statement on the JavaFX platform thread. + */ + private static class StatementOnPlatformThread { + + private final Statement statement; + + /** + * Creates a new statement. + * + * @param statement + * the statement which will be evaluated on the platform thread + */ + public StatementOnPlatformThread(Statement statement) { + this.statement = statement; + } + + /** + * Evaluates the statement specified during construction + * + * @throws Throwable + * when the statement evaluation throws an exception + */ + public void evaluate() throws Throwable { + Property> caughtThrowable = new SimpleObjectProperty<>(Optional.empty()); + CountDownLatch evaluated = new CountDownLatch(1); + + Platform.runLater(() -> { + caughtThrowable.setValue(evaluateOnPlatform(statement)); + evaluated.countDown(); + }); + + evaluated.await(); + + // make sure to rethrow any exception which occurred during evaluation + if (caughtThrowable.getValue().isPresent()) + throw caughtThrowable.getValue().get(); + } + + private static Optional evaluateOnPlatform(Statement statement) { + try { + statement.evaluate(); + return Optional.empty(); + } catch (Throwable ex) { + return Optional.of(ex); + } + } + + } +}