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 public ComponentEventRequestParameters decodeComponentEventRequest(Request request) 256 { 257 String explicitLocale = null; 258 259 // Split the path around slashes into a mutable list of terms, which will be consumed term by term. 260 261 String requestPath = request.getPath(); 262 263 if (applicationFolderPrefix != null) 264 { 265 requestPath = removeApplicationPrefix(requestPath); 266 } 267 268 List<String> path = splitPath(requestPath); 269 270 271 272 if (path.isEmpty()) 273 { 274 return null; 275 } 276 277 // Next up: the locale (which is optional) 278 279 String potentialLocale = path.get(0); 280 281 if (localizationSetter.isSupportedLocaleName(potentialLocale)) 282 { 283 explicitLocale = potentialLocale; 284 path.remove(0); 285 } 286 287 StringBuilder pageName = new StringBuilder(100); 288 String sep = ""; 289 290 while (!path.isEmpty()) 291 { 292 String name = path.remove(0); 293 String eventType = EventConstants.ACTION; 294 String nestedComponentId = ""; 295 296 boolean found = false; 297 298 // First, look for an explicit action name. 299 300 int colonx = name.lastIndexOf(':'); 301 302 if (colonx > 0) 303 { 304 found = true; 305 eventType = name.substring(colonx + 1); 306 name = name.substring(0, colonx); 307 } 308 309 int dotx = name.indexOf('.'); 310 311 if (dotx > 0) 312 { 313 found = true; 314 nestedComponentId = name.substring(dotx + 1); 315 name = name.substring(0, dotx); 316 } 317 318 pageName.append(sep).append(name); 319 320 if (found) 321 { 322 ComponentEventRequestParameters result = validateAndConstructComponentEventRequest(request, pageName.toString(), nestedComponentId, eventType, path); 323 324 if (result == null) 325 { 326 return result; 327 } 328 329 if (explicitLocale == null) 330 { 331 setLocaleFromRequest(request); 332 } else 333 { 334 localizationSetter.setLocaleFromLocaleName(explicitLocale); 335 } 336 337 return result; 338 } 339 340 // Continue on to the next name in the path 341 sep = "/"; 342 } 343 344 // Path empty before finding something that looks like a component id or event name, so 345 // it is not a component event request. 346 347 return null; 348 } 349 350 private ComponentEventRequestParameters validateAndConstructComponentEventRequest(Request request, String pageName, String nestedComponentId, String eventType, List<String> remainingPath) 351 { 352 if (!componentClassResolver.isPageName(pageName)) 353 { 354 return null; 355 } 356 357 String activePageName = componentClassResolver.canonicalizePageName(pageName); 358 359 if (isWhitelistOnlyAndNotValid(activePageName)) 360 { 361 return null; 362 } 363 364 String value = request.getParameter(InternalConstants.CONTAINER_PAGE_NAME); 365 366 String containingPageName = value == null 367 ? activePageName 368 : componentClassResolver.canonicalizePageName(value); 369 370 EventContext eventContext = contextPathEncoder.decodePath(joinPath(remainingPath)); 371 EventContext activationContext = contextPathEncoder.decodePath(request.getParameter(InternalConstants.PAGE_CONTEXT_NAME)); 372 373 return new ComponentEventRequestParameters(activePageName, containingPageName, nestedComponentId, eventType, 374 activationContext, eventContext); 375 } 376 377 private void setLocaleFromRequest(Request request) 378 { 379 Locale locale = request.getLocale(); 380 381 // And explicit locale will have invoked setLocaleFromLocaleName(). 382 383 localizationSetter.setNonPersistentLocaleFromLocaleName(locale.toString()); 384 } 385 386 public PageRenderRequestParameters decodePageRenderRequest(Request request) 387 { 388 boolean explicitLocale = false; 389 390 // The extended name may include a page activation context. The trick is 391 // to figure out where the logical page name stops and where the 392 // activation context begins. Further, strip out the leading slash. 393 394 String path = request.getPath(); 395 396 if (applicationFolderPrefix != null) 397 { 398 path = removeApplicationPrefix(path); 399 } 400 401 402 // TAPESTRY-1343: Sometimes path is the empty string (it should always be at least a slash, 403 // but Tomcat may return the empty string for a root context request). 404 405 String extendedName = path.length() == 0 ? path : path.substring(1); 406 407 // Ignore trailing slashes in the path. 408 while (extendedName.endsWith("/")) 409 { 410 extendedName = extendedName.substring(0, extendedName.length() - 1); 411 } 412 413 int slashx = extendedName.indexOf('/'); 414 415 // So, what can we have left? 416 // 1. A page name 417 // 2. A locale followed by a page name 418 // 3. A page name followed by activation context 419 // 4. A locale name, page name, activation context 420 // 5. Just activation context (for root Index page) 421 // 6. A locale name followed by activation context 422 423 String possibleLocaleName = slashx > 0 ? extendedName.substring(0, slashx) : extendedName; 424 425 if (localizationSetter.setLocaleFromLocaleName(possibleLocaleName)) 426 { 427 extendedName = slashx > 0 ? extendedName.substring(slashx + 1) : ""; 428 explicitLocale = true; 429 } 430 431 slashx = extendedName.length(); 432 boolean atEnd = true; 433 434 while (slashx > 0) 435 { 436 String pageName = extendedName.substring(0, slashx); 437 String pageActivationContext = atEnd ? "" : extendedName.substring(slashx + 1); 438 439 PageRenderRequestParameters parameters = checkIfPage(request, pageName, pageActivationContext); 440 441 if (parameters != null) 442 { 443 return parameters; 444 } 445 446 // Work backwards, splitting at the next slash. 447 slashx = extendedName.lastIndexOf('/', slashx - 1); 448 449 atEnd = false; 450 } 451 452 // OK, maybe its all page activation context for the root Index page. 453 454 PageRenderRequestParameters result = checkIfPage(request, "", extendedName); 455 456 if (result != null && !explicitLocale) 457 { 458 setLocaleFromRequest(request); 459 } 460 461 return result; 462 } 463 464 private String removeApplicationPrefix(String path) { 465 int prefixLength = applicationFolderPrefix.length(); 466 467 assert path.substring(0, prefixLength).equalsIgnoreCase(applicationFolderPrefix); 468 469 // This checks that the character after the prefix is a slash ... the extra complexity 470 // only seems to occur in Selenium. There's some ambiguity about what to do with a request for 471 // the application folder that doesn't end with a slash. Manuyal with Chrome and IE 8 shows that such 472 // requests are passed through with a training slash, automated testing with Selenium and FireFox 473 // can include requests for the folder without the trailing slash. 474 475 assert path.length() <= prefixLength || path.charAt(prefixLength) == '/'; 476 477 // Strip off the folder prefix (i.e., "/foldername"), leaving the rest of the path (i.e., "/en/pagename"). 478 479 path = path.substring(prefixLength); 480 return path; 481 } 482 483 private PageRenderRequestParameters checkIfPage(Request request, String pageName, String pageActivationContext) 484 { 485 if (!componentClassResolver.isPageName(pageName)) 486 { 487 return null; 488 } 489 String canonicalized = componentClassResolver.canonicalizePageName(pageName); 490 491 // If the page is only visible to the whitelist, but the request is not on the whitelist, then 492 // pretend the page doesn't exist! 493 if (isWhitelistOnlyAndNotValid(canonicalized)) 494 { 495 return null; 496 } 497 try 498 { 499 EventContext activationContext = contextPathEncoder.decodePath(pageActivationContext); 500 501 boolean loopback = request.getParameter(TapestryConstants.PAGE_LOOPBACK_PARAMETER_NAME) != null; 502 503 return new PageRenderRequestParameters(canonicalized, activationContext, loopback); 504 } catch (IllegalArgumentException e) 505 { 506 // TAP5-2436 507 return null; 508 } 509 } 510 511 private boolean isWhitelistOnlyAndNotValid(String canonicalized) 512 { 513 return metaDataLocator.findMeta(MetaDataConstants.WHITELIST_ONLY_PAGE, canonicalized, boolean.class) && 514 !clientWhitelist.isClientRequestOnWhitelist(); 515 } 516 517 public void appendContext(boolean seperatorRequired, EventContext context, StringBuilder builder) 518 { 519 String encoded = contextPathEncoder.encodeIntoPath(context); 520 521 if (encoded.length() > 0) 522 { 523 if (seperatorRequired) 524 { 525 builder.append(SLASH); 526 } 527 528 builder.append(encoded); 529 } 530 } 531}