Skip to content

Commit d62a5e4

Browse files
author
Nicolai Parlog
committed
Basic implementation of 'Nestings' and 'NestedObjectProperty'.
1 parent fef172b commit d62a5e4

17 files changed

+1450
-0
lines changed
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package org.codefx.nesting;
2+
3+
import java.util.List;
4+
import java.util.Objects;
5+
import java.util.function.Function;
6+
7+
import javafx.beans.property.Property;
8+
import javafx.beans.property.ReadOnlyProperty;
9+
import javafx.beans.property.SimpleObjectProperty;
10+
import javafx.beans.value.ChangeListener;
11+
import javafx.beans.value.ObservableValue;
12+
13+
/**
14+
* An implementation of {@link Nesting} which uses an outer {@link ObservableValue} and a series of nested value getters
15+
* to get the {@link #innerProperty()}.
16+
*
17+
* @param <O>
18+
* the hierarchy's innermost type of {@link ObservableValue}
19+
*/
20+
@SuppressWarnings("rawtypes")
21+
final class DeepNesting<O extends ObservableValue<?>> implements Nesting<O> {
22+
23+
//#formatter:off
24+
25+
/*
26+
* GENERIC TYPES
27+
*
28+
* Because the depth of the nesting is variable, the number of involved types is determined at runtime. This class
29+
* can hence not use generics for type safety. So it uses tons of raw types, which only works out if the
30+
* constructor is called with the correctly typed outer observable and "nesting steps".
31+
*
32+
*
33+
* DATA STRUCTURES
34+
*
35+
* This nesting uses arrays to store instances which are needed to resolve the nesting. Those arrays all have the
36+
* same length ('maxLevel') and a uniform structure: the same indices correspond to the same levels in the nesting
37+
* hierarchy.
38+
*
39+
* observables: outer nested ... nested inner
40+
* level: 0 1 ... n-1 n
41+
* getters[]: x x x x // each getter uses values[level] to get new observ.[level + 1]
42+
* observ.[]: x x x x // stored to remove listeners; [0] only stored for uniform loop
43+
* values[]: x x x x // stored to compare values and end loop upon reaching same value
44+
* listeners[]: x x x x // stored to remove and add them
45+
*
46+
*
47+
* BEHAVIOR
48+
*
49+
* TODO: document
50+
*/
51+
52+
//#formatter:on
53+
54+
// #region PROPERTIES
55+
56+
/**
57+
* The level of the nesting, which is also the length of the arrays.
58+
*/
59+
private final int maxLevel;
60+
61+
/**
62+
* The getters which implement the nesting step from one observable value to the next.
63+
*/
64+
private final Function[] nestedObservableGetters;
65+
66+
/**
67+
* The current hierarchy of observable values.
68+
*/
69+
private final ObservableValue[] observables;
70+
71+
/**
72+
* The values currently held by the observable values.
73+
*/
74+
private final Object[] values;
75+
76+
/**
77+
* The change listeners which are added to the observable values.
78+
*/
79+
private final ChangeListener[] changeListeners;
80+
81+
/**
82+
* The property holding the current innermost observable value.
83+
*/
84+
private final Property<O> inner;
85+
86+
//#end PROPERTIES
87+
88+
// #region CONSTRUCTION
89+
90+
/**
91+
* Creates a new deep nesting which depends on the specified outer observable value and uses the specified getters
92+
* for its "nesting steps".
93+
*
94+
* @param outerObservableValue
95+
* the {@link ObservableValue} on which this nesting depends
96+
* @param nestedObservableGetters
97+
* the {@link Function Functions} which perform the "nesting step" from one observable's value to the
98+
* next observable; the getters must be ordered such that:
99+
* <ul>
100+
* <li>the first accepts an argument of the type wrapped by the {@code outerObservableValue}
101+
* <li>each next accepts an argument of the type wrapped by the observable returned by the previous
102+
* getter
103+
* </ul>
104+
* These conditions are not checked by the compiler nor during construction but will later lead to
105+
* {@link ClassCastException ClassCastExceptions}.
106+
*/
107+
public DeepNesting(ObservableValue outerObservableValue, List<Function> nestedObservableGetters) {
108+
Objects.requireNonNull(outerObservableValue, "The argument 'outerObservableValue' must not be null.");
109+
Objects.requireNonNull(nestedObservableGetters, "The argument 'nestedObservableGetters' must not be null.");
110+
if (nestedObservableGetters.size() < 1)
111+
throw new IllegalArgumentException("The list 'nestedObservableGetters' must have at least length 1.");
112+
113+
maxLevel = nestedObservableGetters.size();
114+
115+
this.observables = createObservables(outerObservableValue, maxLevel);
116+
this.values = new Object[maxLevel];
117+
this.nestedObservableGetters = nestedObservableGetters.toArray(new Function[maxLevel]);
118+
this.changeListeners = createChangeListeners(maxLevel);
119+
this.inner = new SimpleObjectProperty<O>(this, "inner");
120+
121+
initializeNesting();
122+
}
123+
124+
/**
125+
* Creates an initialized array of observables. Its first item is the specified outer observable (its other items
126+
* are null).
127+
*
128+
* @param outerObservable
129+
* the outer observable upon which this nesting depends
130+
* @param levels
131+
* the number of levels, which is also the new array's length
132+
* @return an initialized array of {@link ObservableValue ObservableValues}
133+
*/
134+
private static ObservableValue[] createObservables(ObservableValue outerObservable, int levels) {
135+
ObservableValue[] observables = new ObservableValue[levels];
136+
observables[0] = outerObservable;
137+
return observables;
138+
}
139+
140+
/**
141+
* Creates an array of change listeners.
142+
*
143+
* @param levels
144+
* the number of levels, which is also the new array's length
145+
* @return an array of {@link ChangeListener ChangeListeners}
146+
*/
147+
private ChangeListener[] createChangeListeners(int levels) {
148+
ChangeListener[] listeners = new ChangeListener[levels];
149+
for (int level = 0; level < levels; level++) {
150+
final int theLevel = level;
151+
listeners[level] = (observable, oldValue, newValue) -> updateNestingFromLevel(theLevel);
152+
}
153+
return listeners;
154+
}
155+
156+
/**
157+
* Initializes this nesting by filling the arrays {@link #observables} and {@link #values} and adding the
158+
* corresponding {@link #changeListeners changeListener} to each observable.
159+
*/
160+
private void initializeNesting() {
161+
/*
162+
* Simply update the nesting from level 0 on. This only works if certain preconditions are met (which depend on
163+
* the structure of 'updateNestingFromLevel') so make sure to create them first.
164+
*/
165+
initializeNestingLevel0();
166+
updateNestingFromLevel(0);
167+
}
168+
169+
/**
170+
* Creates the preconditions necessary to use {@link #updateNestingFromLevel(int) updateNestingFromLevel(0)} to
171+
* initialize this nesting.
172+
*/
173+
@SuppressWarnings("unchecked")
174+
private void initializeNestingLevel0() {
175+
176+
// WARNING:
177+
// This method is highly coupled to 'updateNestingFromLevel'!
178+
// Make sure to inspect both methods upon changing one of them.
179+
180+
// if 'updateNestingFromLevel' encounters the same value in the 'values' array as in the corresponding property,
181+
// it stops updating so make sure this cannot happen (if the property actually contains null, nothing is left to do)
182+
values[0] = null;
183+
184+
// if 'updateNestingFromLevel' encounters the same property in the 'observables' array
185+
// as on the currently checked level, it does not add a listener so do that here
186+
observables[0].addListener(changeListeners[0]);
187+
}
188+
189+
//#end CONSTRUCTION
190+
191+
/**
192+
* Updates the nesting from the specified level on. This includes moving listeners from old to new observables and
193+
* updating the arrays {@link #observables} and {@link #values}.
194+
*
195+
* @param startLevel
196+
* the level on which to start updating; this will be the one to which the {@link #observables
197+
* observable} which changed its value belongs
198+
*/
199+
@SuppressWarnings("unchecked")
200+
private void updateNestingFromLevel(int startLevel) {
201+
202+
// WARNING:
203+
// This method is highly coupled to 'initializeNestingLevel0'!
204+
// Make sure to inspect both methods upon changing one of them.
205+
206+
int currentLevel = startLevel;
207+
208+
ObservableValue currentObservable = observables[startLevel];
209+
// note that unless the observable has a strange implementation which calls change listeners
210+
// even though nothing changed, this will always be true
211+
boolean currentValueChanged = values[currentLevel] != currentObservable.getValue();
212+
213+
// there is no listener on the inner level's observable so the start level can never be the inner level
214+
boolean currentIsInnerLevel = false;
215+
216+
/*
217+
* Loop through the levels [startLevel; innerLevel - 1] unless a level is found where the stored value equals
218+
* the current one. In that case all higher levels must be identical and nothing more needs to be updated. Note
219+
* that the loop will not stop on null observables and null values - instead it continues and still compares to
220+
* stored values.
221+
*/
222+
while (currentValueChanged && !currentIsInnerLevel) {
223+
// update 'observables' array and move listener from old to new observable;
224+
// (note that the test below is never true for the 'startLevel': the listener to that observable called
225+
// this method because the observable's _value_ changed - hence the observable itself cannot have changed.)
226+
ObservableValue storedObservable = observables[currentLevel];
227+
if (storedObservable != currentObservable) {
228+
observables[currentLevel] = currentObservable;
229+
if (storedObservable != null)
230+
storedObservable.removeListener(changeListeners[currentLevel]);
231+
if (currentObservable != null)
232+
currentObservable.addListener(changeListeners[currentLevel]);
233+
}
234+
235+
// update 'values' array
236+
Object storedValue = values[currentLevel];
237+
Object currentValue = null;
238+
if (currentObservable != null)
239+
currentValue = currentObservable.getValue();
240+
currentValueChanged = storedValue != currentValue;
241+
if (currentValueChanged)
242+
values[currentLevel] = currentValue;
243+
244+
// if the value changed, move to next level
245+
if (currentValueChanged) {
246+
// get the values for the next level ...
247+
ObservableValue nextObservable = null;
248+
if (currentValue != null)
249+
nextObservable = (ObservableValue) nestedObservableGetters[currentLevel].apply(currentValue);
250+
boolean nextIsInnerLevel = (currentLevel + 1 == maxLevel);
251+
252+
// ... assign them ...
253+
currentObservable = nextObservable;
254+
currentIsInnerLevel = nextIsInnerLevel;
255+
256+
// ... and finally increase level counter
257+
currentLevel++;
258+
}
259+
}
260+
261+
// if the loop encountered a level where the stored and the current value are identical,
262+
// all higher levels must be identical so nothing is left to do here
263+
if (!currentValueChanged)
264+
return;
265+
266+
// if the inner level was reached, set its property as the new inner property
267+
if (currentIsInnerLevel)
268+
inner.setValue((O) currentObservable);
269+
}
270+
271+
// #region PROPERTY ACCESS
272+
273+
/**
274+
* {@inheritDoc}
275+
*/
276+
@Override
277+
public ReadOnlyProperty<O> innerProperty() {
278+
return inner;
279+
}
280+
281+
//#end PROPERTY ACCESS
282+
283+
}

src/org/codefx/nesting/Nesting.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.codefx.nesting;
2+
3+
import javafx.beans.property.ReadOnlyProperty;
4+
import javafx.beans.value.ObservableValue;
5+
6+
/**
7+
* A nesting encapsulates a hierarchy of nested {@link ObservableValue ObservableValues}. Its {@link #innerProperty()}
8+
* always contains the current innermost observable in that hierarchy. If some observable or its value were null,
9+
* {@code innerProperty} contains null as well.
10+
* <p>
11+
* Nestings will usually be implemented such that they eagerly evaluate the nested observable.
12+
*
13+
* @param <O>
14+
* the hierarchy's innermost type of {@link ObservableValue}
15+
*/
16+
public interface Nesting<O extends ObservableValue<?>> {
17+
18+
/**
19+
* The current innermost observable in the hierarchy. If some observable or its value were null, this contains null
20+
* as well.
21+
*
22+
* @return the innermost {@link ObservableValue}
23+
*/
24+
ReadOnlyProperty<O> innerProperty();
25+
26+
}

0 commit comments

Comments
 (0)