package org.codefx.libfx.nesting; import java.util.List; import java.util.Objects; import java.util.Optional; import javafx.beans.Observable; import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; /** * An implementation of {@link Nesting} which uses an outer {@link ObservableValue} and a series of nesting steps to get * the {@link #innerObservableProperty() innerObservable}. * * @param <O> * the type of the nesting hierarchy's inner {@link Observable} */ @SuppressWarnings("rawtypes") final class DeepNesting<O extends Observable> implements Nesting<O> { //#formatter:off /* * GENERIC TYPES * * Because the depth of the nesting is not fixed, the number of involved types is determined at runtime. This class * can hence not use generics for type safety. So it uses tons of raw types, which only works out if the constructor * is called with the correctly typed outer observable and nesting steps. * * * DATA STRUCTURES * * This nesting uses arrays to store instances which are needed to resolve the nesting. Those arrays all have the * same length ('maxLevel') and a uniform structure: the same indices correspond to the same levels in the nesting * hierarchy. * * observables: outer nested ... nested inner * level: 0 1 ... n-1 n * steps[]: x x x x // each step uses values[level] to get the new observ.[level + 1] * observ.[]: x x x x // stored to remove listeners; [0] only stored for uniform loop * values[]: x x x x // stored to compare values and end loop upon reaching same value * listeners[]: x x x x // stored to remove and add the listeners * * * BEHAVIOR * * Whenever a listener registers a changing value it calls 'updateNestingFromLevel' with the level on which the * value changed. The method will start on that level and use the nesting steps to get to the higher ones until it * reaches the inner observable which will be stored in 'innerObservable'. Check the method for details. * */ //#formatter:on // #begin PROPERTIES /** * The level of the nesting, which is also the length of the arrays. */ private final int maxLevel; /** * The steps one observable's value to the next. */ private final NestingStep[] nestingSteps; /** * The current hierarchy of observables. */ private final ObservableValue[] observables; /** * The values currently held by the observables. * <p> * Before the initialization these will be non-null "uninitialized" objects. This is done to distinguish them from * null values which could be held by the observables. */ private final Object[] values; /** * The change listeners which are added to the observables. */ private final ChangeListener[] changeListeners; /** * The property holding the current inner observable. */ private final Property<Optional<O>> inner; //#end PROPERTIES // #begin CONSTRUCTION /** * Creates a new deep nesting which depends on the specified outer observable and uses specified nesting steps. * * @param outerObservable * the {@link ObservableValue} on which this nesting depends * @param nestingSteps * the {@link NestingStep NestingSteps} from one observable's value to the next observable; they must be * ordered such that: * <ul> * <li>the first accepts an argument of the type wrapped by the {@code outerObservable} and returns an * {@link ObservableValue} * <li>each next accepts an argument of the type wrapped by the observable returned by the step before * and returns an {@link ObservableValue} * <li>only the last step might return an {@link Observable} * </ul> * These conditions are not checked by the compiler nor during construction. Violations will later lead * to {@link ClassCastException ClassCastExceptions}. * @throws IllegalArgumentException * if the list is empty */ public DeepNesting(ObservableValue outerObservable, List<NestingStep> nestingSteps) { Objects.requireNonNull(outerObservable, "The argument 'outerObservable' must not be null."); Objects.requireNonNull(nestingSteps, "The argument 'nestedObservableGetters' must not be null."); if (nestingSteps.size() < 1) throw new IllegalArgumentException("The list 'nestedObservableGetters' must have at least length 1."); maxLevel = nestingSteps.size(); this.observables = createObservables(outerObservable, maxLevel); this.values = createUnitializedValues(); this.nestingSteps = nestingSteps.toArray(new NestingStep[maxLevel]); this.changeListeners = createChangeListeners(maxLevel); this.inner = new SimpleObjectProperty<>(this, "inner"); initializeNesting(); } /** * @return an array of uninitialized values (i.e. non-null values which do not equal any other instances occurring * "in the wild") */ private Object[] createUnitializedValues() { Object[] values = new Object[maxLevel]; for (int i = 0; i < maxLevel; i++) values[i] = new Unitialized(); return values; } /** * Creates an initialized array of observables. Its first item is the specified outer observable (its other items * are null). * * @param outerObservable * the outer observable upon which this nesting depends * @param levels * the number of levels, which is also the new array's length * @return an initialized array of {@link ObservableValue ObservableValues} */ private static ObservableValue[] createObservables(ObservableValue outerObservable, int levels) { ObservableValue[] observables = new ObservableValue[levels]; observables[0] = outerObservable; return observables; } /** * Creates an array of change listeners. * * @param levels * the number of levels, which is also the new array's length * @return an array of {@link ChangeListener ChangeListeners} */ private ChangeListener[] createChangeListeners(int levels) { ChangeListener[] listeners = new ChangeListener[levels]; for (int level = 0; level < levels; level++) { final int theLevel = level; listeners[level] = (observable, oldValue, newValue) -> updateNestingFromLevel(theLevel); } return listeners; } /** * Initializes this nesting by filling the arrays {@link #observables} and {@link #values} and adding the * corresponding {@link #changeListeners changeListener} to each observable. */ private void initializeNesting() { new NestingInitializer().initialize(); } //#end CONSTRUCTION /** * Updates the nesting from the specified level on. This includes moving listeners from old to new observables and * updating the arrays {@link #observables} and {@link #values}. * * @param startLevel * the level on which to start updating; this will be the one to which the {@link #observables * observable} which changed its value belongs */ private void updateNestingFromLevel(int startLevel) { new NestingUpdater(startLevel).update(); } // #begin ACCESSORS /** * {@inheritDoc} */ @Override public ReadOnlyProperty<Optional<O>> innerObservableProperty() { return inner; } //#end ACCESSORS // #begin PRIVATE CLASSES /** * Initializes {@link DeepNesting#observables}, {@link DeepNesting#values} and {@link DeepNesting#inner} as well as * adding {@link DeepNesting#changeListeners} to all observables. */ private class NestingInitializer { /** * Initializes the {@code DeepNesting} by filling the arrays {@link DeepNesting#observables} and * {@link DeepNesting#values}, setting {@link DeepNesting#inner} and adding the corresponding * {@link DeepNesting#changeListeners} to each observable. */ @SuppressWarnings("unchecked") public void initialize() { // WARNING: // This method is highly coupled to 'NestingUpdater.updateCurrentLevel'! // Make sure to inspect both methods upon changing one of them. /* * Simply update the nesting from level 0 on. But if the updater encounters the same property in the * 'observables' array as on the currently checked level, it does not add a listener so do that here. */ observables[0].addListener(changeListeners[0]); new NestingUpdater(0).update(); } } /** * Updates the {@code DeepNesting} when an observable in the nesting hierarchy changes its value - the level on * which the change occurred (i.e. the 'startLevel') is specified during construction. * <p> * The updater loops through the levels {@code [startLevel; innerLevel - 1]}, updates {@code observables} and * {@code values} and moves the {@link DeepNesting#changeListeners} from the old to the new observables. It stops * when a level is found where the stored value equals the current one. In that case all higher levels must be * identical and nothing more needs to be updated. * <p> * Note that the loop will not stop on null observables and null values. Instead it continues and replaces all * stored observables and values with null. This is the desired behavior as the hierarchy is now in an incomplete * state where the old observables and values are obsolete and have to be replaced. */ private class NestingUpdater { /** * The level the updater is currently working on. */ private int currentLevel; /** * Indicates whether the {@link #currentLevel} is the inner level. */ private boolean currentLevelIsInnerLevel; /** * The {@link ObservableValue} on the {@link #currentLevel}. */ private ObservableValue currentObservable; /** * The {@link #currentObservable}'s value. */ private Object currentValue; /** * Indicates whether the {@link #currentValue} differs from the value stored in {@link DeepNesting#values}. */ private boolean currentValueChanged; /** * The observable on the inner level. Must be stored separately because {@link DeepNesting#observables} only * accepts {@link ObservableValue ObservableValues}, which is also the reason why it is too short to also hold * the inner observable. */ private Observable innerObservable; /** * Creates a new updater which starts updating on the specified level. * * @param startLevel * the level on which this updater starts updating */ public NestingUpdater(int startLevel) { currentLevel = startLevel; // there is no listener on the inner level's observable so the start level can never be the inner level currentLevelIsInnerLevel = false; currentObservable = observables[startLevel]; currentValue = currentObservable.getValue(); // note that unless the observable has a strange implementation which calls change listeners // even though nothing changed, this will always be true currentValueChanged = values[currentLevel] != currentObservable.getValue(); } /** * Updates the nesting from the {@link #currentLevel} on. */ public void update() { while (mustUpdateCurrentLevel()) { updateCurrentLevel(); moveToNextLevel(); } updateInnerObservable(); } /** * Indicates whether the current level must be updated. * * @return true if the {@link #currentLevel} must be updated */ private boolean mustUpdateCurrentLevel() { return currentValueChanged && !currentLevelIsInnerLevel; } /** * Updates the {@link DeepNesting#observables} and {@link DeepNesting#values} on the {@link #currentLevel}. */ private void updateCurrentLevel() { // WARNING: // This method is highly coupled to 'NestingInitializer.initializeNestingLevel0'! // Make sure to inspect both methods upon changing one of them. updateObservableOnCurrentLevel(); updateValueOnCurrentLevel(); } /** * Updates {@link DeepNesting#observables}[{@link #currentLevel}] to {@link #currentObservable} and moves the * listener from the old to the new observable. */ @SuppressWarnings("unchecked") private void updateObservableOnCurrentLevel() { ObservableValue storedObservable = DeepNesting.this.observables[currentLevel]; if (storedObservable != currentObservable) { DeepNesting.this.observables[currentLevel] = currentObservable; if (storedObservable != null) storedObservable.removeListener(changeListeners[currentLevel]); if (currentObservable != null) currentObservable.addListener(changeListeners[currentLevel]); } } /** * Updates {@link #currentValue} and {@link #currentValueChanged} and sets {@link DeepNesting#values}[ * {@link #currentLevel}] to {@link #currentValue}. */ private void updateValueOnCurrentLevel() { if (currentObservable == null) currentValue = null; else currentValue = currentObservable.getValue(); Object storedValue = DeepNesting.this.values[currentLevel]; currentValueChanged = storedValue != currentValue; if (currentValueChanged) DeepNesting.this.values[currentLevel] = currentValue; } /** * Moves to the next level by updating {@link #currentLevel}, {@link #currentLevelIsInnerLevel}, * {@link #currentObservable} and possibly {@link #innerObservable}. */ @SuppressWarnings("unchecked") private void moveToNextLevel() { Observable nextObservable = null; if (currentValue != null) nextObservable = nestingSteps[currentLevel].step(currentValue); boolean nextIsInnerLevel = (currentLevel + 1 == maxLevel); // ... assign them ... if (nextIsInnerLevel) { currentLevelIsInnerLevel = true; innerObservable = nextObservable; } else { currentLevelIsInnerLevel = false; // only the last nesting step is allowed to return an 'Observable' currentObservable = (ObservableValue) nextObservable; } // ... and finally increase level counter currentLevel++; } /** * Updates {@link #innerObservable} if the loop reached it. */ @SuppressWarnings("unchecked") private void updateInnerObservable() { // if the loop encountered a level where the stored and the current value are identical, // all higher levels are identical as well and the inner observable can not have changed if (currentLevelIsInnerLevel) { Optional innerObservableOptional = Optional.ofNullable(innerObservable); inner.setValue(innerObservableOptional); } } } /** * Represents an uninitialized entry in the {@link #values} array. */ private static class Unitialized { // no body needed } //#end PRIVATE CLASSES }