You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2023/01/23 13:04:08 UTC
[isis] branch master updated: ISIS-3325: move nested classes from CausewayToWicketTreeAdapter to their own files
This is an automated email from the ASF dual-hosted git repository.
ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git
The following commit(s) were added to refs/heads/master by this push:
new 466cabc5c7 ISIS-3325: move nested classes from CausewayToWicketTreeAdapter to their own files
466cabc5c7 is described below
commit 466cabc5c7bfb0a8908c6830c37576344379e2af
Author: andi-huber <ah...@apache.org>
AuthorDate: Mon Jan 23 14:03:58 2023 +0100
ISIS-3325: move nested classes from CausewayToWicketTreeAdapter to their
own files
---
.../tree/CausewayToWicketTreeAdapter.java | 441 +++------------------
.../tree/_LoadableDetachableTreeModel.java | 103 +++++
.../ui/components/tree/_TreeExpansionModel.java | 87 ++++
.../wicket/ui/components/tree/_TreeModel.java | 57 +++
.../ui/components/tree/_TreeModelTreeAdapter.java | 120 ++++++
.../ui/components/tree/_TreeModelTreeProvider.java | 73 ++++
6 files changed, 492 insertions(+), 389 deletions(-)
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/CausewayToWicketTreeAdapter.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/CausewayToWicketTreeAdapter.java
index 6849700fe6..830ca9bd3e 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/CausewayToWicketTreeAdapter.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/CausewayToWicketTreeAdapter.java
@@ -19,14 +19,7 @@
package org.apache.causeway.viewer.wicket.ui.components.tree;
import java.io.Serializable;
-import java.util.Iterator;
-import java.util.NoSuchElementException;
-import java.util.Objects;
import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
@@ -37,49 +30,36 @@ import org.apache.wicket.extensions.markup.html.repeater.tree.NestedTree;
import org.apache.wicket.extensions.markup.html.repeater.tree.Node;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.model.IModel;
-import org.apache.wicket.model.LoadableDetachableModel;
-import org.apache.wicket.model.Model;
-import org.apache.causeway.applib.graph.tree.TreeAdapter;
import org.apache.causeway.applib.graph.tree.TreeNode;
-import org.apache.causeway.applib.graph.tree.TreePath;
-import org.apache.causeway.applib.services.bookmark.Bookmark;
-import org.apache.causeway.applib.services.factory.FactoryService;
-import org.apache.causeway.commons.functional.IndexedFunction;
-import org.apache.causeway.commons.internal.collections._Lists;
import org.apache.causeway.core.metamodel.context.MetaModelContext;
import org.apache.causeway.core.metamodel.object.ManagedObject;
-import org.apache.causeway.core.metamodel.object.ManagedObjects;
import org.apache.causeway.viewer.wicket.model.models.ScalarModel;
-import org.apache.causeway.viewer.wicket.model.models.UiObjectWkt;
import org.apache.causeway.viewer.wicket.model.models.ValueModel;
-import org.apache.causeway.viewer.wicket.model.util.WktContext;
import org.apache.causeway.viewer.wicket.ui.components.entity.icontitle.EntityIconAndTitlePanel;
-import lombok.NonNull;
import lombok.val;
class CausewayToWicketTreeAdapter {
+ /**
+ * @param valueModel - holder of {@link TreeNode}
+ */
public static Component adapt(final String id, final ValueModel valueModel) {
- if(valueModel==null || valueModel.getObject()==null) {
- return emptyTreeComponent(id);
- }
- val commonContext = valueModel.getMetaModelContext();
- val treeNode = valueModel.getObject();
- return new EntityTree(id, toITreeProvider(commonContext, treeNode),
- toIModelRepresentingCollapseExpandState(commonContext, treeNode));
+ return valueModel==null
+ || valueModel.getObject()==null
+ ? emptyTreeComponent(id)
+ : EntityTree.of(id, valueModel.getObject(), valueModel.getMetaModelContext());
}
+ /**
+ * @param scalarModel - holder of {@link TreeNode}
+ */
public static Component adapt(final String id, final ScalarModel scalarModel) {
- if(scalarModel==null || scalarModel.getObject()==null) {
- return emptyTreeComponent(id);
- }
- val commonContext = scalarModel.getMetaModelContext();
- val treeNode = scalarModel.getObject();
- return new EntityTree(id,
- toITreeProvider(commonContext, treeNode),
- toIModelRepresentingCollapseExpandState(commonContext, treeNode));
+ return scalarModel==null
+ || scalarModel.getObject()==null
+ ? emptyTreeComponent(id)
+ : EntityTree.of(id, scalarModel.getObject(), scalarModel.getMetaModelContext());
}
// -- FALLBACK
@@ -93,14 +73,34 @@ class CausewayToWicketTreeAdapter {
/**
* Wicket's Tree Component implemented for Causeway
*/
- private static class EntityTree extends NestedTree<TreeModel> {
+ private static class EntityTree extends NestedTree<_TreeModel> {
private static final long serialVersionUID = 1L;
- public EntityTree(
+ public static EntityTree of(
+ final String id, final ManagedObject treeNodeObject, final MetaModelContext mmc) {
+
+ val treeNode = (TreeNode<?>) treeNodeObject.getPojo();
+ val treeAdapterClass = treeNode.getTreeAdapterClass();
+
+ val wrappingTreeAdapter = new _TreeModelTreeAdapter(mmc, treeAdapterClass);
+
+ val treeModelTreeProvider = new _TreeModelTreeProvider(
+ wrappingTreeAdapter.wrap(treeNode.getValue(), treeNode.getPositionAsPath()),
+ wrappingTreeAdapter);
+
+ val treeExpansionModel = _TreeExpansionModel.of(mmc,
+ treeNode.getTreeState().getExpandedNodePaths());
+
+ return new EntityTree(id,
+ treeModelTreeProvider,
+ treeExpansionModel);
+ }
+
+ private EntityTree(
final String id,
- final ITreeProvider<TreeModel> provider,
- final TreeExpansionModel collapseExpandState) {
+ final ITreeProvider<_TreeModel> provider,
+ final _TreeExpansionModel collapseExpandState) {
super(id, provider, collapseExpandState);
}
@@ -108,8 +108,8 @@ class CausewayToWicketTreeAdapter {
* To use a custom component for the representation of a node's content we override this method.
*/
@Override
- protected Component newContentComponent(final String id, final IModel<TreeModel> node) {
- final TreeModel treeModel = node.getObject();
+ protected Component newContentComponent(final String id, final IModel<_TreeModel> node) {
+ final _TreeModel treeModel = node.getObject();
final Component entityIconAndTitle = new EntityIconAndTitlePanel(id, treeModel);
return entityIconAndTitle;
}
@@ -118,20 +118,20 @@ class CausewayToWicketTreeAdapter {
* To hardcode Node's <pre>AjaxFallbackLink.isEnabledInHierarchy()->true</pre> we override this method.
*/
@Override
- public Component newNodeComponent(final String id, final IModel<TreeModel> model) {
+ public Component newNodeComponent(final String id, final IModel<_TreeModel> model) {
- final Node<TreeModel> node = new Node<TreeModel>(id, this, model) {
+ final Node<_TreeModel> node = new Node<_TreeModel>(id, this, model) {
private static final long serialVersionUID = 1L;
@Override
- protected Component createContent(final String id, final IModel<TreeModel> model) {
+ protected Component createContent(final String id, final IModel<_TreeModel> model) {
return EntityTree.this.newContentComponent(id, model);
}
@Override
protected MarkupContainer createJunctionComponent(final String id) {
- final Node<TreeModel> node = this;
+ final Node<_TreeModel> node = this;
final Runnable toggleExpandCollapse = (Runnable & Serializable) this::toggle;
return new AjaxFallbackLink<Void>(id) {
@@ -168,9 +168,8 @@ class CausewayToWicketTreeAdapter {
* we override this method.
*/
@Override
- public State getState(final TreeModel t) {
- final TreeExpansionModel treeExpansionModel = (TreeExpansionModel) getModel();
- return treeExpansionModel.contains(t.getTreePath()) ? State.EXPANDED : State.COLLAPSED;
+ public State getState(final _TreeModel t) {
+ return treeExpansionModel().contains(t.getTreePath()) ? State.EXPANDED : State.COLLAPSED;
}
/**
@@ -178,9 +177,8 @@ class CausewayToWicketTreeAdapter {
* we override this method.
*/
@Override
- public void expand(final TreeModel t) {
- final TreeExpansionModel treeExpansionModel = (TreeExpansionModel) getModel();
- treeExpansionModel.onExpand(t);
+ public void expand(final _TreeModel t) {
+ treeExpansionModel().onExpand(t);
super.expand(t);
}
@@ -189,348 +187,13 @@ class CausewayToWicketTreeAdapter {
* we override this method.
*/
@Override
- public void collapse(final TreeModel t) {
- final TreeExpansionModel treeExpansionModel = (TreeExpansionModel) getModel();
- treeExpansionModel.onCollapse(t);
+ public void collapse(final _TreeModel t) {
+ treeExpansionModel().onCollapse(t);
super.collapse(t);
}
- }
-
- // -- CAUSEWAY' TREE-MODEL
-
- /**
- * Extending the UiObjectWkt to also provide a TreePath.
- */
- private static class TreeModel extends UiObjectWkt {
- private static final long serialVersionUID = 8916044984628849300L;
-
- private final TreePath treePath;
- private final boolean isTreePathModelOnly;
-
- public TreeModel(final MetaModelContext commonContext, final TreePath treePath) {
- super(commonContext, commonContext.getObjectManager().adapt(0)); // any bookmarkable will do
- this.treePath = treePath;
- this.isTreePathModelOnly = true;
- }
-
- public TreeModel(final MetaModelContext commonContext, final ManagedObject adapter, final TreePath treePath) {
- super(commonContext, Objects.requireNonNull(adapter));
- this.treePath = treePath;
- this.isTreePathModelOnly = false;
- }
-
- public TreePath getTreePath() {
- return treePath;
- }
-
- public boolean isTreePathModelOnly() {
- return isTreePathModelOnly;
- }
-
- }
-
- // -- CAUSEWAY' TREE ADAPTER (FOR TREES OF TREE-MODEL NODES)
-
- /**
- * TreeAdapter for TreeModel nodes.
- */
- @SuppressWarnings({"rawtypes", "unchecked"})
- private static class TreeModelTreeAdapter implements TreeAdapter<TreeModel>, Serializable {
- private static final long serialVersionUID = 1L;
-
- private final Class<? extends TreeAdapter> treeAdapterClass;
-
- private transient TreeAdapter wrappedTreeAdapter;
- private transient MetaModelContext commonContext;
- private transient FactoryService factoryService;
- private transient Function<Object, ManagedObject> pojoToAdapter;
-
-
- private TreeModelTreeAdapter(
- final MetaModelContext commonContext,
- final Class<? extends TreeAdapter> treeAdapterClass) {
-
- this.treeAdapterClass = treeAdapterClass;
- init(commonContext);
- }
-
- private void init(final MetaModelContext commonContext) {
- this.commonContext = commonContext;
- this.factoryService = commonContext.lookupServiceElseFail(FactoryService.class);
- this.pojoToAdapter = pojo ->
- ManagedObject.adaptSingular(commonContext.getSpecificationLoader(), pojo);
- }
-
- private TreeAdapter wrappedTreeAdapter() {
- if(wrappedTreeAdapter!=null) {
- return wrappedTreeAdapter;
- }
- try {
- ensureInit(); // in case we were de-serialzed
- return wrappedTreeAdapter = factoryService.getOrCreate(treeAdapterClass);
- } catch (Exception e) {
- throw new RuntimeException("failed to instantiate tree adapter", e);
- }
- }
-
- @Override
- public Optional<TreeModel> parentOf(final TreeModel treeModel) {
- if(treeModel==null) {
- return Optional.empty();
- }
- return wrappedTreeAdapter().parentOf(unwrap(treeModel))
- .map(pojo->wrap(pojo, treeModel.getTreePath().getParentIfAny()));
- }
-
- @Override
- public int childCountOf(final TreeModel treeModel) {
- if(treeModel==null) {
- return 0;
- }
- return wrappedTreeAdapter().childCountOf(unwrap(treeModel));
- }
-
- @Override
- public Stream<TreeModel> childrenOf(final TreeModel treeModel) {
- if(treeModel==null) {
- return Stream.empty();
- }
- return wrappedTreeAdapter().childrenOf(unwrap(treeModel))
- .map(newPojoToTreeModelMapper(treeModel));
- }
-
- private TreeModel wrap(final @NonNull Object pojo, final TreePath treePath) {
- ensureInit(); // in case we were de-serialzed
- val objectAdapter = pojoToAdapter.apply(pojo);
- return new TreeModel(commonContext, objectAdapter, treePath);
- }
-
- private Object unwrap(final TreeModel model) {
- Objects.requireNonNull(model);
- return model.getObject().getPojo();
- }
-
- private Function<Object, TreeModel> newPojoToTreeModelMapper(final TreeModel parent) {
- return IndexedFunction.zeroBased((indexWithinSiblings, pojo)->
- wrap(pojo, parent.getTreePath().append(indexWithinSiblings)));
- }
-
- // in case we were de-serialzed
- private void ensureInit() {
- if(commonContext!=null) return;
- init(WktContext.getMetaModelContext());
- }
-
- }
-
- // -- WICKET'S TREE PROVIDER (FOR TREES OF TREE-MODEL NODES)
-
- /**
- * Wicket's ITreeProvider implemented for Causeway
- */
- private static class TreeModelTreeProvider implements ITreeProvider<TreeModel> {
-
- private static final long serialVersionUID = 1L;
-
- /**
- * tree's root
- */
- private final TreeModel primaryValue;
- private final TreeModelTreeAdapter treeAdapter;
-
- private TreeModelTreeProvider(final TreeModel primaryValue, final TreeModelTreeAdapter treeAdapter) {
- this.primaryValue = primaryValue;
- this.treeAdapter = treeAdapter;
- }
-
- @Override
- public void detach() {
- }
-
- @Override
- public Iterator<? extends TreeModel> getRoots() {
- return _Lists.singleton(primaryValue).iterator();
- }
-
- @Override
- public boolean hasChildren(final TreeModel node) {
- return treeAdapter.childCountOf(node)>0;
- }
-
- @Override
- public Iterator<? extends TreeModel> getChildren(final TreeModel node) {
- return treeAdapter.childrenOf(node).iterator();
- }
-
- @Override
- public IModel<TreeModel> model(final TreeModel treeModel) {
- return treeModel.isTreePathModelOnly()
- ? Model.of(treeModel)
- : new LoadableDetachableTreeModel(treeModel);
- }
-
- }
-
- /**
- * @return Wicket's ITreeProvider
- */
- @SuppressWarnings({ "rawtypes", "unchecked" })
- private static ITreeProvider<TreeModel> toITreeProvider(
- final MetaModelContext commonContext,
- final ManagedObject treeNodeObject) {
-
- val treeNode = (TreeNode) treeNodeObject.getPojo();
- val treeAdapterClass = treeNode.getTreeAdapterClass();
-
- val wrappingTreeAdapter = new TreeModelTreeAdapter(commonContext, treeAdapterClass);
-
- return new TreeModelTreeProvider(
- wrappingTreeAdapter.wrap(treeNode.getValue(), treeNode.getPositionAsPath()),
- wrappingTreeAdapter);
- }
-
- // -- WICKET'S LOADABLE/DETACHABLE MODEL FOR TREE-MODEL NODES
-
- /**
- * Wicket's loadable/detachable model for TreeModel nodes.
- */
- private static class LoadableDetachableTreeModel extends LoadableDetachableModel<TreeModel> {
- private static final long serialVersionUID = 1L;
-
- private final Bookmark bookmark;
- private final TreePath treePath;
- private final int hashCode;
-
- private transient MetaModelContext commonContext;
-
- public LoadableDetachableTreeModel(final TreeModel tModel) {
- super(tModel);
- this.treePath = tModel.getTreePath();
- this.bookmark = ManagedObjects.bookmarkElseFail(tModel.getObject());
-
- this.hashCode = Objects.hash(bookmark.hashCode(), treePath.hashCode());
- this.commonContext = tModel.getMetaModelContext();
- }
-
- /*
- * loads EntityModel using Oid (id)
- */
- @Override
- protected TreeModel load() {
-
- commonContext = WktContext.computeIfAbsent(commonContext);
-
- val oid = bookmark;
- val objAdapter = commonContext.getMetaModelContext().getObjectManager()
- .loadObject(oid)
- .orElseThrow(()->new NoSuchElementException(
- String.format("Tree creation: could not recreate TreeModel from Bookmark: '%s'", bookmark)));
-
- final Object pojo = objAdapter.getPojo();
- if(pojo==null) {
- throw new NoSuchElementException(
- String.format("Tree creation: could not recreate Pojo from Oid: '%s'", bookmark));
- }
-
- return new TreeModel(commonContext, objAdapter, treePath);
- }
-
- /*
- * Important! Models must be identifiable by their contained object. Also IDs must be
- * unique within a tree structure.
- */
- @Override
- public boolean equals(final Object obj) {
- if (obj instanceof LoadableDetachableTreeModel) {
- final LoadableDetachableTreeModel other = (LoadableDetachableTreeModel) obj;
- return treePath.equals(other.treePath) && bookmark.equals(other.bookmark);
- }
- return false;
- }
-
- /*
- * Important! Models must be identifiable by their contained object.
- */
- @Override
- public int hashCode() {
- return hashCode;
- }
- }
-
- // -- COLLAPSE/EXPAND
-
- /**
- *
- * @param actionOrPropertyModel
- * @return Wicket's model for collapse/expand state
- */
- @SuppressWarnings({ "rawtypes" })
- private static TreeExpansionModel toIModelRepresentingCollapseExpandState(
- final MetaModelContext commonContext,
- final ManagedObject treeNodeObject) {
-
- val treeNode = (TreeNode) treeNodeObject.getPojo();
- val treeState = treeNode.getTreeState();
- return TreeExpansionModel.of(commonContext, treeState.getExpandedNodePaths());
- }
-
- /**
- * Wicket's model for collapse/expand state
- */
- private static class TreeExpansionModel implements IModel<Set<TreeModel>> {
- private static final long serialVersionUID = 648152234030889164L;
-
- public static TreeExpansionModel of(
- final MetaModelContext commonContext,
- final Set<TreePath> expandedTreePaths) {
-
- return new TreeExpansionModel(commonContext, expandedTreePaths);
- }
-
- /**
- * Happens on user interaction via UI.
- * @param t
- */
- public void onExpand(final TreeModel t) {
- expandedTreePaths.add(t.getTreePath());
- }
-
- /**
- * Happens on user interaction via UI.
- * @param t
- */
- public void onCollapse(final TreeModel t) {
- expandedTreePaths.remove(t.getTreePath());
- }
-
- public boolean contains(final TreePath treePath) {
- return expandedTreePaths.contains(treePath);
- }
-
- private final Set<TreePath> expandedTreePaths;
- private final Set<TreeModel> expandedNodes;
-
- private TreeExpansionModel(
- final MetaModelContext commonContext,
- final Set<TreePath> expandedTreePaths) {
-
- this.expandedTreePaths = expandedTreePaths;
- this.expandedNodes = expandedTreePaths.stream()
- .map(tPath->new TreeModel(commonContext, tPath))
- .collect(Collectors.toSet());
- }
-
- @Override
- public Set<TreeModel> getObject() {
- return expandedNodes;
- }
-
- @Override
- public String toString() {
- return "{" + expandedTreePaths.stream()
- .map(TreePath::toString)
- .collect(Collectors.joining(", ")) + "}";
+ private _TreeExpansionModel treeExpansionModel() {
+ return (_TreeExpansionModel) getModel();
}
}
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/_LoadableDetachableTreeModel.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/_LoadableDetachableTreeModel.java
new file mode 100644
index 0000000000..d7d50156dd
--- /dev/null
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/_LoadableDetachableTreeModel.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.wicket.ui.components.tree;
+
+import java.util.NoSuchElementException;
+import java.util.Objects;
+
+import org.apache.wicket.model.LoadableDetachableModel;
+
+import org.apache.causeway.applib.graph.tree.TreePath;
+import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.core.metamodel.context.HasMetaModelContext;
+import org.apache.causeway.core.metamodel.context.MetaModelContext;
+import org.apache.causeway.core.metamodel.object.ManagedObjects;
+import org.apache.causeway.viewer.wicket.model.util.WktContext;
+
+import lombok.val;
+
+/**
+ * Wicket's loadable/detachable model for TreeModel nodes.
+ */
+class _LoadableDetachableTreeModel
+extends LoadableDetachableModel<_TreeModel>
+implements HasMetaModelContext {
+ private static final long serialVersionUID = 1L;
+
+ private final Bookmark bookmark;
+ private final TreePath treePath;
+ private final int hashCode;
+
+ private transient MetaModelContext metaModelContext;
+
+ public _LoadableDetachableTreeModel(final _TreeModel tModel) {
+ super(tModel);
+ this.treePath = tModel.getTreePath();
+ this.bookmark = ManagedObjects.bookmarkElseFail(tModel.getObject());
+
+ this.hashCode = Objects.hash(bookmark.hashCode(), treePath.hashCode());
+ this.metaModelContext = tModel.getMetaModelContext();
+ }
+
+ /*
+ * loads EntityModel using Oid (id)
+ */
+ @Override
+ protected _TreeModel load() {
+
+ val objAdapter = getObjectManager()
+ .loadObject(bookmark)
+ .orElseThrow(()->new NoSuchElementException(
+ String.format("Tree creation: could not recreate TreeModel from Bookmark: '%s'", bookmark)));
+
+ final Object pojo = objAdapter.getPojo();
+ if(pojo==null) {
+ throw new NoSuchElementException(
+ String.format("Tree creation: could not recreate Pojo from Oid: '%s'", bookmark));
+ }
+
+ return new _TreeModel(getMetaModelContext(), objAdapter, treePath);
+ }
+
+ @Override
+ public MetaModelContext getMetaModelContext() {
+ return this.metaModelContext = WktContext.computeIfAbsent(metaModelContext);
+ }
+
+ /*
+ * Important! Models must be identifiable by their contained object. Also IDs must be
+ * unique within a tree structure.
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj instanceof _LoadableDetachableTreeModel) {
+ final _LoadableDetachableTreeModel other = (_LoadableDetachableTreeModel) obj;
+ return treePath.equals(other.treePath) && bookmark.equals(other.bookmark);
+ }
+ return false;
+ }
+
+ /*
+ * Important! Models must be identifiable by their contained object.
+ */
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+}
\ No newline at end of file
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/_TreeExpansionModel.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/_TreeExpansionModel.java
new file mode 100644
index 0000000000..8a32e363a4
--- /dev/null
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/_TreeExpansionModel.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.wicket.ui.components.tree;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.wicket.model.IModel;
+
+import org.apache.causeway.applib.graph.tree.TreePath;
+import org.apache.causeway.core.metamodel.context.MetaModelContext;
+
+/**
+ * Wicket's model for collapse/expand state
+ */
+class _TreeExpansionModel implements IModel<Set<_TreeModel>> {
+ private static final long serialVersionUID = 648152234030889164L;
+
+ public static _TreeExpansionModel of(
+ final MetaModelContext commonContext,
+ final Set<TreePath> expandedTreePaths) {
+
+ return new _TreeExpansionModel(commonContext, expandedTreePaths);
+ }
+
+ /**
+ * Happens on user interaction via UI.
+ * @param t
+ */
+ public void onExpand(final _TreeModel t) {
+ expandedTreePaths.add(t.getTreePath());
+ }
+
+ /**
+ * Happens on user interaction via UI.
+ * @param t
+ */
+ public void onCollapse(final _TreeModel t) {
+ expandedTreePaths.remove(t.getTreePath());
+ }
+
+ public boolean contains(final TreePath treePath) {
+ return expandedTreePaths.contains(treePath);
+ }
+
+ private final Set<TreePath> expandedTreePaths;
+ private final Set<_TreeModel> expandedNodes;
+
+ private _TreeExpansionModel(
+ final MetaModelContext commonContext,
+ final Set<TreePath> expandedTreePaths) {
+
+ this.expandedTreePaths = expandedTreePaths;
+ this.expandedNodes = expandedTreePaths.stream()
+ .map(tPath->new _TreeModel(commonContext, tPath))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public Set<_TreeModel> getObject() {
+ return expandedNodes;
+ }
+
+ @Override
+ public String toString() {
+ return "{" + expandedTreePaths.stream()
+ .map(TreePath::toString)
+ .collect(Collectors.joining(", ")) + "}";
+ }
+
+}
\ No newline at end of file
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/_TreeModel.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/_TreeModel.java
new file mode 100644
index 0000000000..2234b6c983
--- /dev/null
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/_TreeModel.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.wicket.ui.components.tree;
+
+import java.util.Objects;
+
+import org.apache.causeway.applib.graph.tree.TreePath;
+import org.apache.causeway.core.metamodel.context.MetaModelContext;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.viewer.wicket.model.models.UiObjectWkt;
+
+/**
+ * Extending the UiObjectWkt to also provide a TreePath.
+ */
+class _TreeModel extends UiObjectWkt {
+ private static final long serialVersionUID = 8916044984628849300L;
+
+ private final TreePath treePath;
+ private final boolean isTreePathModelOnly;
+
+ public _TreeModel(final MetaModelContext commonContext, final TreePath treePath) {
+ super(commonContext, commonContext.getObjectManager().adapt(0)); // any bookmarkable will do
+ this.treePath = treePath;
+ this.isTreePathModelOnly = true;
+ }
+
+ public _TreeModel(final MetaModelContext commonContext, final ManagedObject adapter, final TreePath treePath) {
+ super(commonContext, Objects.requireNonNull(adapter));
+ this.treePath = treePath;
+ this.isTreePathModelOnly = false;
+ }
+
+ public TreePath getTreePath() {
+ return treePath;
+ }
+
+ public boolean isTreePathModelOnly() {
+ return isTreePathModelOnly;
+ }
+
+}
\ No newline at end of file
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/_TreeModelTreeAdapter.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/_TreeModelTreeAdapter.java
new file mode 100644
index 0000000000..6b1bb3e348
--- /dev/null
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/_TreeModelTreeAdapter.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.wicket.ui.components.tree;
+
+import java.io.Serializable;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import org.apache.causeway.applib.graph.tree.TreeAdapter;
+import org.apache.causeway.applib.graph.tree.TreePath;
+import org.apache.causeway.commons.functional.IndexedFunction;
+import org.apache.causeway.core.metamodel.context.HasMetaModelContext;
+import org.apache.causeway.core.metamodel.context.MetaModelContext;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.viewer.wicket.model.util.WktContext;
+
+import lombok.NonNull;
+
+/**
+ * {@link TreeAdapter} for _TreeModel nodes.
+ */
+@SuppressWarnings({"rawtypes", "unchecked"})
+class _TreeModelTreeAdapter
+implements
+ TreeAdapter<_TreeModel>,
+ HasMetaModelContext,
+ Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Class<? extends TreeAdapter> treeAdapterClass;
+
+ private transient TreeAdapter wrappedTreeAdapter;
+ private transient MetaModelContext metaModelContext;
+
+ _TreeModelTreeAdapter(
+ final MetaModelContext mmc,
+ final Class<? extends TreeAdapter> treeAdapterClass) {
+ this.metaModelContext = mmc;
+ this.treeAdapterClass = treeAdapterClass;
+ }
+
+ @Override
+ public MetaModelContext getMetaModelContext() {
+ return this.metaModelContext = WktContext.computeIfAbsent(metaModelContext);
+ }
+
+ @Override
+ public Optional<_TreeModel> parentOf(final _TreeModel treeModel) {
+ if(treeModel==null) {
+ return Optional.empty();
+ }
+ return wrappedTreeAdapter().parentOf(unwrap(treeModel))
+ .map(pojo->wrap(pojo, treeModel.getTreePath().getParentIfAny()));
+ }
+
+ @Override
+ public int childCountOf(final _TreeModel treeModel) {
+ if(treeModel==null) {
+ return 0;
+ }
+ return wrappedTreeAdapter().childCountOf(unwrap(treeModel));
+ }
+
+ @Override
+ public Stream<_TreeModel> childrenOf(final _TreeModel treeModel) {
+ if(treeModel==null) {
+ return Stream.empty();
+ }
+ return wrappedTreeAdapter().childrenOf(unwrap(treeModel))
+ .map(newPojoToTreeModelMapper(treeModel));
+ }
+
+ _TreeModel wrap(final @NonNull Object pojo, final TreePath treePath) {
+ return new _TreeModel(
+ getMetaModelContext(),
+ ManagedObject.adaptSingular(getSpecificationLoader(), pojo),
+ treePath);
+ }
+
+ private Object unwrap(final _TreeModel model) {
+ Objects.requireNonNull(model);
+ return model.getObject().getPojo();
+ }
+
+ private Function<Object, _TreeModel> newPojoToTreeModelMapper(final _TreeModel parent) {
+ return IndexedFunction.zeroBased((indexWithinSiblings, pojo)->
+ wrap(pojo, parent.getTreePath().append(indexWithinSiblings)));
+ }
+
+ private TreeAdapter wrappedTreeAdapter() {
+ if(wrappedTreeAdapter!=null) {
+ return wrappedTreeAdapter;
+ }
+ try {
+ return wrappedTreeAdapter = getFactoryService().getOrCreate(treeAdapterClass);
+ } catch (Exception e) {
+ throw new RuntimeException("failed to instantiate tree adapter", e);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/_TreeModelTreeProvider.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/_TreeModelTreeProvider.java
new file mode 100644
index 0000000000..683231f2c1
--- /dev/null
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/tree/_TreeModelTreeProvider.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.viewer.wicket.ui.components.tree;
+
+import java.util.Iterator;
+
+import org.apache.wicket.extensions.markup.html.repeater.tree.ITreeProvider;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+
+import org.apache.causeway.commons.internal.collections._Lists;
+
+/**
+ * Wicket's {@link ITreeProvider} implemented for Causeway
+ */
+class _TreeModelTreeProvider implements ITreeProvider<_TreeModel> {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * tree's root
+ */
+ private final _TreeModel primaryValue;
+ private final _TreeModelTreeAdapter treeAdapter;
+
+ _TreeModelTreeProvider(final _TreeModel primaryValue, final _TreeModelTreeAdapter treeAdapter) {
+ this.primaryValue = primaryValue;
+ this.treeAdapter = treeAdapter;
+ }
+
+ @Override
+ public void detach() {
+ }
+
+ @Override
+ public Iterator<? extends _TreeModel> getRoots() {
+ return _Lists.singleton(primaryValue).iterator();
+ }
+
+ @Override
+ public boolean hasChildren(final _TreeModel node) {
+ return treeAdapter.childCountOf(node)>0;
+ }
+
+ @Override
+ public Iterator<? extends _TreeModel> getChildren(final _TreeModel node) {
+ return treeAdapter.childrenOf(node).iterator();
+ }
+
+ @Override
+ public IModel<_TreeModel> model(final _TreeModel treeModel) {
+ return treeModel.isTreePathModelOnly()
+ ? Model.of(treeModel)
+ : new _LoadableDetachableTreeModel(treeModel);
+ }
+
+}
\ No newline at end of file