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