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 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}