001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.devutils.inspector; 018 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.Comparator; 024import java.util.HashSet; 025import java.util.Iterator; 026import java.util.List; 027import java.util.Optional; 028import java.util.Set; 029 030import org.apache.wicket.Component; 031import org.apache.wicket.MarkupContainer; 032import org.apache.wicket.Page; 033import org.apache.wicket.ajax.AjaxRequestTarget; 034import org.apache.wicket.ajax.markup.html.AjaxFallbackLink; 035import org.apache.wicket.ajax.markup.html.form.AjaxFallbackButton; 036import org.apache.wicket.behavior.Behavior; 037import org.apache.wicket.core.util.lang.WicketObjects; 038import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator; 039import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn; 040import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn; 041import org.apache.wicket.extensions.markup.html.repeater.data.table.PropertyColumn; 042import org.apache.wicket.extensions.markup.html.repeater.tree.AbstractTree; 043import org.apache.wicket.extensions.markup.html.repeater.tree.DefaultTableTree; 044import org.apache.wicket.extensions.markup.html.repeater.tree.table.TreeColumn; 045import org.apache.wicket.extensions.markup.html.repeater.util.SortableTreeProvider; 046import org.apache.wicket.markup.head.CssHeaderItem; 047import org.apache.wicket.markup.head.IHeaderResponse; 048import org.apache.wicket.markup.html.basic.Label; 049import org.apache.wicket.markup.html.debug.PageView; 050import org.apache.wicket.markup.html.form.CheckBox; 051import org.apache.wicket.markup.html.form.CheckBoxMultipleChoice; 052import org.apache.wicket.markup.html.form.Form; 053import org.apache.wicket.markup.html.panel.GenericPanel; 054import org.apache.wicket.markup.repeater.Item; 055import org.apache.wicket.markup.repeater.OddEvenItem; 056import org.apache.wicket.model.IModel; 057import org.apache.wicket.model.LoadableDetachableModel; 058import org.apache.wicket.model.Model; 059import org.apache.wicket.model.PropertyModel; 060import org.apache.wicket.request.resource.CssResourceReference; 061import org.apache.wicket.util.io.IClusterable; 062import org.apache.wicket.util.lang.Bytes; 063import org.apache.wicket.util.string.Strings; 064 065/** 066 * Enhanced {@link PageView} which displays all <code>Component</code>s and <code>Behavior</code>s 067 * of a <code>Page</code> in a <code>TableTree</code> representation. <code>Component</code>s and 068 * <code>Behavior</code>s can be shown based on their statefulness status. There are also filtering 069 * options to choose the information displayed. Useful for debugging. 070 * 071 * @author Bertrand Guay-Paquet 072 */ 073public final class EnhancedPageView extends GenericPanel<Page> 074{ 075 private static final long serialVersionUID = 1L; 076 077 private ExpandState expandState; 078 private boolean showStatefulAndParentsOnly; 079 private boolean showBehaviors; 080 081 private List<IColumn<TreeNode, Void>> allColumns; 082 private List<IColumn<TreeNode, Void>> visibleColumns; 083 084 private AbstractTree<TreeNode> componentTree; 085 086 /** 087 * Constructor. 088 * 089 * @param id 090 * See Component 091 * @param model 092 * The page to be analyzed 093 */ 094 public EnhancedPageView(String id, IModel<Page> model) 095 { 096 super(id, model); 097 098 expandState = new ExpandState(); 099 expandState.expandAll(); 100 showStatefulAndParentsOnly = false; 101 showBehaviors = true; 102 allColumns = allColumns(); 103 visibleColumns = new ArrayList<>(allColumns); 104 105 // Name of page 106 add(new Label("info", new Model<String>() 107 { 108 private static final long serialVersionUID = 1L; 109 110 @Override 111 public String getObject() 112 { 113 Page page = getModelObject(); 114 return page == null ? "[Stateless Page]" : page.toString(); 115 } 116 })); 117 118 Model<String> pageRenderDuration = new Model<String>() 119 { 120 private static final long serialVersionUID = 1L; 121 122 @Override 123 public String getObject() 124 { 125 Page page = getModelObject(); 126 if (page != null) 127 { 128 Long renderTime = page.getMetaData(PageView.RENDER_KEY); 129 if (renderTime != null) 130 { 131 return renderTime.toString(); 132 } 133 } 134 return "n/a"; 135 } 136 }; 137 add(new Label("pageRenderDuration", pageRenderDuration)); 138 139 addTreeControls(); 140 componentTree = newTree(); 141 add(componentTree); 142 } 143 144 private List<IColumn<TreeNode, Void>> allColumns() 145 { 146 List<IColumn<TreeNode, Void>> columns = new ArrayList<>(); 147 148 columns.add(new PropertyColumn<TreeNode, Void>(Model.of("Path"), "path") 149 { 150 private static final long serialVersionUID = 1L; 151 152 @Override 153 public String getCssClass() 154 { 155 return "col_path"; 156 } 157 158 @Override 159 public String toString() 160 { 161 return getDisplayModel().getObject(); 162 } 163 }); 164 165 columns.add(new TreeColumn<TreeNode, Void>(Model.of("Tree")) 166 { 167 private static final long serialVersionUID = 1L; 168 169 @Override 170 public String toString() 171 { 172 return getDisplayModel().getObject(); 173 } 174 }); 175 176 columns.add(new PropertyColumn<TreeNode, Void>(Model.of("Stateless"), "stateless") 177 { 178 private static final long serialVersionUID = 1L; 179 180 @Override 181 public String getCssClass() 182 { 183 return "col_stateless"; 184 } 185 186 @Override 187 public String toString() 188 { 189 return getDisplayModel().getObject(); 190 } 191 }); 192 columns.add(new PropertyColumn<TreeNode, Void>(Model.of("Render time (ms)"), "renderTime") 193 { 194 private static final long serialVersionUID = 1L; 195 196 @Override 197 public String getCssClass() 198 { 199 return "col_renderTime"; 200 } 201 202 @Override 203 public String toString() 204 { 205 return getDisplayModel().getObject(); 206 } 207 }); 208 columns.add(new AbstractColumn<TreeNode, Void>(Model.of("Size")) 209 { 210 private static final long serialVersionUID = 1L; 211 212 @Override 213 public void populateItem(Item<ICellPopulator<TreeNode>> item, String componentId, 214 IModel<TreeNode> rowModel) 215 { 216 item.add(new Label(componentId, Bytes.bytes(rowModel.getObject().getSize()) 217 .toString())); 218 } 219 220 @Override 221 public String getCssClass() 222 { 223 return "col_size"; 224 } 225 226 @Override 227 public String toString() 228 { 229 return getDisplayModel().getObject(); 230 } 231 }); 232 columns.add(new PropertyColumn<TreeNode, Void>(Model.of("Type"), "type") 233 { 234 private static final long serialVersionUID = 1L; 235 236 @Override 237 public String toString() 238 { 239 return getDisplayModel().getObject(); 240 } 241 }); 242 columns.add(new PropertyColumn<TreeNode, Void>(Model.of("Model Object"), "model") 243 { 244 private static final long serialVersionUID = 1L; 245 246 @Override 247 public String toString() 248 { 249 return getDisplayModel().getObject(); 250 } 251 }); 252 253 return columns; 254 } 255 256 private void addTreeControls() 257 { 258 Form<Void> form = new Form<>("form"); 259 add(form); 260 form.add(new CheckBox("showStateless", new PropertyModel<Boolean>(this, 261 "showStatefulAndParentsOnly"))); 262 form.add(new CheckBox("showBehaviors", new PropertyModel<Boolean>(this, "showBehaviors"))); 263 form.add(new CheckBoxMultipleChoice<>("visibleColumns", 264 new PropertyModel<List<IColumn<TreeNode, Void>>>(this, "visibleColumns"), allColumns).setSuffix(" ")); 265 form.add(new AjaxFallbackButton("submit", form) 266 { 267 private static final long serialVersionUID = 1L; 268 269 @Override 270 protected void onAfterSubmit(Optional<AjaxRequestTarget> target) 271 { 272 target.ifPresent(t -> t.add(componentTree)); 273 } 274 }); 275 276 277 add(new AjaxFallbackLink<Void>("expandAll") 278 { 279 private static final long serialVersionUID = 1L; 280 281 @Override 282 public void onClick(Optional<AjaxRequestTarget> targetOptional) 283 { 284 expandState.expandAll(); 285 targetOptional.ifPresent(target -> target.add(componentTree)); 286 } 287 }); 288 add(new AjaxFallbackLink<Void>("collapseAll") 289 { 290 private static final long serialVersionUID = 1L; 291 292 @Override 293 public void onClick(Optional<AjaxRequestTarget> targetOptional) 294 { 295 expandState.collapseAll(); 296 targetOptional.ifPresent(target -> target.add(componentTree)); 297 } 298 }); 299 } 300 301 private AbstractTree<TreeNode> newTree() 302 { 303 TreeProvider provider = new TreeProvider(); 304 IModel<Set<TreeNode>> expandStateModel = new LoadableDetachableModel<Set<TreeNode>>() 305 { 306 private static final long serialVersionUID = 1L; 307 308 @Override 309 protected Set<TreeNode> load() 310 { 311 return expandState; 312 } 313 }; 314 AbstractTree<TreeNode> tree = new DefaultTableTree<TreeNode, Void>("tree", visibleColumns, 315 provider, Integer.MAX_VALUE, expandStateModel) 316 { 317 private static final long serialVersionUID = 1L; 318 319 @Override 320 protected Item<TreeNode> newRowItem(String id, int index, IModel<TreeNode> model) 321 { 322 return new OddEvenItem<>(id, index, model); 323 } 324 }; 325 tree.setOutputMarkupId(true); 326 return tree; 327 } 328 329 /** 330 * Tree node representing either a <code>Page</code>, a <code>Component</code> or a 331 * <code>Behavior</code> 332 */ 333 private static class TreeNode 334 { 335 public IClusterable node; 336 public TreeNode parent; 337 public List<TreeNode> children; 338 339 public TreeNode(IClusterable node, TreeNode parent) 340 { 341 this.node = node; 342 this.parent = parent; 343 children = new ArrayList<>(); 344 if (!(node instanceof Component) && !(node instanceof Behavior)) 345 throw new IllegalArgumentException("Only accepts Components and Behaviors"); 346 } 347 348 public boolean hasChildren() 349 { 350 return !children.isEmpty(); 351 } 352 353 /** 354 * @return list of indexes to navigate from the root of the tree to this node (e.g. the path 355 * to the node). 356 */ 357 public List<Integer> getPathIndexes() 358 { 359 List<Integer> path = new ArrayList<>(); 360 TreeNode nextChild = this; 361 TreeNode parent; 362 while ((parent = nextChild.parent) != null) 363 { 364 int indexOf = parent.children.indexOf(nextChild); 365 if (indexOf < 0) 366 throw new AssertionError("Child not found in parent"); 367 path.add(indexOf); 368 nextChild = parent; 369 } 370 Collections.reverse(path); 371 return path; 372 } 373 374 public String getPath() 375 { 376 if (node instanceof Component) 377 { 378 return ((Component)node).getPath(); 379 } 380 else 381 { 382 Behavior behavior = (Behavior)node; 383 Component parent = (Component)this.parent.node; 384 String parentPath = parent.getPath(); 385 int indexOf = parent.getBehaviors().indexOf(behavior); 386 return parentPath + Component.PATH_SEPARATOR + "Behavior_" + indexOf; 387 } 388 } 389 390 public String getRenderTime() 391 { 392 if (node instanceof Component) 393 { 394 Long renderDuration = ((Component)node).getMetaData(PageView.RENDER_KEY); 395 if (renderDuration != null) 396 { 397 return renderDuration.toString(); 398 } 399 } 400 return "n/a"; 401 } 402 403 public long getSize() 404 { 405 if (node instanceof Component) 406 { 407 long size = ((Component)node).getSizeInBytes(); 408 return size; 409 } 410 else 411 { 412 long size = WicketObjects.sizeof(node); 413 return size; 414 } 415 } 416 417 public String getType() 418 { 419 // anonymous class? Get the parent's class name 420 String type = node.getClass().getName(); 421 if (type.indexOf("$") > 0) 422 { 423 type = node.getClass().getSuperclass().getName(); 424 } 425 return type; 426 } 427 428 public String getModel() 429 { 430 if (node instanceof Component) 431 { 432 String model; 433 try 434 { 435 model = ((Component)node).getDefaultModelObjectAsString(); 436 } 437 catch (Exception e) 438 { 439 model = e.getMessage(); 440 } 441 return model; 442 } 443 return null; 444 } 445 446 public boolean isStateless() 447 { 448 if (node instanceof Page) 449 { 450 return ((Page)node).isPageStateless(); 451 } 452 else if (node instanceof Component) 453 { 454 return ((Component)node).isStateless(); 455 } 456 else 457 { 458 Behavior behavior = (Behavior)node; 459 Component parent = (Component)this.parent.node; 460 return behavior.getStatelessHint(parent); 461 } 462 } 463 464 @Override 465 public String toString() 466 { 467 if (node instanceof Page) 468 { 469 // Last component of getType() i.e. almost the same as getClass().getSimpleName(); 470 String type = getType(); 471 type = Strings.lastPathComponent(type, '.'); 472 return type; 473 } 474 else if (node instanceof Component) 475 { 476 return ((Component)node).getId(); 477 } 478 else 479 { 480 // Last component of getType() i.e. almost the same as getClass().getSimpleName(); 481 String type = getType(); 482 type = Strings.lastPathComponent(type, '.'); 483 return type; 484 } 485 } 486 } 487 488 489 /** 490 * TreeNode provider for the page. Provides nodes for the components and behaviors of the 491 * analyzed page. 492 */ 493 private class TreeProvider extends SortableTreeProvider<TreeNode, Void> 494 { 495 private static final long serialVersionUID = 1L; 496 497 private TreeModel componentTreeModel = new TreeModel(); 498 499 @Override 500 public void detach() 501 { 502 componentTreeModel.detach(); 503 } 504 505 @Override 506 public Iterator<? extends TreeNode> getRoots() 507 { 508 TreeNode tree = componentTreeModel.getObject(); 509 List<TreeNode> roots; 510 if (tree == null) 511 roots = Collections.emptyList(); 512 else 513 roots = Arrays.asList(tree); 514 return roots.iterator(); 515 } 516 517 @Override 518 public boolean hasChildren(TreeNode node) 519 { 520 return node.hasChildren(); 521 } 522 523 @Override 524 public Iterator<? extends TreeNode> getChildren(TreeNode node) 525 { 526 return node.children.iterator(); 527 } 528 529 @Override 530 public IModel<TreeNode> model(TreeNode object) 531 { 532 return new TreeNodeModel(object); 533 } 534 535 /** 536 * Model of the page component and behavior tree 537 */ 538 private class TreeModel extends LoadableDetachableModel<TreeNode> 539 { 540 private static final long serialVersionUID = 1L; 541 542 @Override 543 protected TreeNode load() 544 { 545 Page page = getModelObject(); 546 if (page == null) 547 return null; 548 return buildTree(page, null); 549 } 550 551 private TreeNode buildTree(Component node, TreeNode parent) 552 { 553 TreeNode treeNode = new TreeNode(node, parent); 554 List<TreeNode> children = treeNode.children; 555 556 // Add its behaviors 557 if (showBehaviors) 558 { 559 for (Behavior behavior : node.getBehaviors()) 560 { 561 if (!showStatefulAndParentsOnly || !behavior.getStatelessHint(node)) 562 children.add(new TreeNode(behavior, treeNode)); 563 } 564 } 565 566 // Add its children 567 if (node instanceof MarkupContainer) 568 { 569 MarkupContainer container = (MarkupContainer)node; 570 for (Component child : container) 571 { 572 buildTree(child, treeNode); 573 } 574 } 575 576 // Sort the children list, putting behaviors first 577 Collections.sort(children, new Comparator<TreeNode>() 578 { 579 @Override 580 public int compare(TreeNode o1, TreeNode o2) 581 { 582 if (o1.node instanceof Component) 583 { 584 if (o2.node instanceof Component) 585 { 586 return o1.getPath().compareTo((o2).getPath()); 587 } 588 else 589 { 590 return 1; 591 } 592 } 593 else 594 { 595 if (o2.node instanceof Component) 596 { 597 return -1; 598 } 599 else 600 { 601 return o1.getPath().compareTo((o2).getPath()); 602 } 603 } 604 } 605 }); 606 607 // Add this node to its parent if 608 // -it has children or 609 // -it is stateful or 610 // -stateless components are visible 611 if (parent != null && 612 (!showStatefulAndParentsOnly || treeNode.hasChildren() || !node.isStateless())) 613 { 614 parent.children.add(treeNode); 615 } 616 return treeNode; 617 } 618 } 619 620 /** 621 * Rertrieves a TreeNode based on its path 622 */ 623 private class TreeNodeModel extends LoadableDetachableModel<TreeNode> 624 { 625 private static final long serialVersionUID = 1L; 626 627 private List<Integer> path; 628 629 public TreeNodeModel(TreeNode treeNode) 630 { 631 super(treeNode); 632 path = treeNode.getPathIndexes(); 633 } 634 635 @Override 636 protected TreeNode load() 637 { 638 TreeNode tree = componentTreeModel.getObject(); 639 TreeNode currentItem = tree; 640 for (Integer index : path) 641 { 642 currentItem = currentItem.children.get(index); 643 } 644 return currentItem; 645 } 646 647 /** 648 * Important! Models must be identifyable by their contained object. 649 */ 650 @Override 651 public int hashCode() 652 { 653 return path.hashCode(); 654 } 655 656 /** 657 * Important! Models must be identifyable by their contained object. 658 */ 659 @Override 660 public boolean equals(Object obj) 661 { 662 if (obj instanceof TreeNodeModel) 663 { 664 return ((TreeNodeModel)obj).path.equals(path); 665 } 666 return false; 667 } 668 } 669 } 670 671 /** 672 * Expansion state of the tree's nodes 673 */ 674 private static class ExpandState implements Set<TreeNode>, IClusterable 675 { 676 private static final long serialVersionUID = 1L; 677 678 private HashSet<List<Integer>> set = new HashSet<>(); 679 private boolean reversed = false; 680 681 public void expandAll() 682 { 683 set.clear(); 684 reversed = true; 685 } 686 687 public void collapseAll() 688 { 689 set.clear(); 690 reversed = false; 691 } 692 693 @Override 694 public boolean add(TreeNode a_e) 695 { 696 List<Integer> pathIndexes = a_e.getPathIndexes(); 697 if (reversed) 698 { 699 return set.remove(pathIndexes); 700 } 701 else 702 { 703 return set.add(pathIndexes); 704 } 705 } 706 707 @Override 708 public boolean remove(Object a_o) 709 { 710 TreeNode item = (TreeNode)a_o; 711 List<Integer> pathIndexes = item.getPathIndexes(); 712 if (reversed) 713 { 714 return set.add(pathIndexes); 715 } 716 else 717 { 718 return set.remove(pathIndexes); 719 } 720 } 721 722 @Override 723 public boolean contains(Object a_o) 724 { 725 TreeNode item = (TreeNode)a_o; 726 List<Integer> pathIndexes = item.getPathIndexes(); 727 if (reversed) 728 { 729 return !set.contains(pathIndexes); 730 } 731 else 732 { 733 return set.contains(pathIndexes); 734 } 735 } 736 737 @Override 738 public int size() 739 { 740 throw new UnsupportedOperationException(); 741 } 742 743 @Override 744 public boolean isEmpty() 745 { 746 throw new UnsupportedOperationException(); 747 } 748 749 @Override 750 public Iterator<TreeNode> iterator() 751 { 752 throw new UnsupportedOperationException(); 753 } 754 755 @Override 756 public Object[] toArray() 757 { 758 throw new UnsupportedOperationException(); 759 } 760 761 @Override 762 public <T> T[] toArray(T[] a_a) 763 { 764 throw new UnsupportedOperationException(); 765 } 766 767 @Override 768 public boolean containsAll(Collection<?> a_c) 769 { 770 throw new UnsupportedOperationException(); 771 } 772 773 @Override 774 public boolean addAll(Collection<? extends TreeNode> a_c) 775 { 776 throw new UnsupportedOperationException(); 777 } 778 779 @Override 780 public boolean retainAll(Collection<?> a_c) 781 { 782 throw new UnsupportedOperationException(); 783 } 784 785 @Override 786 public boolean removeAll(Collection<?> a_c) 787 { 788 throw new UnsupportedOperationException(); 789 } 790 791 @Override 792 public void clear() 793 { 794 throw new UnsupportedOperationException(); 795 } 796 } 797 798 @Override 799 public void renderHead(IHeaderResponse response) 800 { 801 super.renderHead(response); 802 response.render(CssHeaderItem.forReference( 803 new CssResourceReference(EnhancedPageView.class, "enhancedpageview.css"))); 804 } 805}