/*
* 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.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(AjaxRequestTarget target, Form<?> form)
{
if (target != null)
{
target.add(componentTree);
}
}
});
add(new AjaxFallbackLink<Void>("expandAll")
{
private static final long serialVersionUID = 1L;
@Override
public void onClick(AjaxRequestTarget target)
{
expandState.expandAll();
if (target != null)
target.add(componentTree);
}
});
add(new AjaxFallbackLink<Void>("collapseAll")
{
private static final long serialVersionUID = 1L;
@Override
public void onClick(AjaxRequestTarget target)
{
expandState.collapseAll();
if (target != null)
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();
}
}
}