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.libfxLibFX
- 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.libfxLibFX
- 0.2.1
+ 0.3.0jarLibFX
- LibFX provides utility classes for JavaFX.
+ LibFX provides usability classes for Java and JavaFX.http://libfx.codefx.org
@@ -70,14 +70,14 @@
junitjunit
- 4.11
+ 4.12testorg.mockitomockito-all
- 1.9.5
+ 1.10.19test
@@ -85,6 +85,21 @@
net.sourceforge.nekohtmlnekohtml1.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.pluginsmaven-compiler-plugin
- 3.2
+ 3.31.8
+
+
+ org.codefx.maven.plugin
+ jdeps-maven-plugin
+ 0.1
+
+
+
+ jdkinternals
+
+
+
+ org.apache.maven.plugins
@@ -127,7 +155,7 @@
org.apache.maven.pluginsmaven-javadoc-plugin
- 2.10.1
+ 2.10.3attach-javadocs
@@ -137,14 +165,6 @@
-
-
-
- api_1.8
- https://docs.oracle.com/javase/8/docs/api/
-
- org.apache.maven.pluginsmaven-gpg-plugin
- 1.5
+ 1.6sign-artifacts
@@ -175,7 +195,7 @@
org.sonatype.pluginsnexus-staging-maven-plugin
- 1.6.2
+ 1.6.5trueossrh
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 extends O> otherCollection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final boolean remove(Object object) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final boolean removeIf(Predicate super O> 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 extends OK, ? extends OV> outerMap) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final OV compute(OK key, BiFunction super OK, ? super OV, ? extends OV> remappingFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final OV computeIfAbsent(OK key, Function super OK, ? extends OV> mappingFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final OV computeIfPresent(OK key, BiFunction super OK, ? super OV, ? extends OV> remappingFunction) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final OV merge(OK key, OV value, BiFunction super OV, ? super OV, ? extends OV> 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 super OK, ? super OV, ? extends OV> 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 extends O> otherCollection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final boolean remove(Object object) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final boolean removeIf(Predicate super O> 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 extends O> 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 extends O> 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 extends O> 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 super O> 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 super O> 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 extends I> 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 extends O> 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 extends O> 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 extends O> 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 super O> 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 extends OK, ? extends OV> 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 super OK, ? super OV, ? extends OV> 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 super OK, ? extends OV> 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 super OK, ? super OV, ? extends OV> 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 super OV, ? super OV, ? extends OV> 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 super OK, ? super OV, ? extends OV> 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 super OK, ? super OV> 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 super IK, ? extends IV> transformToInnerToKeyValueFunction(
+ Function super OK, ? extends OV> function) {
+
+ return innerKey -> transformToInnerValue(function.apply(transformToOuterKey(innerKey)));
+ }
+
+ private BiFunction super IK, ? super IV, ? extends IV> transformToInnerKeyValueToValueFunction(
+ BiFunction super OK, ? super OV, ? extends OV> function) {
+
+ return (innerKey, innerValue) -> transformToInnerValue(function.apply(
+ transformToOuterKey(innerKey),
+ transformToOuterValue(innerValue)));
+ }
+
+ private BiFunction super IV, ? super IV, ? extends IV> transformToInnerValueValueToValueFunction(
+ BiFunction super OV, ? super OV, ? extends OV> function) {
+
+ return (innerValue1, innerValue2) -> transformToInnerValue(function.apply(
+ transformToOuterValue(innerValue1),
+ transformToOuterValue(innerValue2)));
+ }
+
+ private BiConsumer super IK, ? super IV> transformToInnerKeyValueConsumer(
+ BiConsumer super OK, ? super OV> 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 extends OK> 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 extends OV> 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 extends Entry> otherCollection) {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+
+ private class TransformToReadOnlyInnerMap extends AbstractReadOnlyTransformingMap {
+
+ private final Map extends OK, ? extends OV> transformedMap;
+
+ public TransformToReadOnlyInnerMap(Map extends OK, ? extends OV> 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 super O> action) {
+ Consumer transformThenAction = transformThen(action);
+ return getInnerSpliterator().tryAdvance(transformThenAction);
+ }
+
+ @Override
+ public void forEachRemaining(Consumer super O> 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 super O> getComparator() {
+ Comparator super I> 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 super O> 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 super E, ? super E> equals;
+ private final ToIntFunction super E> hash;
+
+ private EqHash(E element, BiPredicate super E, ? super E> equals, ToIntFunction super E> 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 super E, ? super E> equals, ToIntFunction super E> 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:
+ *
+ *
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}
+ *
+ *
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 super E> outerKeyTypeToken;
+ private BiPredicate super E, ? super E> equals;
+ private ToIntFunction super E> hash;
+
+ // #begin CONSTRUCTION
+
+ private EqualityTransformingCollectionBuilder(Class super E> 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 super E> 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 super E, ? super E> 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 super E, ? super E> equals) {
+ Objects.requireNonNull(equals, "The argument 'equals' must not be null.");
+ return withEqualsHandlingNull(makeNullSafe(equals));
+ }
+
+ private static BiPredicate super E, ? super E> makeNullSafe(BiPredicate super E, ? super E> 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 super E> 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 super E> hash) {
+ Objects.requireNonNull(hash, "The argument 'hash' must not be null.");
+ return withHashHandlingNull(makeNullSafe(hash));
+ }
+
+ private static ToIntFunction super E> makeNullSafe(ToIntFunction super E> 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