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