001 // Copyright 2009, 2010, 2011 The Apache Software Foundation 002 // 003 // Licensed under the Apache License, Version 2.0 (the "License"); 004 // you may not use this file except in compliance with the License. 005 // You may obtain a copy of the License at 006 // 007 // http://www.apache.org/licenses/LICENSE-2.0 008 // 009 // Unless required by applicable law or agreed to in writing, software 010 // distributed under the License is distributed on an "AS IS" BASIS, 011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 // See the License for the specific language governing permissions and 013 // limitations under the License. 014 015 package org.apache.tapestry5.internal.services; 016 017 import org.apache.tapestry5.*; 018 import org.apache.tapestry5.internal.InternalConstants; 019 import org.apache.tapestry5.ioc.annotations.Symbol; 020 import org.apache.tapestry5.ioc.internal.util.InternalUtils; 021 import org.apache.tapestry5.services.*; 022 import org.apache.tapestry5.services.security.ClientWhitelist; 023 024 import java.util.Locale; 025 import java.util.regex.Matcher; 026 import java.util.regex.Pattern; 027 028 public class ComponentEventLinkEncoderImpl implements ComponentEventLinkEncoder 029 { 030 private final ComponentClassResolver componentClassResolver; 031 032 private final ContextPathEncoder contextPathEncoder; 033 034 private final LocalizationSetter localizationSetter; 035 036 private final Request request; 037 038 private final Response response; 039 040 private final RequestSecurityManager requestSecurityManager; 041 042 private final BaseURLSource baseURLSource; 043 044 private final PersistentLocale persistentLocale; 045 046 private final boolean encodeLocaleIntoPath; 047 048 private final MetaDataLocator metaDataLocator; 049 050 private final ClientWhitelist clientWhitelist; 051 052 private final String applicationFolder; 053 054 private final String applicationFolderPrefix; 055 056 private static final int BUFFER_SIZE = 100; 057 058 private static final char SLASH = '/'; 059 060 // A beast that recognizes all the elements of a path in a single go. 061 // We skip the leading slash, then take the next few terms (until a dot or a colon) 062 // as the page name. Then there's a sequence that sees a dot 063 // and recognizes the nested component id (which may be missing), which ends 064 // at the colon, or at the slash (or the end of the string). The colon identifies 065 // the event name (the event name is also optional). A valid path will always have 066 // a nested component id or an event name (or both) ... when both are missing, then the 067 // path is most likely a page render request. After the optional event name, 068 // the next piece is the action context, which is the remainder of the path. 069 070 private final Pattern COMPONENT_EVENT_REQUEST_PATH_PATTERN; 071 072 // Constants for the match groups in the above pattern. 073 private static final int LOGICAL_PAGE_NAME = 1; 074 private static final int NESTED_ID = 6; 075 private static final int EVENT_NAME = 9; 076 private static final int CONTEXT = 11; 077 078 public ComponentEventLinkEncoderImpl(ComponentClassResolver componentClassResolver, 079 ContextPathEncoder contextPathEncoder, LocalizationSetter localizationSetter, Request request, 080 Response response, RequestSecurityManager requestSecurityManager, BaseURLSource baseURLSource, 081 PersistentLocale persistentLocale, @Symbol(SymbolConstants.ENCODE_LOCALE_INTO_PATH) 082 boolean encodeLocaleIntoPath, @Symbol(SymbolConstants.APPLICATION_FOLDER) String applicationFolder, MetaDataLocator metaDataLocator, ClientWhitelist clientWhitelist) 083 { 084 this.componentClassResolver = componentClassResolver; 085 this.contextPathEncoder = contextPathEncoder; 086 this.localizationSetter = localizationSetter; 087 this.request = request; 088 this.response = response; 089 this.requestSecurityManager = requestSecurityManager; 090 this.baseURLSource = baseURLSource; 091 this.persistentLocale = persistentLocale; 092 this.encodeLocaleIntoPath = encodeLocaleIntoPath; 093 this.applicationFolder = applicationFolder; 094 this.metaDataLocator = metaDataLocator; 095 this.clientWhitelist = clientWhitelist; 096 097 boolean hasAppFolder = applicationFolder.equals(""); 098 099 applicationFolderPrefix = hasAppFolder ? null : SLASH + applicationFolder; 100 101 String applicationFolderPattern = hasAppFolder ? "" : applicationFolder + SLASH; 102 103 COMPONENT_EVENT_REQUEST_PATH_PATTERN = Pattern.compile( 104 105 "^/" + // The leading slash is recognized but skipped 106 applicationFolderPattern + // The folder containing the application (TAP5-743) 107 "(((\\w(?:\\w|-)*)/)*(\\w+))" + // A series of folder names (which allow dashes) leading up to the page name, forming 108 // the logical page name (may include the locale name) 109 "(\\.(\\w+(\\.\\w+)*))?" + // The first dot separates the page name from the nested 110 // component id 111 "(\\:(\\w+))?" + // A colon, then the event type 112 "(/(.*))?", // A slash, then the action context 113 Pattern.COMMENTS); 114 } 115 116 public Link createPageRenderLink(PageRenderRequestParameters parameters) 117 { 118 StringBuilder builder = new StringBuilder(BUFFER_SIZE); 119 120 // Build up the absolute URI. 121 122 String activePageName = parameters.getLogicalPageName(); 123 124 builder.append(request.getContextPath()); 125 126 encodeAppFolderAndLocale(builder); 127 128 builder.append(SLASH); 129 130 String encodedPageName = encodePageName(activePageName); 131 132 builder.append(encodedPageName); 133 134 appendContext(encodedPageName.length() > 0, parameters.getActivationContext(), builder); 135 136 Link link = new LinkImpl(builder.toString(), false, requestSecurityManager.checkPageSecurity(activePageName), 137 response, contextPathEncoder, baseURLSource); 138 139 if (parameters.isLoopback()) 140 { 141 link.addParameter(TapestryConstants.PAGE_LOOPBACK_PARAMETER_NAME, "t"); 142 } 143 144 return link; 145 } 146 147 private void encodeAppFolderAndLocale(StringBuilder builder) 148 { 149 if (!applicationFolder.equals("")) 150 { 151 builder.append(SLASH).append(applicationFolder); 152 } 153 154 if (encodeLocaleIntoPath) 155 { 156 Locale locale = persistentLocale.get(); 157 158 if (locale != null) 159 { 160 builder.append(SLASH); 161 builder.append(locale.toString()); 162 } 163 } 164 } 165 166 private String encodePageName(String pageName) 167 { 168 if (pageName.equalsIgnoreCase("index")) 169 return ""; 170 171 String encoded = pageName.toLowerCase(); 172 173 if (!encoded.endsWith("/index")) 174 return encoded; 175 176 return encoded.substring(0, encoded.length() - 6); 177 } 178 179 public Link createComponentEventLink(ComponentEventRequestParameters parameters, boolean forForm) 180 { 181 StringBuilder builder = new StringBuilder(BUFFER_SIZE); 182 183 // Build up the absolute URI. 184 185 String activePageName = parameters.getActivePageName(); 186 String containingPageName = parameters.getContainingPageName(); 187 String eventType = parameters.getEventType(); 188 189 String nestedComponentId = parameters.getNestedComponentId(); 190 boolean hasComponentId = InternalUtils.isNonBlank(nestedComponentId); 191 192 builder.append(request.getContextPath()); 193 194 encodeAppFolderAndLocale(builder); 195 196 builder.append(SLASH); 197 builder.append(activePageName.toLowerCase()); 198 199 if (hasComponentId) 200 { 201 builder.append('.'); 202 builder.append(nestedComponentId); 203 } 204 205 if (!hasComponentId || !eventType.equals(EventConstants.ACTION)) 206 { 207 builder.append(":"); 208 builder.append(encodePageName(eventType)); 209 } 210 211 appendContext(true, parameters.getEventContext(), builder); 212 213 Link result = new LinkImpl(builder.toString(), forForm, 214 requestSecurityManager.checkPageSecurity(activePageName), response, contextPathEncoder, baseURLSource); 215 216 EventContext pageActivationContext = parameters.getPageActivationContext(); 217 218 if (pageActivationContext.getCount() != 0) 219 { 220 // Reuse the builder 221 builder.setLength(0); 222 appendContext(true, pageActivationContext, builder); 223 224 // Omit that first slash 225 result.addParameter(InternalConstants.PAGE_CONTEXT_NAME, builder.substring(1)); 226 } 227 228 // TAPESTRY-2044: Sometimes the active page drags in components from another page and we 229 // need to differentiate that. 230 231 if (!containingPageName.equalsIgnoreCase(activePageName)) 232 result.addParameter(InternalConstants.CONTAINER_PAGE_NAME, encodePageName(containingPageName)); 233 234 return result; 235 } 236 237 public ComponentEventRequestParameters decodeComponentEventRequest(Request request) 238 { 239 boolean explicitLocale = false; 240 241 Matcher matcher = COMPONENT_EVENT_REQUEST_PATH_PATTERN.matcher(request.getPath()); 242 243 if (!matcher.matches()) 244 return null; 245 246 String nestedComponentId = matcher.group(NESTED_ID); 247 248 String eventType = matcher.group(EVENT_NAME); 249 250 if (nestedComponentId == null && eventType == null) 251 return null; 252 253 String activePageName = matcher.group(LOGICAL_PAGE_NAME); 254 255 int slashx = activePageName.indexOf('/'); 256 257 String possibleLocaleName = slashx > 0 ? activePageName.substring(0, slashx) : ""; 258 259 if (localizationSetter.setLocaleFromLocaleName(possibleLocaleName)) 260 { 261 activePageName = activePageName.substring(slashx + 1); 262 explicitLocale = true; 263 } 264 265 if (!componentClassResolver.isPageName(activePageName)) 266 return null; 267 268 activePageName = componentClassResolver.canonicalizePageName(activePageName); 269 270 if (isWhitelistOnlyAndNotValid(activePageName)) 271 { 272 return null; 273 } 274 275 EventContext eventContext = contextPathEncoder.decodePath(matcher.group(CONTEXT)); 276 277 EventContext activationContext = contextPathEncoder.decodePath(request 278 .getParameter(InternalConstants.PAGE_CONTEXT_NAME)); 279 280 // The event type is often omitted, and defaults to "action". 281 282 if (eventType == null) 283 eventType = EventConstants.ACTION; 284 285 if (nestedComponentId == null) 286 nestedComponentId = ""; 287 288 String containingPageName = request.getParameter(InternalConstants.CONTAINER_PAGE_NAME); 289 290 if (containingPageName == null) 291 containingPageName = activePageName; 292 else 293 containingPageName = componentClassResolver.canonicalizePageName(containingPageName); 294 295 if (!explicitLocale) 296 { 297 setLocaleFromRequest(request); 298 } 299 300 return new ComponentEventRequestParameters(activePageName, containingPageName, nestedComponentId, eventType, 301 activationContext, eventContext); 302 } 303 304 private void setLocaleFromRequest(Request request) 305 { 306 Locale locale = request.getLocale(); 307 308 // And explicit locale will have invoked setLocaleFromLocaleName(). 309 310 localizationSetter.setNonPeristentLocaleFromLocaleName(locale.toString()); 311 } 312 313 public PageRenderRequestParameters decodePageRenderRequest(Request request) 314 { 315 boolean explicitLocale = false; 316 317 // The extended name may include a page activation context. The trick is 318 // to figure out where the logical page name stops and where the 319 // activation context begins. Further, strip out the leading slash. 320 321 String path = request.getPath(); 322 323 if (applicationFolderPrefix != null) 324 { 325 int prefixLength = applicationFolderPrefix.length(); 326 327 assert path.substring(0, prefixLength).equalsIgnoreCase(applicationFolderPrefix); 328 329 // This checks that the character after the prefix is a slash ... the extra complexity 330 // only seems to occur in Selenium. There's some ambiguity about what to do with a request for 331 // the application folder that doesn't end with a slash. Manuyal with Chrome and IE 8 shows that such 332 // requests are passed through with a training slash, automated testing with Selenium and FireFox 333 // can include requests for the folder without the trailing slash. 334 335 assert path.length() <= prefixLength || path.charAt(prefixLength) == '/'; 336 337 // Strip off the folder prefix (i.e., "/foldername"), leaving the rest of the path (i.e., "/en/pagename"). 338 339 path = path.substring(prefixLength); 340 } 341 342 343 // TAPESTRY-1343: Sometimes path is the empty string (it should always be at least a slash, 344 // but Tomcat may return the empty string for a root context request). 345 346 String extendedName = path.length() == 0 ? path : path.substring(1); 347 348 // Ignore trailing slashes in the path. 349 while (extendedName.endsWith("/")) 350 { 351 extendedName = extendedName.substring(0, extendedName.length() - 1); 352 } 353 354 int slashx = extendedName.indexOf('/'); 355 356 // So, what can we have left? 357 // 1. A page name 358 // 2. A locale followed by a page name 359 // 3. A page name followed by activation context 360 // 4. A locale name, page name, activation context 361 // 5. Just activation context (for root Index page) 362 // 6. A locale name followed by activation context 363 364 String possibleLocaleName = slashx > 0 ? extendedName.substring(0, slashx) : extendedName; 365 366 if (localizationSetter.setLocaleFromLocaleName(possibleLocaleName)) 367 { 368 extendedName = slashx > 0 ? extendedName.substring(slashx + 1) : ""; 369 explicitLocale = true; 370 } 371 372 slashx = extendedName.length(); 373 boolean atEnd = true; 374 375 while (slashx > 0) 376 { 377 String pageName = extendedName.substring(0, slashx); 378 String pageActivationContext = atEnd ? "" : extendedName.substring(slashx + 1); 379 380 PageRenderRequestParameters parameters = checkIfPage(request, pageName, pageActivationContext); 381 382 if (parameters != null) 383 { 384 return parameters; 385 } 386 387 // Work backwards, splitting at the next slash. 388 slashx = extendedName.lastIndexOf('/', slashx - 1); 389 390 atEnd = false; 391 } 392 393 // OK, maybe its all page activation context for the root Index page. 394 395 PageRenderRequestParameters result = checkIfPage(request, "", extendedName); 396 397 if (result != null && !explicitLocale) 398 { 399 setLocaleFromRequest(request); 400 } 401 402 return result; 403 } 404 405 private PageRenderRequestParameters checkIfPage(Request request, String pageName, String pageActivationContext) 406 { 407 if (!componentClassResolver.isPageName(pageName)) 408 { 409 return null; 410 } 411 412 String canonicalized = componentClassResolver.canonicalizePageName(pageName); 413 414 // If the page is only visible to the whitelist, but the request is not on the whitelist, then 415 // pretend the page doesn't exist! 416 if (isWhitelistOnlyAndNotValid(canonicalized)) 417 { 418 return null; 419 } 420 421 EventContext activationContext = contextPathEncoder.decodePath(pageActivationContext); 422 423 boolean loopback = request.getParameter(TapestryConstants.PAGE_LOOPBACK_PARAMETER_NAME) != null; 424 425 return new PageRenderRequestParameters(canonicalized, activationContext, loopback); 426 } 427 428 private boolean isWhitelistOnlyAndNotValid(String canonicalized) 429 { 430 return metaDataLocator.findMeta(MetaDataConstants.WHITELIST_ONLY_PAGE, canonicalized, boolean.class) && 431 !clientWhitelist.isClientRequestOnWhitelist(); 432 } 433 434 public void appendContext(boolean seperatorRequired, EventContext context, StringBuilder builder) 435 { 436 String encoded = contextPathEncoder.encodeIntoPath(context); 437 438 if (encoded.length() > 0) 439 { 440 if (seperatorRequired) 441 { 442 builder.append(SLASH); 443 } 444 445 builder.append(encoded); 446 } 447 } 448 }