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}