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("/my/path", 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}