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.mapper;
018
019import java.util.Objects;
020import java.util.function.Supplier;
021
022import org.apache.wicket.core.request.handler.ListenerRequestHandler;
023import org.apache.wicket.request.IRequestHandler;
024import org.apache.wicket.request.Request;
025import org.apache.wicket.request.Url;
026import org.apache.wicket.request.component.IRequestablePage;
027import org.apache.wicket.request.mapper.info.ComponentInfo;
028import org.apache.wicket.request.mapper.info.PageComponentInfo;
029import org.apache.wicket.request.mapper.info.PageInfo;
030import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
031import org.apache.wicket.request.mapper.parameter.PageParameters;
032import org.apache.wicket.request.mapper.parameter.PageParametersEncoder;
033import org.apache.wicket.util.lang.Args;
034import org.apache.wicket.util.reference.ClassReference;
035import org.apache.wicket.util.string.Strings;
036
037/**
038 * Encoder for mounted URL. The mount path can contain parameter placeholders, i.e.
039 * <code>/mount/${foo}/path</code>. In that case the appropriate segment from the URL will be
040 * accessible as named parameter "foo" in the {@link PageParameters}. Similarly when the URL is
041 * constructed, the second segment will contain the value of the "foo" named page parameter.
042 * Optional parameters are denoted by using a # instead of $: <code>/mount/#{foo}/path/${bar}</code>
043 * has an optional {@code foo} parameter, a fixed {@code /path/} part and a required {@code bar}
044 * parameter. When in doubt, parameters are matched from left to right, where required parameters
045 * are matched before optional parameters, and optional parameters eager (from left to right).
046 * <p>
047 * Decodes and encodes the following URLs:
048 * 
049 * <pre>
050 *  Page Class - Render (BookmarkablePageRequestHandler for mounted pages)
051 *  /mount/point
052 *  (these will redirect to hybrid alternative if page is not stateless)
053 * 
054 *  IPage Instance - Render Hybrid (RenderPageRequestHandler for mounted pages)
055 *  /mount/point?2
056 * 
057 *  IPage Instance - Bookmarkable Listener (BookmarkableListenerRequestHandler for mounted pages)
058 *  /mount/point?2-click-foo-bar-baz
059 *  /mount/point?2-5.click.1-foo-bar-baz (1 is behavior index, 5 is render count)
060 *  (these will redirect to hybrid if page is not stateless)
061 * </pre>
062 * 
063 * @author Matej Knopp
064 */
065public class MountedMapper extends AbstractBookmarkableMapper
066{
067        /** bookmarkable page class. */
068        private final Supplier<Class<? extends IRequestablePage>> pageClassProvider;
069
070        /**
071         * Construct.
072         * 
073         * @param mountPath
074         * @param pageClass
075         */
076        public MountedMapper(String mountPath, Class<? extends IRequestablePage> pageClass)
077        {
078                this(mountPath, pageClass, new PageParametersEncoder());
079        }
080
081        /**
082         * Construct.
083         * 
084         * @param mountPath
085         * @param pageClassProvider
086         */
087        public MountedMapper(String mountPath,
088                Supplier<Class<? extends IRequestablePage>> pageClassProvider)
089        {
090                this(mountPath, pageClassProvider, new PageParametersEncoder());
091        }
092
093        /**
094         * Construct.
095         * 
096         * @param mountPath
097         * @param pageClass
098         * @param pageParametersEncoder
099         */
100        public MountedMapper(String mountPath, Class<? extends IRequestablePage> pageClass,
101                IPageParametersEncoder pageParametersEncoder)
102        {
103                this(mountPath, new ClassReference(pageClass), pageParametersEncoder);
104        }
105
106        /**
107         * Construct.
108         * 
109         * @param mountPath
110         * @param pageClassProvider
111         * @param pageParametersEncoder
112         */
113        public MountedMapper(String mountPath,
114                Supplier<Class<? extends IRequestablePage>> pageClassProvider,
115                IPageParametersEncoder pageParametersEncoder)
116        {
117                super(mountPath, pageParametersEncoder);
118
119                Args.notNull(pageClassProvider, "pageClassProvider");
120
121                this.pageClassProvider = pageClassProvider;
122        }
123
124        @Override
125        protected UrlInfo parseRequest(Request request)
126        {
127                Url url = request.getUrl();
128
129                // when redirect to buffer/render is active and redirectFromHomePage returns true
130                // check mounted class against the home page class. if it matches let wicket redirect
131                // to the mounted URL
132                if (redirectFromHomePage() && checkHomePage(url))
133                {
134                        return new UrlInfo(null, getContext().getHomePageClass(), newPageParameters());
135                }
136                // check if the URL starts with the proper segments
137                else if (urlStartsWithMountedSegments(url))
138                {
139                        // try to extract page and component information from URL
140                        PageComponentInfo info = getPageComponentInfo(url);
141                        Class<? extends IRequestablePage> pageClass = getPageClass();
142                        PageParameters pageParameters = extractPageParameters(request, url);
143
144                        return new UrlInfo(info, pageClass, pageParameters);
145                }
146                else
147                {
148                        return null;
149                }
150        }
151
152        @Override
153        public Url mapHandler(IRequestHandler requestHandler)
154        {
155                Url url = super.mapHandler(requestHandler);
156
157                if (url == null && requestHandler instanceof ListenerRequestHandler &&
158                        getRecreateMountedPagesAfterExpiry())
159                {
160                        ListenerRequestHandler handler = (ListenerRequestHandler)requestHandler;
161                        IRequestablePage page = handler.getPage();
162                        if (checkPageInstance(page))
163                        {
164                                Integer renderCount = null;
165                                if (handler.includeRenderCount())
166                                {
167                                        renderCount = page.getRenderCount();
168                                }
169
170                                String componentPath = handler.getComponentPath();
171                                PageInfo pageInfo = getPageInfo(handler);
172                                ComponentInfo componentInfo = new ComponentInfo(renderCount, componentPath, handler.getBehaviorIndex());
173                                PageComponentInfo pageComponentInfo = new PageComponentInfo(pageInfo, componentInfo);
174                                PageParameters parameters = newPageParameters();
175                                parameters.mergeWith(page.getPageParameters());
176                                UrlInfo urlInfo = new UrlInfo(pageComponentInfo, page.getClass(),
177                                        parameters.mergeWith(handler.getPageParameters()));
178                                url = buildUrl(urlInfo);
179                        }
180                }
181
182                return url;
183        }
184
185        /**
186         * @see AbstractBookmarkableMapper#buildUrl(AbstractBookmarkableMapper.UrlInfo)
187         */
188        @Override
189        protected Url buildUrl(UrlInfo info)
190        {
191                Url url = new Url();
192                for (String s : mountSegments)
193                {
194                        url.getSegments().add(s);
195                }
196                encodePageComponentInfo(url, info.getPageComponentInfo());
197
198                PageParameters copy = newPageParameters();
199                copy.mergeWith(info.getPageParameters());
200                if (setPlaceholders(copy, url) == false)
201                {
202                        // mandatory parameter is not provided => cannot build Url
203                        return null;
204                }
205
206                return encodePageParameters(url, copy, pageParametersEncoder);
207        }
208
209        /**
210         * Check if the URL is for home page and the home page class match mounted class. If so,
211         * redirect to mounted URL.
212         * 
213         * @param url
214         * @return request handler or <code>null</code>
215         */
216        private boolean checkHomePage(Url url)
217        {
218                if (url.getSegments().isEmpty() && url.getQueryParameters().isEmpty())
219                {
220                        // this is home page
221                        if (getPageClass().equals(getContext().getHomePageClass()))
222                        {
223                                return true;
224                        }
225                }
226                return false;
227        }
228
229        /**
230         * If this method returns <code>true</code> and application home page class is same as the class
231         * mounted with this encoder, request to home page will result in a redirect to the mounted
232         * path.
233         * 
234         * @return whether this encode should respond to home page request when home page class is same
235         *         as mounted class.
236         */
237        protected boolean redirectFromHomePage()
238        {
239                return true;
240        }
241
242        /**
243         * @see AbstractBookmarkableMapper#pageMustHaveBeenCreatedBookmarkable()
244         */
245        @Override
246        protected boolean pageMustHaveBeenCreatedBookmarkable()
247        {
248                return false;
249        }
250
251        /**
252         * @see AbstractBookmarkableMapper#checkPageClass(java.lang.Class)
253         */
254        @Override
255        protected boolean checkPageClass(Class<? extends IRequestablePage> pageClass)
256        {
257                return Objects.equals(pageClass, this.getPageClass());
258        }
259
260        private Class<? extends IRequestablePage> getPageClass()
261        {
262                return pageClassProvider.get();
263        }
264
265        @Override
266        public String toString()
267        {
268                return "MountedMapper [mountSegments=" + Strings.join("/", mountSegments) + "]";
269        }
270}