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 }