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