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.stateless;
018
019import org.apache.wicket.Component;
020import org.apache.wicket.MarkupContainer;
021import org.apache.wicket.Page;
022import org.apache.wicket.application.IComponentOnBeforeRenderListener;
023import org.apache.wicket.behavior.Behavior;
024import org.apache.wicket.util.lang.Classes;
025import org.apache.wicket.util.string.StringList;
026import org.apache.wicket.util.visit.IVisit;
027import org.apache.wicket.util.visit.IVisitor;
028
029/**
030 * Stateless checker. Checks if components with {@link StatelessComponent} annotation are really
031 * stateless. This is a utility that is intended for use primarily during development. If you add an
032 * instance of this class to your application, it will check all components or pages marked with the
033 * <tt>StatelessComponent</tt> annotation to make sure that they are stateless as you intended.
034 *
035 * This is useful when trying to maintain stateless pages since it is very easy to inadvertently add
036 * a component to a page that internally uses stateful links, etc.
037 *
038 * @author Marat Radchenko
039 * @see StatelessComponent
040 */
041public class StatelessChecker implements IComponentOnBeforeRenderListener
042{
043        /**
044         * Returns <code>true</code> if checker must check given component, <code>false</code>
045         * otherwise.
046         *
047         * @param component
048         *            component to check.
049         * @return <code>true</code> if checker must check given component.
050         */
051        protected boolean mustCheck(final Component component)
052        {
053                final StatelessComponent ann = component.getClass().getAnnotation(StatelessComponent.class);
054                return (ann != null) && ann.enabled();
055        }
056        /**
057         * The given component claims to be stateless but isn't.
058         *
059         * @param e StatelessCheckFailureException
060         */
061        protected void fail(StatelessCheckFailureException e)
062        {
063                throw e;
064        }
065
066        /**
067         * @see org.apache.wicket.application.IComponentOnBeforeRenderListener#onBeforeRender(org.apache.wicket.Component)
068         */
069        @Override
070        public void onBeforeRender(final Component component)
071        {
072                if (mustCheck(component))
073                {
074                        final IVisitor<Component, Component> visitor = new IVisitor<Component, Component>()
075                        {
076                                @Override
077                                public void component(final Component comp, final IVisit<Component> visit)
078                                {
079                                        if ((component instanceof Page) && mustCheck(comp))
080                                        {
081                                                // Do not go deeper, because this component will be
082                                                // checked by checker
083                                                // itself.
084                                                // Actually we could go deeper but that would mean we
085                                                // traverse it twice
086                                                // (for current component and for inspected one).
087                                                // We go deeper for Page because full tree will be
088                                                // inspected during
089                                                // isPageStateless call.
090                                                visit.dontGoDeeper();
091                                        }
092                                        else if (!comp.isStateless())
093                                        {
094                                                visit.stop(comp);
095                                        }
096                                        else
097                                        {
098                                                // continue
099                                        }
100                                }
101                        };
102
103                        if (component.isStateless() == false)
104                        {
105                                StringList statefulBehaviors = new StringList();
106                                for (Behavior b : component.getBehaviors())
107                                {
108                                        if (b.getStatelessHint(component) == false)
109                                        {
110                                                statefulBehaviors.add(Classes.name(b.getClass()));
111                                        }
112                                }
113                                String reason;
114                                if (statefulBehaviors.size() == 0)
115                                {
116                                    reason = " Possible reason: no stateless hint";
117                                }
118                                else
119                                {
120                                    reason = " Stateful behaviors: " + statefulBehaviors.join();
121                                }
122                                fail(new StatelessCheckFailureException(component, reason));
123                                return;
124                        }
125
126                        if (component instanceof MarkupContainer)
127                        {
128                                MarkupContainer container = ((MarkupContainer)component);
129                                // Traverse children
130                                final Object o = container.visitChildren(visitor);
131                                if (o != null)
132                                {
133                                        fail(new StatelessCheckFailureException(container, " Offending component: " + o));
134                                        return;
135                                }
136                        }
137
138                        if (component instanceof Page)
139                        {
140                                final Page p = (Page)component;
141                                if (!p.isBookmarkable())
142                                {
143                                        fail(new StatelessCheckFailureException(p, " Only bookmarkable pages can be stateless"));
144                                        return;
145                                }
146                                if (!p.isPageStateless())
147                                {
148                                        fail(new StatelessCheckFailureException(p, " for unknown reason"));
149                                        return;
150                                }
151                        }
152                }
153        }
154}