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.core.request.handler;
018
019import org.apache.wicket.Component;
020import org.apache.wicket.IRequestListener;
021import org.apache.wicket.Page;
022import org.apache.wicket.WicketRuntimeException;
023import org.apache.wicket.behavior.Behavior;
024import org.apache.wicket.core.request.handler.RenderPageRequestHandler.RedirectPolicy;
025import org.apache.wicket.core.request.handler.logger.ListenerLogData;
026import org.apache.wicket.request.ILoggableRequestHandler;
027import org.apache.wicket.request.IRequestCycle;
028import org.apache.wicket.request.component.IRequestableComponent;
029import org.apache.wicket.request.component.IRequestablePage;
030import org.apache.wicket.request.http.WebRequest;
031import org.apache.wicket.request.mapper.parameter.PageParameters;
032import org.apache.wicket.util.lang.Args;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036/**
037 * Request handler that invokes an {@link IRequestListener} on component or behavior and renders page afterwards.
038 *
039 * @author Matej Knopp
040 */
041public class ListenerRequestHandler
042        implements
043                IPageRequestHandler,
044                IComponentRequestHandler,
045                ILoggableRequestHandler
046{
047        
048        private static final Logger LOG = LoggerFactory.getLogger(ListenerRequestHandler.class);
049
050        private final IPageAndComponentProvider pageComponentProvider;
051
052        private final Integer behaviorId;
053
054        private ListenerLogData logData;
055
056        /**
057         * Construct.
058         *
059         * @param pageComponentProvider
060         * @param behaviorIndex
061         */
062        public ListenerRequestHandler(IPageAndComponentProvider pageComponentProvider, Integer behaviorIndex)
063        {
064                Args.notNull(pageComponentProvider, "pageComponentProvider");
065
066                this.pageComponentProvider = pageComponentProvider;
067                behaviorId = behaviorIndex;
068        }
069
070        /**
071         * Construct.
072         *
073         * @param pageComponentProvider
074         */
075        public ListenerRequestHandler(PageAndComponentProvider pageComponentProvider)
076        {
077                this(pageComponentProvider, null);
078        }
079
080        public boolean includeRenderCount()
081        {
082                if (behaviorId == null)
083                {
084                        return ((IRequestListener)getComponent()).rendersPage();
085                }
086                else
087                {
088                        return ((IRequestListener)getComponent().getBehaviorById(getBehaviorIndex()))
089                                .rendersPage();
090                }
091        }
092        
093        @Override
094        public IRequestableComponent getComponent()
095        {
096                return pageComponentProvider.getComponent();
097        }
098
099        @Override
100        public IRequestablePage getPage()
101        {
102                return pageComponentProvider.getPageInstance();         
103        }
104
105        @Override
106        public Class<? extends IRequestablePage> getPageClass()
107        {
108                return pageComponentProvider.getPageClass();
109        }
110
111        @Override
112        public Integer getPageId()
113        {
114                return pageComponentProvider.getPageId();
115        }
116
117        @Override
118        public PageParameters getPageParameters()
119        {
120                return pageComponentProvider.getPageParameters();
121        }
122
123        @Override
124        public void detach(IRequestCycle requestCycle)
125        {
126                if (logData == null)
127                {
128                        logData = new ListenerLogData(pageComponentProvider, behaviorId);
129                }
130                pageComponentProvider.detach();
131        }
132
133        /**
134         * Index of target behavior or <code>null</code> if component is the target.
135         *
136         * @return behavior index or <code>null</code>
137         */
138        public Integer getBehaviorIndex()
139        {
140                return behaviorId;
141        }
142
143        @Override
144        public void respond(final IRequestCycle requestCycle)
145        {
146                final IRequestablePage page = getPage();
147                final boolean freshPage = pageComponentProvider.doesProvideNewPage();
148                final boolean isAjax = ((WebRequest)requestCycle.getRequest()).isAjax();
149
150                IRequestableComponent component;
151                try
152                {
153                        component = getComponent();
154                }
155                catch (ComponentNotFoundException e)
156                {
157                        // either the page is stateless and the component we are looking for is not added in the
158                        // constructor
159                        // or the page is stateful+stale and a new instances was created by pageprovider
160                        // we denote this by setting component to null
161                        component = null;
162                }
163
164                if ((component == null && !freshPage) || (component != null && component.getPage() != page))
165                {
166                        throw new ComponentNotFoundException("Component '" + getComponentPath()
167                                        + "' has been removed from page.");
168                }
169
170                RedirectPolicy policy = page.isPageStateless()
171                        ? RedirectPolicy.NEVER_REDIRECT
172                        : RedirectPolicy.AUTO_REDIRECT;
173
174                boolean blockIfExpired = component != null && !component.canCallListenerAfterExpiry();
175
176                boolean lateComponent = component == null && freshPage;
177
178                if ((pageComponentProvider.wasExpired() && blockIfExpired) || lateComponent)
179                {
180                        // A request listener is invoked on an expired page or the component couldn't be
181                        // determined. The best we can do is to re-paint the newly constructed page.
182                        // Reference: WICKET-4454, WICKET-6288
183
184                        if (LOG.isDebugEnabled())
185                        {
186                                LOG.debug(
187                                        "An IRequestListener was called but its page/component({}) couldn't be resolved. "
188                                                + "Scheduling re-create of the page and ignoring the listener interface...",
189                                        getComponentPath());
190                        }
191
192                        if (isAjax)
193                        {
194                                policy = RedirectPolicy.ALWAYS_REDIRECT;
195                        }
196
197                        requestCycle.scheduleRequestHandlerAfterCurrent(new RenderPageRequestHandler(
198                                new PageProvider(page), policy));
199                        return;
200                }
201
202                invokeListener(requestCycle, policy, isAjax);
203        }
204
205        private void invokeListener(IRequestCycle requestCycle, RedirectPolicy policy, boolean ajax)
206        {
207                if (getBehaviorIndex() == null)
208                {
209                        invoke(requestCycle, policy, ajax, getComponent());
210                }
211                else
212                {
213                        final Behavior behavior;
214                        try
215                        {
216                                behavior = getComponent().getBehaviorById(behaviorId);
217                        }
218                        catch (IndexOutOfBoundsException e)
219                        {
220                                throw new WicketRuntimeException("Couldn't find component behavior.", e);
221                        }
222                        invoke(requestCycle, policy, ajax, getComponent(), behavior);
223                }
224        }
225        
226        /**
227         * Invokes a given interface on a component.
228         * 
229         * @param rcomponent
230         *            The component
231         * 
232         * @throws ListenerInvocationNotAllowedException
233         *             when listener invocation attempted on a component that does not allow it
234         */
235        private final void invoke(final IRequestCycle requestCycle, RedirectPolicy policy, boolean ajax, final IRequestableComponent rcomponent)
236        {
237                // we are in Wicket core land
238                final Component component = (Component)rcomponent;
239
240                if (!component.canCallListener())
241                {
242                        // just return so that we have a silent fail and just re-render the
243                        // page
244                        LOG.info("component not enabled or visible; ignoring call. Component: " + component);
245                        throw new ListenerInvocationNotAllowedException(component, null,
246                                "Component rejected interface invocation");
247                }
248
249                internalInvoke(requestCycle, policy, ajax, component, component);
250        }
251
252        /**
253         * Invokes a given interface on a component's behavior.
254         * 
255         * @param rcomponent
256         *            The component
257         * @param behavior
258         * @throws ListenerInvocationNotAllowedException
259         *             when listener invocation attempted on a component that does not allow it
260         */
261        private final void invoke(final IRequestCycle requestCycle, RedirectPolicy policy, boolean ajax, final IRequestableComponent rcomponent, final Behavior behavior)
262        {
263                // we are in Wicket core land
264                final Component component = (Component)rcomponent;
265
266                if (!behavior.canCallListener(component))
267                {
268                        LOG.warn("behavior not enabled; ignore call. Behavior {} at component {}", behavior,
269                                component);
270                        throw new ListenerInvocationNotAllowedException(component, behavior,
271                                "Behavior rejected interface invocation. ");
272                }
273
274                internalInvoke(requestCycle, policy, ajax, component, behavior);
275        }
276
277        private void internalInvoke(final IRequestCycle requestCycle, RedirectPolicy policy, boolean ajax, final Component component, final Object target)
278        {
279                // save a reference to the page because the component can be removed
280                // during the invocation of the listener and thus lose its parent
281                Page page = component.getPage();
282
283                // initialization is required for stateless pages
284                if (!page.isInitialized())
285                {
286                        page.internalInitialize();
287                }
288
289                IRequestListener requestListener = (IRequestListener)target;
290                
291                if (requestListener.rendersPage() && !ajax)
292                {
293                        // schedule page render after current request handler is done. this can be
294                        // overridden during invocation of listener
295                        // method (i.e. by calling RequestCycle#setResponsePage)
296                        requestCycle.scheduleRequestHandlerAfterCurrent(new RenderPageRequestHandler(
297                                new PageProvider(page), policy));
298                }
299
300                requestListener.onRequest();
301        }
302
303        @Override
304        public final boolean isPageInstanceCreated()
305        {
306                return pageComponentProvider.hasPageInstance();
307        }
308
309        @Override
310        public final String getComponentPath()
311        {
312                return pageComponentProvider.getComponentPath();
313        }
314
315        @Override
316        public final Integer getRenderCount()
317        {
318                return pageComponentProvider.getRenderCount();
319        }
320
321        @Override
322        public ListenerLogData getLogData()
323        {
324                return logData;
325        }
326}