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}