/* * 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.wicket.devutils.inspector; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.Set; import org.apache.wicket.Component; import org.apache.wicket.MarkupContainer; import org.apache.wicket.Page; import org.apache.wicket.PageReference; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.markup.html.AjaxFallbackLink; import org.apache.wicket.ajax.markup.html.form.AjaxFallbackButton; import org.apache.wicket.behavior.Behavior; import org.apache.wicket.core.util.lang.WicketObjects; import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator; import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn; import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn; import org.apache.wicket.extensions.markup.html.repeater.data.table.PropertyColumn; import org.apache.wicket.extensions.markup.html.repeater.tree.AbstractTree; import org.apache.wicket.extensions.markup.html.repeater.tree.DefaultTableTree; import org.apache.wicket.extensions.markup.html.repeater.tree.table.TreeColumn; import org.apache.wicket.extensions.markup.html.repeater.util.SortableTreeProvider; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.debug.PageView; import org.apache.wicket.markup.html.form.CheckBox; import org.apache.wicket.markup.html.form.CheckBoxMultipleChoice; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.panel.GenericPanel; import org.apache.wicket.markup.repeater.Item; import org.apache.wicket.markup.repeater.OddEvenItem; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.Model; import org.apache.wicket.model.PropertyModel; import org.apache.wicket.util.io.IClusterable; import org.apache.wicket.util.lang.Bytes; import org.apache.wicket.util.string.Strings; /** * Enhanced {@link PageView} which displays all <code>Component</code>s and <code>Behavior</code>s * of a <code>Page</code> in a <code>TableTree</code> representation. <code>Component</code>s and * <code>Behavior</code>s can be shown based on their statefulness status. There are also filtering * options to choose the information displayed. Useful for debugging. * * @author Bertrand Guay-Paquet */ public final class EnhancedPageView extends GenericPanel<Page> { private static final long serialVersionUID = 1L; private ExpandState expandState; private boolean showStatefulAndParentsOnly; private boolean showBehaviors; private List<IColumn<TreeNode, Void>> allColumns; private List<IColumn<TreeNode, Void>> visibleColumns; private AbstractTree<TreeNode> componentTree; /** * Constructor. * * @param id * See Component * @param page * The page to be analyzed */ public EnhancedPageView(String id, Page page) { this(id, getModelFor(page == null ? null : page.getPageReference())); } private static IModel<Page> getModelFor(final PageReference pageRef) { return new LoadableDetachableModel<Page>() { private static final long serialVersionUID = 1L; @Override protected Page load() { if (pageRef == null) return null; Page page = pageRef.getPage(); return page; } }; } /** * Constructor. * * @param id * See Component * @param pageModel * The page to be analyzed */ public EnhancedPageView(String id, IModel<Page> pageModel) { super(id, pageModel); expandState = new ExpandState(); expandState.expandAll(); showStatefulAndParentsOnly = false; showBehaviors = true; allColumns = allColumns(); visibleColumns = new ArrayList<>(allColumns); // Name of page add(new Label("info", new Model<String>() { private static final long serialVersionUID = 1L; @Override public String getObject() { Page page = getModelObject(); return page == null ? "[Stateless Page]" : page.toString(); } })); Model<String> pageRenderDuration = new Model<String>() { private static final long serialVersionUID = 1L; @Override public String getObject() { Page page = getModelObject(); if (page != null) { Long renderTime = page.getMetaData(PageView.RENDER_KEY); if (renderTime != null) { return renderTime.toString(); } } return "n/a"; } }; add(new Label("pageRenderDuration", pageRenderDuration)); addTreeControls(); componentTree = newTree(); add(componentTree); } private List<IColumn<TreeNode, Void>> allColumns() { List<IColumn<TreeNode, Void>> columns = new ArrayList<>(); columns.add(new PropertyColumn<TreeNode, Void>(Model.of("Path"), "path") { private static final long serialVersionUID = 1L; @Override public String getCssClass() { return "col_path"; } @Override public String toString() { return getDisplayModel().getObject(); } }); columns.add(new TreeColumn<TreeNode, Void>(Model.of("Tree")) { private static final long serialVersionUID = 1L; @Override public String toString() { return getDisplayModel().getObject(); } }); columns.add(new PropertyColumn<TreeNode, Void>(Model.of("Stateless"), "stateless") { private static final long serialVersionUID = 1L; @Override public String getCssClass() { return "col_stateless"; } @Override public String toString() { return getDisplayModel().getObject(); } }); columns.add(new PropertyColumn<TreeNode, Void>(Model.of("Render time (ms)"), "renderTime") { private static final long serialVersionUID = 1L; @Override public String getCssClass() { return "col_renderTime"; } @Override public String toString() { return getDisplayModel().getObject(); } }); columns.add(new AbstractColumn<TreeNode, Void>(Model.of("Size")) { private static final long serialVersionUID = 1L; @Override public void populateItem(Item<ICellPopulator<TreeNode>> item, String componentId, IModel<TreeNode> rowModel) { item.add(new Label(componentId, Bytes.bytes(rowModel.getObject().getSize()) .toString())); } @Override public String getCssClass() { return "col_size"; } @Override public String toString() { return getDisplayModel().getObject(); } }); columns.add(new PropertyColumn<TreeNode, Void>(Model.of("Type"), "type") { private static final long serialVersionUID = 1L; @Override public String toString() { return getDisplayModel().getObject(); } }); columns.add(new PropertyColumn<TreeNode, Void>(Model.of("Model Object"), "model") { private static final long serialVersionUID = 1L; @Override public String toString() { return getDisplayModel().getObject(); } }); return columns; } private void addTreeControls() { Form<Void> form = new Form<>("form"); add(form); form.add(new CheckBox("showStateless", new PropertyModel<Boolean>(this, "showStatefulAndParentsOnly"))); form.add(new CheckBox("showBehaviors", new PropertyModel<Boolean>(this, "showBehaviors"))); form.add(new CheckBoxMultipleChoice<>("visibleColumns", new PropertyModel<List<IColumn<TreeNode, Void>>>(this, "visibleColumns"), allColumns).setSuffix(" ")); form.add(new AjaxFallbackButton("submit", form) { private static final long serialVersionUID = 1L; @Override protected void onAfterSubmit(Optional<AjaxRequestTarget> target) { target.ifPresent(t -> t.add(componentTree)); } }); add(new AjaxFallbackLink<Void>("expandAll") { private static final long serialVersionUID = 1L; public void onClick(Optional<AjaxRequestTarget> targetOptional) { expandState.expandAll(); targetOptional.ifPresent(target -> target.add(componentTree)); } }); add(new AjaxFallbackLink<Void>("collapseAll") { private static final long serialVersionUID = 1L; @Override public void onClick(Optional<AjaxRequestTarget> targetOptional) { expandState.collapseAll(); targetOptional.ifPresent(target -> target.add(componentTree)); } }); } private AbstractTree<TreeNode> newTree() { TreeProvider provider = new TreeProvider(); IModel<Set<TreeNode>> expandStateModel = new LoadableDetachableModel<Set<TreeNode>>() { private static final long serialVersionUID = 1L; @Override protected Set<TreeNode> load() { return expandState; } }; AbstractTree<TreeNode> tree = new DefaultTableTree<TreeNode, Void>("tree", visibleColumns, provider, Integer.MAX_VALUE, expandStateModel) { private static final long serialVersionUID = 1L; @Override protected Item<TreeNode> newRowItem(String id, int index, IModel<TreeNode> model) { return new OddEvenItem<>(id, index, model); } }; tree.setOutputMarkupId(true); return tree; } /** * Tree node representing either a <code>Page</code>, a <code>Component</code> or a * <code>Behavior</code> */ private static class TreeNode { public IClusterable node; public TreeNode parent; public List<TreeNode> children; public TreeNode(IClusterable node, TreeNode parent) { this.node = node; this.parent = parent; children = new ArrayList<>(); if (!(node instanceof Component) && !(node instanceof Behavior)) throw new IllegalArgumentException("Only accepts Components and Behaviors"); } public boolean hasChildren() { return !children.isEmpty(); } /** * @return list of indexes to navigate from the root of the tree to this node (e.g. the path * to the node). */ public List<Integer> getPathIndexes() { List<Integer> path = new ArrayList<>(); TreeNode nextChild = this; TreeNode parent; while ((parent = nextChild.parent) != null) { int indexOf = parent.children.indexOf(nextChild); if (indexOf < 0) throw new AssertionError("Child not found in parent"); path.add(indexOf); nextChild = parent; } Collections.reverse(path); return path; } public String getPath() { if (node instanceof Component) { return ((Component)node).getPath(); } else { Behavior behavior = (Behavior)node; Component parent = (Component)this.parent.node; String parentPath = parent.getPath(); int indexOf = parent.getBehaviors().indexOf(behavior); return parentPath + Component.PATH_SEPARATOR + "Behavior_" + indexOf; } } public String getRenderTime() { if (node instanceof Component) { Long renderDuration = ((Component)node).getMetaData(PageView.RENDER_KEY); if (renderDuration != null) { return renderDuration.toString(); } } return "n/a"; } public long getSize() { if (node instanceof Component) { long size = ((Component)node).getSizeInBytes(); return size; } else { long size = WicketObjects.sizeof(node); return size; } } public String getType() { // anonymous class? Get the parent's class name String type = node.getClass().getName(); if (type.indexOf("$") > 0) { type = node.getClass().getSuperclass().getName(); } return type; } public String getModel() { if (node instanceof Component) { String model; try { model = ((Component)node).getDefaultModelObjectAsString(); } catch (Exception e) { model = e.getMessage(); } return model; } return null; } public boolean isStateless() { if (node instanceof Page) { return ((Page)node).isPageStateless(); } else if (node instanceof Component) { return ((Component)node).isStateless(); } else { Behavior behavior = (Behavior)node; Component parent = (Component)this.parent.node; return behavior.getStatelessHint(parent); } } @Override public String toString() { if (node instanceof Page) { // Last component of getType() i.e. almost the same as getClass().getSimpleName(); String type = getType(); type = Strings.lastPathComponent(type, '.'); return type; } else if (node instanceof Component) { return ((Component)node).getId(); } else { // Last component of getType() i.e. almost the same as getClass().getSimpleName(); String type = getType(); type = Strings.lastPathComponent(type, '.'); return type; } } } /** * TreeNode provider for the page. Provides nodes for the components and behaviors of the * analyzed page. */ private class TreeProvider extends SortableTreeProvider<TreeNode, Void> { private static final long serialVersionUID = 1L; private TreeModel componentTreeModel = new TreeModel(); @Override public void detach() { componentTreeModel.detach(); } @Override public Iterator<? extends TreeNode> getRoots() { TreeNode tree = componentTreeModel.getObject(); List<TreeNode> roots; if (tree == null) roots = Collections.emptyList(); else roots = Arrays.asList(tree); return roots.iterator(); } @Override public boolean hasChildren(TreeNode node) { return node.hasChildren(); } @Override public Iterator<? extends TreeNode> getChildren(TreeNode node) { return node.children.iterator(); } @Override public IModel<TreeNode> model(TreeNode object) { return new TreeNodeModel(object); } /** * Model of the page component and behavior tree */ private class TreeModel extends LoadableDetachableModel<TreeNode> { private static final long serialVersionUID = 1L; @Override protected TreeNode load() { Page page = getModelObject(); if (page == null) return null; return buildTree(page, null); } private TreeNode buildTree(Component node, TreeNode parent) { TreeNode treeNode = new TreeNode(node, parent); List<TreeNode> children = treeNode.children; // Add its behaviors if (showBehaviors) { for (Behavior behavior : node.getBehaviors()) { if (!showStatefulAndParentsOnly || !behavior.getStatelessHint(node)) children.add(new TreeNode(behavior, treeNode)); } } // Add its children if (node instanceof MarkupContainer) { MarkupContainer container = (MarkupContainer)node; for (Component child : container) { buildTree(child, treeNode); } } // Sort the children list, putting behaviors first Collections.sort(children, new Comparator<TreeNode>() { @Override public int compare(TreeNode o1, TreeNode o2) { if (o1.node instanceof Component) { if (o2.node instanceof Component) { return o1.getPath().compareTo((o2).getPath()); } else { return 1; } } else { if (o2.node instanceof Component) { return -1; } else { return o1.getPath().compareTo((o2).getPath()); } } } }); // Add this node to its parent if // -it has children or // -it is stateful or // -stateless components are visible if (parent != null && (!showStatefulAndParentsOnly || treeNode.hasChildren() || !node.isStateless())) { parent.children.add(treeNode); } return treeNode; } } /** * Rertrieves a TreeNode based on its path */ private class TreeNodeModel extends LoadableDetachableModel<TreeNode> { private static final long serialVersionUID = 1L; private List<Integer> path; public TreeNodeModel(TreeNode treeNode) { super(treeNode); path = treeNode.getPathIndexes(); } @Override protected TreeNode load() { TreeNode tree = componentTreeModel.getObject(); TreeNode currentItem = tree; for (Integer index : path) { currentItem = currentItem.children.get(index); } return currentItem; } /** * Important! Models must be identifyable by their contained object. */ @Override public int hashCode() { return path.hashCode(); } /** * Important! Models must be identifyable by their contained object. */ @Override public boolean equals(Object obj) { if (obj instanceof TreeNodeModel) { return ((TreeNodeModel)obj).path.equals(path); } return false; } } } /** * Expansion state of the tree's nodes */ private static class ExpandState implements Set<TreeNode>, IClusterable { private static final long serialVersionUID = 1L; private HashSet<List<Integer>> set = new HashSet<>(); private boolean reversed = false; public void expandAll() { set.clear(); reversed = true; } public void collapseAll() { set.clear(); reversed = false; } @Override public boolean add(TreeNode a_e) { List<Integer> pathIndexes = a_e.getPathIndexes(); if (reversed) { return set.remove(pathIndexes); } else { return set.add(pathIndexes); } } @Override public boolean remove(Object a_o) { TreeNode item = (TreeNode)a_o; List<Integer> pathIndexes = item.getPathIndexes(); if (reversed) { return set.add(pathIndexes); } else { return set.remove(pathIndexes); } } @Override public boolean contains(Object a_o) { TreeNode item = (TreeNode)a_o; List<Integer> pathIndexes = item.getPathIndexes(); if (reversed) { return !set.contains(pathIndexes); } else { return set.contains(pathIndexes); } } @Override public int size() { throw new UnsupportedOperationException(); } @Override public boolean isEmpty() { throw new UnsupportedOperationException(); } @Override public Iterator<TreeNode> iterator() { throw new UnsupportedOperationException(); } @Override public Object[] toArray() { throw new UnsupportedOperationException(); } @Override public <T> T[] toArray(T[] a_a) { throw new UnsupportedOperationException(); } @Override public boolean containsAll(Collection<?> a_c) { throw new UnsupportedOperationException(); } @Override public boolean addAll(Collection<? extends TreeNode> a_c) { throw new UnsupportedOperationException(); } @Override public boolean retainAll(Collection<?> a_c) { throw new UnsupportedOperationException(); } @Override public boolean removeAll(Collection<?> a_c) { throw new UnsupportedOperationException(); } @Override public void clear() { throw new UnsupportedOperationException(); } } }