|
| 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 | +} |
0 commit comments