TreePathLens.java
package swingtree;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sprouts.Lens;
import sprouts.Tuple;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
/**
* A {@link Lens} focusing on a single node inside a deeply nested, immutable tree,
* identified by the path of ids leading down to it. Reading walks down the path by id;
* writing walks down and rebuilds the chain bottom up, so a change nine levels deep
* produces one new bound value with everything off the path shared rather than copied.
* <p>
* The descent begins at the top level nodes, which {@link TreeRoots} reads out of the one
* value the tree is bound to: one root node for a tree, a whole tuple of them for a forest.
* The first id of the path therefore names a top level node in both forms.
* <p>
* Two properties of this lens matter a great deal:
* <ul>
* <li><b>It never touches the UI, nor anything the UI thread owns.</b> It needs only
* the bound <i>value</i> and the {@link TreeConf}, whose rule lookup is concurrent, so
* it may be evaluated on any thread. That is essential, because it runs whenever the
* bound property changes, which under a decoupled UI is the application thread.</li>
* <li><b>It fails softly.</b> A node which has vanished from the tree yields the last
* value this lens did resolve, and a write into it is dropped. A lens whose node is
* gone may still be bound to a view on its way out, and disturbing that view is worse
* than doing nothing. The tuple item lens follows the same policy.</li>
* </ul>
*
* @param <I> The identity type of the nodes, which the path is made of.
* @param <N> The common node type of the tree.
* @param <R> The type of the value the tree is bound to: one node, or a tuple of them.
*/
final class TreePathLens<I, N, R> implements Lens<R, N>
{
private static final Logger log = LoggerFactory.getLogger(TreePathLens.class);
private final TreeConf<I, N> _conf;
private final TreeRoots<N, R> _roots;
private final Object[] _idPath;
private final AtomicReference<@Nullable N> _lastResolved;
TreePathLens( TreeConf<I, N> conf, TreeRoots<N, R> roots, Object[] idPath, @Nullable N initial ) {
_conf = Objects.requireNonNull(conf);
_roots = Objects.requireNonNull(roots);
_idPath = Objects.requireNonNull(idPath);
_lastResolved = new AtomicReference<>(initial);
}
@Override
public N getter( R boundValue ) {
try {
@Nullable N found = _resolve(boundValue);
if ( found != null )
_lastResolved.set(found);
else
found = _lastResolved.get();
return NullUtil.fakeNonNull(found);
} catch (Exception e) {
log.debug(SwingTree.get().logMarker(), "Failed to resolve a tree node lens.", e);
return NullUtil.fakeNonNull(_lastResolved.get());
}
}
@Override
public R wither( R boundValue, N newNode ) {
if ( _idPath.length == 0 )
return boundValue; // Nothing is focused, so there is nothing to write into.
try {
Tuple<N> roots = _roots.of(boundValue);
int rootIndex = _indexOfIdIn(roots, _idPath[0]);
if ( rootIndex < 0 )
return boundValue;
List<Step> steps = _walk(roots.get(rootIndex));
if ( steps == null )
return boundValue; // The node is gone, so the write no longer applies.
Object rebuilt = newNode;
for ( int i = steps.size() - 1; i >= 0; i-- ) {
Step step = steps.get(i);
if ( !step.rule.isStructurallyWritable() ) {
_warnAboutReadOnlyBranch(step);
return boundValue;
}
Tuple<Object> updatedChildren = step.children.setAt(step.index, rebuilt);
rebuilt = step.rule.withChildren(step.parent, updatedChildren);
}
_lastResolved.set(newNode);
return _roots.with(boundValue, roots.setAt(rootIndex, _conf.nodeType().cast(rebuilt)));
} catch (Exception e) {
log.debug(SwingTree.get().logMarker(), "Failed to write through a tree node lens.", e);
return boundValue;
}
}
private @Nullable N _resolve( @Nullable R boundValue ) {
if ( _idPath.length == 0 )
return null;
Tuple<N> roots = _roots.of(boundValue);
int rootIndex = _indexOfIdIn(roots, _idPath[0]);
if ( rootIndex < 0 )
return null; // Not a top level node of this value, so this path means nothing in it.
Object current = roots.get(rootIndex);
for ( int level = 1; level < _idPath.length; level++ ) {
TreeNodeConf<N, ?> rule = _conf.ruleFor(current);
if ( rule == null || !rule.hasChildrenRule() )
return null;
Tuple<Object> children = rule.childrenOf(current);
int index = _indexOfIdIn(children, _idPath[level]);
if ( index < 0 )
return null;
current = children.get(index);
}
return _conf.nodeType().cast(current);
}
private @Nullable List<Step> _walk( Object topLevelNode ) {
List<Step> steps = new ArrayList<>(_idPath.length - 1);
Object current = topLevelNode;
for ( int level = 1; level < _idPath.length; level++ ) {
TreeNodeConf<N, ?> rule = _conf.ruleFor(current);
if ( rule == null || !rule.hasChildrenRule() )
return null;
Tuple<Object> children = rule.childrenOf(current);
int index = _indexOfIdIn(children, _idPath[level]);
if ( index < 0 )
return null;
steps.add(new Step(rule, current, children, index));
current = children.get(index);
}
return steps;
}
private int _indexOfIdIn( Tuple<?> children, Object id ) {
for ( int i = 0; i < children.size(); i++ )
if ( Objects.equals(_conf.idOf(children.get(i)), id) )
return i;
return -1;
}
/**
* The dedup lives on the configuration rather than on this lens, because a lens is
* built afresh for every single edit, so a per lens flag would repeat the same
* complaint on every keystroke that ends one.
*/
private void _warnAboutReadOnlyBranch( Step step ) {
Class<?> branchType = step.parent.getClass();
if ( !_conf.shouldWarnAbout(branchType) )
return;
log.warn(SwingTree.get().logMarker(),
"Dropping a write to a tree node, because the branch of type '{}' above it was declared " +
"with a read only 'children(getter)' rule. Declare it as 'children(getter, wither)' to " +
"let edits below it travel back up into the bound property.",
branchType.getSimpleName()
);
}
/** One level of the descent from a top level node down to the focused node. */
private static final class Step {
final TreeNodeConf<?, ?> rule;
final Object parent;
final Tuple<Object> children;
final int index;
Step( TreeNodeConf<?, ?> rule, Object parent, Tuple<Object> children, int index ) {
this.rule = rule;
this.parent = parent;
this.children = children;
this.index = index;
}
}
}