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}