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.lang.reflect.Modifier;
020import java.util.List;
021
022import org.apache.wicket.protocol.http.WebApplication;
023import org.apache.wicket.request.Request;
024import org.apache.wicket.request.Url;
025import org.apache.wicket.request.component.IRequestablePage;
026import org.apache.wicket.request.mapper.info.PageComponentInfo;
027import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
028import org.apache.wicket.request.mapper.parameter.PageParameters;
029import org.apache.wicket.request.mapper.parameter.PageParametersEncoder;
030import org.apache.wicket.util.lang.Args;
031import org.apache.wicket.util.lang.PackageName;
032import org.apache.wicket.util.string.Strings;
033
034/**
035 * A request mapper that mounts all bookmarkable pages in a given package.
036 * <p>
037 * To mount this mapper onto a path use the {@link WebApplication#mountPackage(String, Class)}, ex:
038 *
039 * <pre>
040 * MyApp#init() {
041 *
042 *   super.init();
043 *   mountPackage(&quot;/my/path&quot;, MyPage.class);
044 * }
045 * </pre>
046 *
047 * will result in urls like {@code /my/path/MyPage}
048 * </p>
049 *
050 * <pre>
051 *  Page Class - Render (BookmarkablePageRequestHandler)
052 *  /MyPage
053 *  (will redirect to hybrid alternative if page is not stateless)
054 *
055 *  Page Instance - Render Hybrid (RenderPageRequestHandler for pages that were created using bookmarkable URLs)
056 *  /MyPage?2
057 *
058 *  Page Instance - Bookmarkable Listener (BookmarkableListenerRequestHandler)
059 *  /MyPage?2-click-foo-bar-baz
060 *  /MyPage?2-click.1-foo-bar-baz (1 is behavior index)
061 *  (these will redirect to hybrid if page is not stateless)
062 * </pre>
063 */
064public class PackageMapper extends AbstractBookmarkableMapper
065{
066        /**
067         * the name of the package for which all bookmarkable pages should be mounted
068         */
069        private final PackageName packageName;
070
071        /**
072         * Constructor.
073         *
074         * @param packageName
075         */
076        public PackageMapper(String mountPath, final PackageName packageName)
077        {
078                this(mountPath, packageName, new PageParametersEncoder());
079        }
080
081        /**
082         * Constructor.
083         *
084         * @param packageName
085         * @param pageParametersEncoder
086         */
087        public PackageMapper(String mountPath, final PackageName packageName,
088                final IPageParametersEncoder pageParametersEncoder)
089        {
090                super(mountPath, pageParametersEncoder);
091
092                Args.notNull(packageName, "packageName");
093
094                this.packageName = packageName;
095        }
096
097        @Override
098        protected Url buildUrl(UrlInfo info)
099        {
100                Class<? extends IRequestablePage> pageClass = info.getPageClass();
101                PackageName pageClassPackageName = PackageName.forClass(pageClass);
102                if (pageClassPackageName.equals(packageName))
103                {
104                        Url url = new Url();
105                        for (String s : mountSegments)
106                        {
107                                url.getSegments().add(s);
108                        }
109
110                        String fullyQualifiedClassName = pageClass.getName();
111                        String packageRelativeClassName = fullyQualifiedClassName;
112                        int packageNameLength = packageName.getName().length();
113                        if (packageNameLength > 0)
114                        {
115                                packageRelativeClassName = fullyQualifiedClassName.substring(packageNameLength + 1);
116                        }
117                        packageRelativeClassName = transformForUrl(packageRelativeClassName);
118                        url.getSegments().add(packageRelativeClassName);
119                        encodePageComponentInfo(url, info.getPageComponentInfo());
120
121                        PageParameters copy = newPageParameters();
122                        copy.mergeWith(info.getPageParameters());
123                        if (setPlaceholders(copy, url) == false)
124                        {
125                                // mandatory parameter is not provided => cannot build Url
126                                return null;
127                        }
128
129                        return encodePageParameters(url, copy, pageParametersEncoder);
130                }
131
132                return null;
133        }
134
135        @Override
136        protected UrlInfo parseRequest(Request request)
137        {
138                Url url = request.getUrl();
139                if (urlStartsWithMountedSegments(url))
140                {
141                        // try to extract page and component information from URL
142                        PageComponentInfo info = getPageComponentInfo(url);
143
144                        final List<String> segments = url.getSegments();
145                        if (segments.size() <= mountSegments.length)
146                        {
147                                return null;
148                        }
149
150                        // load the page class
151                        String name = segments.get(mountSegments.length);
152
153                        if (Strings.isEmpty(name))
154                        {
155                                return null;
156                        }
157
158                        String className = cleanClassName(name);
159
160                        if (isValidClassName(className) == false)
161                        {
162                                return null;
163                        }
164
165                        className = transformFromUrl(className);
166                        String fullyQualifiedClassName = packageName.getName() + '.' + className;
167                        Class<? extends IRequestablePage> pageClass = getPageClass(fullyQualifiedClassName);
168
169                        if (pageClass != null && Modifier.isAbstract(pageClass.getModifiers()) == false &&
170                                IRequestablePage.class.isAssignableFrom(pageClass))
171                        {
172                                // extract the PageParameters from URL if there are any
173                                Url urlWithoutPageSegment = new Url(url);
174                                urlWithoutPageSegment.getSegments().remove(mountSegments.length);
175                                Request requestWithoutPageSegment = request.cloneWithUrl(urlWithoutPageSegment);
176                                PageParameters pageParameters = extractPageParameters(requestWithoutPageSegment, urlWithoutPageSegment);
177
178                                return new UrlInfo(info, pageClass, pageParameters);
179                        }
180                }
181                return null;
182        }
183
184        /**
185         * filter out invalid class names for package mapper. getting trash for class names
186         * can e.g. happen when the home page is in the same package that is mounted by package mapper
187         * but the request was previously mapped by e.g. {@link HomePageMapper}. We then get some
188         * strange url like '/example/..' and wicket tries to look up class name '..'.
189         * <p/>
190         *  @see <a href="https://issues.apache.org/jira/browse/WICKET-4303">WICKET-4303</a>
191         *  <p/>
192         */
193        private boolean isValidClassName(String className)
194        {
195                // darn simple check - feel free to enhance this method to your needs
196                if (Strings.isEmpty(className))
197                {
198                        return false;
199                }
200                // java class names never start with '.'
201                if (className.startsWith("."))
202                {
203                        return false;
204                }
205                return true;
206        }
207
208        /**
209         * Gives a chance to specializations of this mapper to transform the alias of the class name to
210         * the real class name
211         *
212         * @param classNameAlias
213         *            the alias for the class name
214         * @return the real class name
215         */
216        protected String transformFromUrl(final String classNameAlias)
217        {
218                return classNameAlias;
219        }
220
221        /**
222         * Gives a chance to specializations of this mapper to transform the real class name to an alias
223         * which is prettier to represent in the Url
224         *
225         * @param className
226         *            the real class name
227         * @return the class name alias
228         */
229        protected String transformForUrl(final String className)
230        {
231                return className;
232        }
233
234        @Override
235        protected boolean pageMustHaveBeenCreatedBookmarkable()
236        {
237                return false;
238        }
239        
240        @Override
241        protected boolean checkPageClass(Class<? extends IRequestablePage> pageClass)
242        {
243                if (pageClass == null)
244                {
245                        return false;
246                }
247                PackageName pageClassPackageName = PackageName.forClass(pageClass);
248                return packageName.equals(pageClassPackageName);
249        }
250}