001// Copyright 2009, 2010, 2011, 2012 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 015package org.apache.tapestry5.internal.services; 016 017import org.apache.tapestry5.*; 018import org.apache.tapestry5.beanmodel.services.*; 019import org.apache.tapestry5.commons.util.CollectionFactory; 020import org.apache.tapestry5.http.Link; 021import org.apache.tapestry5.http.TapestryHttpSymbolConstants; 022import org.apache.tapestry5.http.services.BaseURLSource; 023import org.apache.tapestry5.http.services.Request; 024import org.apache.tapestry5.http.services.Response; 025import org.apache.tapestry5.internal.InternalConstants; 026import org.apache.tapestry5.internal.TapestryInternalUtils; 027import org.apache.tapestry5.ioc.annotations.Symbol; 028import org.apache.tapestry5.ioc.internal.util.InternalUtils; 029import org.apache.tapestry5.services.ComponentClassResolver; 030import org.apache.tapestry5.services.ComponentEventLinkEncoder; 031import org.apache.tapestry5.services.ComponentEventRequestParameters; 032import org.apache.tapestry5.services.ContextPathEncoder; 033import org.apache.tapestry5.services.LocalizationSetter; 034import org.apache.tapestry5.services.MetaDataLocator; 035import org.apache.tapestry5.services.PageRenderRequestParameters; 036import org.apache.tapestry5.services.PersistentLocale; 037import org.apache.tapestry5.services.security.ClientWhitelist; 038 039import java.util.List; 040import java.util.Locale; 041 042public class ComponentEventLinkEncoderImpl implements ComponentEventLinkEncoder 043{ 044 private final ComponentClassResolver componentClassResolver; 045 046 private final ContextPathEncoder contextPathEncoder; 047 048 private final LocalizationSetter localizationSetter; 049 050 private final Response response; 051 052 private final RequestSecurityManager requestSecurityManager; 053 054 private final BaseURLSource baseURLSource; 055 056 private final PersistentLocale persistentLocale; 057 058 private final boolean encodeLocaleIntoPath; 059 060 private final MetaDataLocator metaDataLocator; 061 062 private final ClientWhitelist clientWhitelist; 063 064 private final String contextPath; 065 066 private final String applicationFolder; 067 068 private final String applicationFolderPrefix; 069 070 private static final int BUFFER_SIZE = 100; 071 072 private static final char SLASH = '/'; 073 074 public ComponentEventLinkEncoderImpl(ComponentClassResolver componentClassResolver, 075 ContextPathEncoder contextPathEncoder, LocalizationSetter localizationSetter, 076 Response response, RequestSecurityManager requestSecurityManager, BaseURLSource baseURLSource, 077 PersistentLocale persistentLocale, 078 @Symbol(SymbolConstants.ENCODE_LOCALE_INTO_PATH) 079 boolean encodeLocaleIntoPath, 080 @Symbol(TapestryHttpSymbolConstants.CONTEXT_PATH) 081 String contextPath, 082 @Symbol(SymbolConstants.APPLICATION_FOLDER) String applicationFolder, 083 MetaDataLocator metaDataLocator, 084 ClientWhitelist clientWhitelist) 085 { 086 this.componentClassResolver = componentClassResolver; 087 this.contextPathEncoder = contextPathEncoder; 088 this.localizationSetter = localizationSetter; 089 this.response = response; 090 this.requestSecurityManager = requestSecurityManager; 091 this.baseURLSource = baseURLSource; 092 this.persistentLocale = persistentLocale; 093 this.encodeLocaleIntoPath = encodeLocaleIntoPath; 094 this.contextPath = contextPath; 095 this.applicationFolder = applicationFolder; 096 this.metaDataLocator = metaDataLocator; 097 this.clientWhitelist = clientWhitelist; 098 099 boolean hasAppFolder = applicationFolder.equals(""); 100 101 applicationFolderPrefix = hasAppFolder ? null : SLASH + applicationFolder; 102 } 103 104 public Link createPageRenderLink(PageRenderRequestParameters parameters) 105 { 106 StringBuilder builder = new StringBuilder(BUFFER_SIZE); 107 108 // Build up the absolute URI. 109 110 String activePageName = parameters.getLogicalPageName(); 111 112 builder.append(contextPath); 113 114 encodeAppFolderAndLocale(builder); 115 116 builder.append(SLASH); 117 118 String encodedPageName = encodePageName(activePageName); 119 120 builder.append(encodedPageName); 121 122 appendContext(encodedPageName.length() > 0, parameters.getActivationContext(), builder); 123 124 Link link = new LinkImpl(builder.toString(), false, requestSecurityManager.checkPageSecurity(activePageName), 125 response, contextPathEncoder, baseURLSource); 126 127 if (parameters.isLoopback()) 128 { 129 link.addParameter(TapestryConstants.PAGE_LOOPBACK_PARAMETER_NAME, "t"); 130 } 131 132 return link; 133 } 134 135 private void encodeAppFolderAndLocale(StringBuilder builder) 136 { 137 if (!applicationFolder.equals("")) 138 { 139 builder.append(SLASH).append(applicationFolder); 140 } 141 142 if (encodeLocaleIntoPath) 143 { 144 Locale locale = persistentLocale.get(); 145 146 if (locale != null) 147 { 148 builder.append(SLASH); 149 builder.append(locale.toString()); 150 } 151 } 152 } 153 154 private String encodePageName(String pageName) 155 { 156 if (pageName.equalsIgnoreCase("index")) 157 return ""; 158 159 String encoded = pageName.toLowerCase(); 160 161 if (!encoded.endsWith("/index")) 162 return encoded; 163 164 return encoded.substring(0, encoded.length() - 6); 165 } 166 167 public Link createComponentEventLink(ComponentEventRequestParameters parameters, boolean forForm) 168 { 169 StringBuilder builder = new StringBuilder(BUFFER_SIZE); 170 171 // Build up the absolute URI. 172 173 String activePageName = parameters.getActivePageName(); 174 String containingPageName = parameters.getContainingPageName(); 175 String eventType = parameters.getEventType(); 176 177 String nestedComponentId = parameters.getNestedComponentId(); 178 boolean hasComponentId = InternalUtils.isNonBlank(nestedComponentId); 179 180 builder.append(contextPath); 181 182 encodeAppFolderAndLocale(builder); 183 184 builder.append(SLASH); 185 builder.append(activePageName.toLowerCase()); 186 187 if (hasComponentId) 188 { 189 builder.append('.'); 190 builder.append(nestedComponentId); 191 } 192 193 if (!hasComponentId || !eventType.equals(EventConstants.ACTION)) 194 { 195 builder.append(':'); 196 builder.append(encodePageName(eventType)); 197 } 198 199 appendContext(true, parameters.getEventContext(), builder); 200 201 Link result = new LinkImpl(builder.toString(), forForm, 202 requestSecurityManager.checkPageSecurity(activePageName), response, contextPathEncoder, baseURLSource); 203 204 EventContext pageActivationContext = parameters.getPageActivationContext(); 205 206 if (pageActivationContext.getCount() != 0) 207 { 208 // Reuse the builder 209 builder.setLength(0); 210 appendContext(true, pageActivationContext, builder); 211 212 // Omit that first slash 213 result.addParameter(InternalConstants.PAGE_CONTEXT_NAME, builder.substring(1)); 214 } 215 216 // TAPESTRY-2044: Sometimes the active page drags in components from another page and we 217 // need to differentiate that. 218 219 if (!containingPageName.equalsIgnoreCase(activePageName)) 220 result.addParameter(InternalConstants.CONTAINER_PAGE_NAME, encodePageName(containingPageName)); 221 222 return result; 223 } 224 225 /** 226 * Splits path at slashes into a <em>mutable</em> list of strings. Empty terms, including the 227 * expected leading term (paths start with a '/') are dropped. 228 * 229 * @param path 230 * @return mutable list of path elements 231 */ 232 private List<String> splitPath(String path) 233 { 234 String[] split = TapestryInternalUtils.splitPath(path); 235 236 List<String> result = CollectionFactory.newList(); 237 238 for (String name : split) 239 { 240 if (name.length() > 0) 241 { 242 result.add(name); 243 } 244 } 245 246 return result; 247 } 248 249 private String joinPath(List<String> path) 250 { 251 if (path.isEmpty()) 252 { 253 return ""; 254 } 255 256 StringBuilder builder = new StringBuilder(100); 257 String sep = ""; 258 259 for (String term : path) 260 { 261 builder.append(sep).append(term); 262 sep = "/"; 263 } 264 265 return builder.toString(); 266 } 267 268 public ComponentEventRequestParameters decodeComponentEventRequest(Request request) 269 { 270 String explicitLocale = null; 271 272 // Split the path around slashes into a mutable list of terms, which will be consumed term by term. 273 274 String requestPath = request.getPath(); 275 276 if (applicationFolderPrefix != null) 277 { 278 requestPath = removeApplicationPrefix(requestPath); 279 } 280 281 List<String> path = splitPath(requestPath); 282 283 284 285 if (path.isEmpty()) 286 { 287 return null; 288 } 289 290 // Next up: the locale (which is optional) 291 292 String potentialLocale = path.get(0); 293 294 if (localizationSetter.isSupportedLocaleName(potentialLocale)) 295 { 296 explicitLocale = potentialLocale; 297 path.remove(0); 298 } 299 300 StringBuilder pageName = new StringBuilder(100); 301 String sep = ""; 302 303 while (!path.isEmpty()) 304 { 305 String name = path.remove(0); 306 String eventType = EventConstants.ACTION; 307 String nestedComponentId = ""; 308 309 boolean found = false; 310 311 // First, look for an explicit action name. 312 313 int colonx = name.lastIndexOf(':'); 314 315 if (colonx > 0) 316 { 317 found = true; 318 eventType = name.substring(colonx + 1); 319 name = name.substring(0, colonx); 320 } 321 322 int dotx = name.indexOf('.'); 323 324 if (dotx > 0) 325 { 326 found = true; 327 nestedComponentId = name.substring(dotx + 1); 328 name = name.substring(0, dotx); 329 } 330 331 pageName.append(sep).append(name); 332 333 if (found) 334 { 335 ComponentEventRequestParameters result = validateAndConstructComponentEventRequest(request, pageName.toString(), nestedComponentId, eventType, path); 336 337 if (result == null) 338 { 339 return result; 340 } 341 342 if (explicitLocale == null) 343 { 344 localizationSetter.setNonPersistentLocaleFromRequest(request); 345 } else 346 { 347 localizationSetter.setLocaleFromLocaleName(explicitLocale); 348 } 349 350 return result; 351 } 352 353 // Continue on to the next name in the path 354 sep = "/"; 355 } 356 357 // Path empty before finding something that looks like a component id or event name, so 358 // it is not a component event request. 359 360 return null; 361 } 362 363 private ComponentEventRequestParameters validateAndConstructComponentEventRequest(Request request, String pageName, String nestedComponentId, String eventType, List<String> remainingPath) 364 { 365 if (!componentClassResolver.isPageName(pageName)) 366 { 367 return null; 368 } 369 370 String activePageName = componentClassResolver.canonicalizePageName(pageName); 371 372 if (isWhitelistOnlyAndNotValid(activePageName)) 373 { 374 return null; 375 } 376 377 String value = request.getParameter(InternalConstants.CONTAINER_PAGE_NAME); 378 379 String containingPageName = value == null 380 ? activePageName 381 : componentClassResolver.canonicalizePageName(value); 382 383 EventContext eventContext = contextPathEncoder.decodePath(joinPath(remainingPath)); 384 EventContext activationContext = contextPathEncoder.decodePath(request.getParameter(InternalConstants.PAGE_CONTEXT_NAME)); 385 386 return new ComponentEventRequestParameters(activePageName, containingPageName, nestedComponentId, eventType, 387 activationContext, eventContext); 388 } 389 390 public PageRenderRequestParameters decodePageRenderRequest(Request request) 391 { 392 boolean explicitLocale = false; 393 394 // The extended name may include a page activation context. The trick is 395 // to figure out where the logical page name stops and where the 396 // activation context begins. Further, strip out the leading slash. 397 398 String path = request.getPath(); 399 400 if (applicationFolderPrefix != null) 401 { 402 path = removeApplicationPrefix(path); 403 } 404 405 406 // TAPESTRY-1343: Sometimes path is the empty string (it should always be at least a slash, 407 // but Tomcat may return the empty string for a root context request). 408 409 String extendedName = path.length() == 0 ? path : path.substring(1); 410 411 // Ignore trailing slashes in the path. 412 while (extendedName.endsWith("/")) 413 { 414 extendedName = extendedName.substring(0, extendedName.length() - 1); 415 } 416 417 int slashx = extendedName.indexOf('/'); 418 419 // So, what can we have left? 420 // 1. A page name 421 // 2. A locale followed by a page name 422 // 3. A page name followed by activation context 423 // 4. A locale name, page name, activation context 424 // 5. Just activation context (for root Index page) 425 // 6. A locale name followed by activation context 426 427 String possibleLocaleName = slashx > 0 ? extendedName.substring(0, slashx) : extendedName; 428 429 if (localizationSetter.setLocaleFromLocaleName(possibleLocaleName)) 430 { 431 extendedName = slashx > 0 ? extendedName.substring(slashx + 1) : ""; 432 explicitLocale = true; 433 } 434 435 slashx = extendedName.length(); 436 boolean atEnd = true; 437 438 while (slashx > 0) 439 { 440 String pageName = extendedName.substring(0, slashx); 441 String pageActivationContext = atEnd ? "" : extendedName.substring(slashx + 1); 442 443 PageRenderRequestParameters parameters = checkIfPage(request, pageName, pageActivationContext); 444 445 if (parameters != null) 446 { 447 return parameters; 448 } 449 450 // Work backwards, splitting at the next slash. 451 slashx = extendedName.lastIndexOf('/', slashx - 1); 452 453 atEnd = false; 454 } 455 456 // OK, maybe its all page activation context for the root Index page. 457 458 PageRenderRequestParameters result = checkIfPage(request, "", extendedName); 459 460 if (result != null && !explicitLocale) 461 { 462 localizationSetter.setNonPersistentLocaleFromRequest(request); 463 } 464 465 return result; 466 } 467 468 private String removeApplicationPrefix(String path) { 469 int prefixLength = applicationFolderPrefix.length(); 470 471 assert path.substring(0, prefixLength).equalsIgnoreCase(applicationFolderPrefix); 472 473 // This checks that the character after the prefix is a slash ... the extra complexity 474 // only seems to occur in Selenium. There's some ambiguity about what to do with a request for 475 // the application folder that doesn't end with a slash. Manuyal with Chrome and IE 8 shows that such 476 // requests are passed through with a training slash, automated testing with Selenium and FireFox 477 // can include requests for the folder without the trailing slash. 478 479 assert path.length() <= prefixLength || path.charAt(prefixLength) == '/'; 480 481 // Strip off the folder prefix (i.e., "/foldername"), leaving the rest of the path (i.e., "/en/pagename"). 482 483 path = path.substring(prefixLength); 484 return path; 485 } 486 487 private PageRenderRequestParameters checkIfPage(Request request, String pageName, String pageActivationContext) 488 { 489 if (!componentClassResolver.isPageName(pageName)) 490 { 491 return null; 492 } 493 String canonicalized = componentClassResolver.canonicalizePageName(pageName); 494 495 // If the page is only visible to the whitelist, but the request is not on the whitelist, then 496 // pretend the page doesn't exist! 497 if (isWhitelistOnlyAndNotValid(canonicalized)) 498 { 499 return null; 500 } 501 try 502 { 503 EventContext activationContext = contextPathEncoder.decodePath(pageActivationContext); 504 505 boolean loopback = request.getParameter(TapestryConstants.PAGE_LOOPBACK_PARAMETER_NAME) != null; 506 507 return new PageRenderRequestParameters(canonicalized, activationContext, loopback); 508 } catch (IllegalArgumentException e) 509 { 510 // TAP5-2436 511 return null; 512 } 513 } 514 515 private boolean isWhitelistOnlyAndNotValid(String canonicalized) 516 { 517 return metaDataLocator.findMeta(MetaDataConstants.WHITELIST_ONLY_PAGE, canonicalized, boolean.class) && 518 !clientWhitelist.isClientRequestOnWhitelist(); 519 } 520 521 public void appendContext(boolean seperatorRequired, EventContext context, StringBuilder builder) 522 { 523 String encoded = contextPathEncoder.encodeIntoPath(context); 524 525 if (encoded.length() > 0) 526 { 527 if (seperatorRequired) 528 { 529 builder.append(SLASH); 530 } 531 532 builder.append(encoded); 533 } 534 } 535}