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.Application;
020import org.apache.wicket.core.request.mapper.IPageSource;
021import org.apache.wicket.core.request.mapper.StalePageException;
022import org.apache.wicket.protocol.http.PageExpiredException;
023import org.apache.wicket.request.IRequestHandler;
024import org.apache.wicket.request.IRequestMapper;
025import org.apache.wicket.request.component.IRequestablePage;
026import org.apache.wicket.request.mapper.parameter.PageParameters;
027import org.apache.wicket.util.io.IClusterable;
028import org.apache.wicket.util.lang.Args;
029
030/**
031 * Provides page instance for request handlers. Each of the constructors has just enough information
032 * to get existing or create new page instance. Requesting or creating page instance is deferred
033 * until {@link #getPageInstance()} is called.
034 * <p>
035 * Purpose of this class is to reduce complexity of both {@link IRequestMapper}s and
036 * {@link IRequestHandler}s. {@link IRequestMapper} examines the URL, gathers all relevant
037 * information about the page in the URL (combination of page id, page class, page parameters and
038 * render count), creates {@link PageProvider} object and creates a {@link IRequestHandler} instance
039 * that can use the {@link PageProvider} to access the page.
040 * <p>
041 * Apart from simplifying {@link IRequestMapper}s and {@link IRequestHandler}s {@link PageProvider}
042 * also helps performance because creating or obtaining page from {@link org.apache.wicket.page.IPageManager} is delayed
043 * until the {@link IRequestHandler} actually requires the page.
044 * 
045 * @author Matej Knopp
046 */
047public class PageProvider implements IPageProvider, IClusterable
048{
049        private static final long serialVersionUID = 1L;
050
051        private final Integer renderCount;
052
053        private final Integer pageId;
054
055        private transient IPageSource pageSource;
056
057        private Class<? extends IRequestablePage> pageClass;
058
059        private PageParameters pageParameters;
060
061        private transient Provision provision;
062
063        /**
064         * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
065         * will return page instance with specified id.
066         * 
067         * @param pageId
068         * @param renderCount
069         *            optional argument
070         */
071        public PageProvider(final Integer pageId, final Integer renderCount)
072        {
073                this.pageId = pageId;
074                this.renderCount = renderCount;
075        }
076
077        /**
078         * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
079         * will return page instance with specified id if it exists and it's class matches pageClass. If
080         * none of these is true new page instance will be created.
081         * 
082         * @param pageId
083         * @param pageClass
084         * @param renderCount
085         *            optional argument
086         */
087        public PageProvider(final Integer pageId, final Class<? extends IRequestablePage> pageClass,
088                Integer renderCount)
089        {
090                this(pageId, pageClass, new PageParameters(), renderCount);
091        }
092
093        /**
094         * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
095         * will return page instance with specified id if it exists and it's class matches pageClass. If
096         * none of these is true new page instance will be created.
097         * 
098         * @param pageId
099         * @param pageClass
100         * @param pageParameters
101         * @param renderCount
102         *            optional argument
103         */
104        public PageProvider(final Integer pageId, final Class<? extends IRequestablePage> pageClass,
105                final PageParameters pageParameters, final Integer renderCount)
106        {
107                this.pageId = pageId;
108                setPageClass(pageClass);
109                setPageParameters(pageParameters);
110                this.renderCount = renderCount;
111        }
112
113        /**
114         * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
115         * will return new instance of page with specified class.
116         * 
117         * @param pageClass
118         * @param pageParameters
119         */
120        public PageProvider(final Class<? extends IRequestablePage> pageClass,
121                final PageParameters pageParameters)
122        {
123                setPageClass(pageClass);
124                if (pageParameters != null)
125                {
126                        setPageParameters(pageParameters);
127                }
128                pageId = null;
129                renderCount = null;
130        }
131
132        /**
133         * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
134         * will return new instance of page with specified class.
135         * 
136         * @param pageClass
137         */
138        public PageProvider(Class<? extends IRequestablePage> pageClass)
139        {
140                this(pageClass, null);
141        }
142
143        /**
144         * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
145         * will return the given page instance.
146         * 
147         * @param page
148         */
149        public PageProvider(IRequestablePage page)
150        {
151                Args.notNull(page, "page");
152
153                provision = new Provision().resolveTo(page);
154                pageId = page.getPageId();
155                renderCount = page.getRenderCount();
156        }
157
158        private Provision getProvision()
159        {
160                if (provision == null)
161                {
162                        provision = new Provision().resolve();
163                }
164                return provision;
165        }
166
167        @Override
168        public IRequestablePage getPageInstance() throws PageExpiredException
169        {
170                IRequestablePage page = getProvision().getPage();
171                
172                if (page == null && wasExpired()) 
173                {
174                        throw new PageExpiredException("Page with id '" + pageId + "' has expired.");
175                }
176                
177                return page;
178        }
179
180        @Override
181        public PageParameters getPageParameters() throws PageExpiredException
182        {
183                if (pageParameters != null)
184                {
185                        return pageParameters;
186                }
187                else if (hasPageInstance())
188                {
189                        return getPageInstance().getPageParameters();
190                }
191                else
192                {
193                        return null;
194                }
195        }
196
197        /**
198         * If this provider returns existing page, regardless if it was already created by PageProvider
199         * itself or is or can be found in the data store. The only guarantee is that by calling
200         * {@link PageProvider#getPageInstance()} this provider will return an existing instance and no
201         * page will be created.
202         * 
203         * @return if provides an existing page
204         */
205        @Override
206        public final boolean hasPageInstance()
207        {
208                if (provision != null || pageId != null)
209                {
210                        return getProvision().didResolveToPage();
211                }
212                else
213                {
214                        return false;
215                }
216        }
217
218        /**
219         * Returns whether or not the page instance held by this provider has been instantiated by the
220         * provider.
221         * 
222         * @return {@code true} iff the page instance held by this provider was instantiated by the
223         *         provider
224         */
225        @Override
226        public final boolean doesProvideNewPage()
227        {
228                return getProvision().doesProvideNewPage();
229        }
230
231        @Override
232        public boolean wasExpired()
233        {
234                return pageId != null && getProvision().didFailToFindStoredPage();
235        }
236
237        @Override
238        public Class<? extends IRequestablePage> getPageClass() throws PageExpiredException
239        {
240                if (pageClass != null)
241                {
242                        return pageClass;
243                }
244                else
245                {
246                        return getPageInstance().getClass();
247                }
248        }
249
250        protected IPageSource getPageSource()
251        {
252                if (pageSource != null)
253                {
254                        return pageSource;
255                }
256                if (Application.exists())
257                {
258                        return Application.get().getMapperContext();
259                }
260                else
261                {
262                        throw new IllegalStateException(
263                                "No application is bound to current thread. Call setPageSource() to manually assign pageSource to this provider.");
264                }
265        }
266
267
268        /**
269         * Detaches the page if it has been loaded (that means either
270         * {@link #PageProvider(IRequestablePage)} constructor has been used or
271         * {@link #getPageInstance()} has been called).
272         */
273        @Override
274        public void detach()
275        {
276                if (provision != null)
277                {
278                        provision.detach();
279                        provision = null;
280                }
281        }
282
283        /**
284         * If the {@link PageProvider} is used outside request thread (thread that does not have
285         * application instance assigned) it is necessary to specify a {@link IPageSource} instance so
286         * that {@link PageProvider} knows how to get a page instance.
287         * 
288         * @param pageSource
289         */
290        public void setPageSource(IPageSource pageSource)
291        {
292                if (provision != null)
293                {
294                        throw new IllegalStateException("A page was already provided.");
295                }
296                this.pageSource = pageSource;
297        }
298
299        /**
300         * 
301         * @param pageClass
302         */
303        private void setPageClass(Class<? extends IRequestablePage> pageClass)
304        {
305                Args.notNull(pageClass, "pageClass");
306
307                this.pageClass = pageClass;
308        }
309
310        /**
311         * 
312         * @param pageParameters
313         */
314        protected void setPageParameters(PageParameters pageParameters)
315        {
316                this.pageParameters = pageParameters;
317        }
318
319        /**
320         * 
321         * @return page id
322         */
323        @Override
324        public Integer getPageId()
325        {
326                return pageId;
327        }
328
329        @Override
330        public Integer getRenderCount()
331        {
332                return renderCount;
333        }
334
335        @Override
336        public String toString()
337        {
338                return "PageProvider{" + "renderCount=" + renderCount + ", pageId=" + pageId
339                        + ", pageClass=" + pageClass + ", pageParameters=" + pageParameters + '}';
340        }
341
342        /**
343         * A provision is the work necessary to provide a page. It includes to resolve parameters to a
344         * page, to track the resolution metadata and to keep a reference of the resolved page.
345         * 
346         * The logic based on {@link PageProvider}'s parameters:
347         * 
348         * - having an stored page id, the stored page is provided
349         * 
350         * - having only a page class, a new instance of it is provided
351         * 
352         * - having non stored page id plus page class, a new instance of the page class is provided
353         * 
354         * - having non stored page id and no page class, no page is provided
355         * 
356         * - being a page instance, the instance itself will be the provided page
357         *
358         * @author pedro
359         */
360        private class Provision
361        {
362                transient IRequestablePage page;
363                boolean failedToFindStoredPage;
364
365                IRequestablePage getPage()
366                {
367                        if (page == null && doesProvideNewPage())
368                        {
369                                page = getPageSource().newPageInstance(pageClass, pageParameters);
370                        }
371                        return page;
372                }
373
374                boolean didResolveToPage()
375                {
376                        return page != null;
377                }
378
379                boolean doesProvideNewPage()
380                {
381                        return (pageId == null || failedToFindStoredPage) && pageClass != null;
382                }
383
384                boolean didFailToFindStoredPage()
385                {
386                        return failedToFindStoredPage;
387                }
388
389                Provision resolveTo(IRequestablePage page)
390                {
391                        this.page = page;
392
393                        return this;
394                }
395
396                Provision resolve()
397                {
398
399                        if (pageId != null)
400                        {
401                                IRequestablePage stored = getPageSource().getPageInstance(pageId);
402                                if (stored != null && (pageClass == null || pageClass.equals(stored.getClass())))
403                                {
404
405                                        page = stored;
406
407                                        if (renderCount != null && page.getRenderCount() != renderCount)
408                                                throw new StalePageException(page);
409                                }
410
411                                failedToFindStoredPage = page == null;
412                        }
413
414                        return this;
415                }
416
417                void detach()
418                {
419                        if (page != null)
420                        {
421                                page.detach();
422                        }
423                }
424
425        }
426}