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.beanmodel.services.*;
019import org.apache.tapestry5.commons.util.CollectionFactory;
020import org.apache.tapestry5.http.Link;
021import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
022import org.apache.tapestry5.http.services.BaseURLSource;
023import org.apache.tapestry5.http.services.Request;
024import org.apache.tapestry5.http.services.Response;
025import org.apache.tapestry5.internal.InternalConstants;
026import org.apache.tapestry5.internal.TapestryInternalUtils;
027import org.apache.tapestry5.ioc.annotations.Symbol;
028import org.apache.tapestry5.ioc.internal.util.InternalUtils;
029import org.apache.tapestry5.services.ComponentClassResolver;
030import org.apache.tapestry5.services.ComponentEventLinkEncoder;
031import org.apache.tapestry5.services.ComponentEventRequestParameters;
032import org.apache.tapestry5.services.ContextPathEncoder;
033import org.apache.tapestry5.services.LocalizationSetter;
034import org.apache.tapestry5.services.MetaDataLocator;
035import org.apache.tapestry5.services.PageRenderRequestParameters;
036import org.apache.tapestry5.services.PersistentLocale;
037import org.apache.tapestry5.services.security.ClientWhitelist;
038
039import java.util.List;
040import java.util.Locale;
041
042public class ComponentEventLinkEncoderImpl implements ComponentEventLinkEncoder
043{
044    private final ComponentClassResolver componentClassResolver;
045
046    private final ContextPathEncoder contextPathEncoder;
047
048    private final LocalizationSetter localizationSetter;
049
050    private final Response response;
051
052    private final RequestSecurityManager requestSecurityManager;
053
054    private final BaseURLSource baseURLSource;
055
056    private final PersistentLocale persistentLocale;
057
058    private final boolean encodeLocaleIntoPath;
059
060    private final MetaDataLocator metaDataLocator;
061
062    private final ClientWhitelist clientWhitelist;
063
064    private final String contextPath;
065
066    private final String applicationFolder;
067
068    private final String applicationFolderPrefix;
069
070    private static final int BUFFER_SIZE = 100;
071
072    private static final char SLASH = '/';
073
074    public ComponentEventLinkEncoderImpl(ComponentClassResolver componentClassResolver,
075                                         ContextPathEncoder contextPathEncoder, LocalizationSetter localizationSetter,
076                                         Response response, RequestSecurityManager requestSecurityManager, BaseURLSource baseURLSource,
077                                         PersistentLocale persistentLocale,
078                                         @Symbol(SymbolConstants.ENCODE_LOCALE_INTO_PATH)
079                                         boolean encodeLocaleIntoPath,
080                                         @Symbol(TapestryHttpSymbolConstants.CONTEXT_PATH)
081                                         String contextPath,
082                                         @Symbol(SymbolConstants.APPLICATION_FOLDER) String applicationFolder,
083                                         MetaDataLocator metaDataLocator,
084                                         ClientWhitelist clientWhitelist)
085    {
086        this.componentClassResolver = componentClassResolver;
087        this.contextPathEncoder = contextPathEncoder;
088        this.localizationSetter = localizationSetter;
089        this.response = response;
090        this.requestSecurityManager = requestSecurityManager;
091        this.baseURLSource = baseURLSource;
092        this.persistentLocale = persistentLocale;
093        this.encodeLocaleIntoPath = encodeLocaleIntoPath;
094        this.contextPath = contextPath;
095        this.applicationFolder = applicationFolder;
096        this.metaDataLocator = metaDataLocator;
097        this.clientWhitelist = clientWhitelist;
098
099        boolean hasAppFolder = applicationFolder.equals("");
100
101        applicationFolderPrefix = hasAppFolder ? null : SLASH + applicationFolder;
102    }
103
104    public Link createPageRenderLink(PageRenderRequestParameters parameters)
105    {
106        StringBuilder builder = new StringBuilder(BUFFER_SIZE);
107
108        // Build up the absolute URI.
109
110        String activePageName = parameters.getLogicalPageName();
111
112        builder.append(contextPath);
113
114        encodeAppFolderAndLocale(builder);
115
116        builder.append(SLASH);
117
118        String encodedPageName = encodePageName(activePageName);
119
120        builder.append(encodedPageName);
121
122        appendContext(encodedPageName.length() > 0, parameters.getActivationContext(), builder);
123
124        Link link = new LinkImpl(builder.toString(), false, requestSecurityManager.checkPageSecurity(activePageName),
125                response, contextPathEncoder, baseURLSource);
126
127        if (parameters.isLoopback())
128        {
129            link.addParameter(TapestryConstants.PAGE_LOOPBACK_PARAMETER_NAME, "t");
130        }
131
132        return link;
133    }
134
135    private void encodeAppFolderAndLocale(StringBuilder builder)
136    {
137        if (!applicationFolder.equals(""))
138        {
139            builder.append(SLASH).append(applicationFolder);
140        }
141
142        if (encodeLocaleIntoPath)
143        {
144            Locale locale = persistentLocale.get();
145
146            if (locale != null)
147            {
148                builder.append(SLASH);
149                builder.append(locale.toString());
150            }
151        }
152    }
153
154    private String encodePageName(String pageName)
155    {
156        if (pageName.equalsIgnoreCase("index"))
157            return "";
158
159        String encoded = pageName.toLowerCase();
160
161        if (!encoded.endsWith("/index"))
162            return encoded;
163
164        return encoded.substring(0, encoded.length() - 6);
165    }
166
167    public Link createComponentEventLink(ComponentEventRequestParameters parameters, boolean forForm)
168    {
169        StringBuilder builder = new StringBuilder(BUFFER_SIZE);
170
171        // Build up the absolute URI.
172
173        String activePageName = parameters.getActivePageName();
174        String containingPageName = parameters.getContainingPageName();
175        String eventType = parameters.getEventType();
176
177        String nestedComponentId = parameters.getNestedComponentId();
178        boolean hasComponentId = InternalUtils.isNonBlank(nestedComponentId);
179
180        builder.append(contextPath);
181
182        encodeAppFolderAndLocale(builder);
183
184        builder.append(SLASH);
185        builder.append(activePageName.toLowerCase());
186
187        if (hasComponentId)
188        {
189            builder.append('.');
190            builder.append(nestedComponentId);
191        }
192
193        if (!hasComponentId || !eventType.equals(EventConstants.ACTION))
194        {
195            builder.append(':');
196            builder.append(encodePageName(eventType));
197        }
198
199        appendContext(true, parameters.getEventContext(), builder);
200
201        Link result = new LinkImpl(builder.toString(), forForm,
202                requestSecurityManager.checkPageSecurity(activePageName), response, contextPathEncoder, baseURLSource);
203
204        EventContext pageActivationContext = parameters.getPageActivationContext();
205
206        if (pageActivationContext.getCount() != 0)
207        {
208            // Reuse the builder
209            builder.setLength(0);
210            appendContext(true, pageActivationContext, builder);
211
212            // Omit that first slash
213            result.addParameter(InternalConstants.PAGE_CONTEXT_NAME, builder.substring(1));
214        }
215
216        // TAPESTRY-2044: Sometimes the active page drags in components from another page and we
217        // need to differentiate that.
218
219        if (!containingPageName.equalsIgnoreCase(activePageName))
220            result.addParameter(InternalConstants.CONTAINER_PAGE_NAME, encodePageName(containingPageName));
221
222        return result;
223    }
224
225    /**
226     * Splits path at slashes into a <em>mutable</em> list of strings. Empty terms, including the
227     * expected leading term (paths start with a '/') are dropped.
228     *
229     * @param path
230     * @return mutable list of path elements
231     */
232    private List<String> splitPath(String path)
233    {
234        String[] split = TapestryInternalUtils.splitPath(path);
235
236        List<String> result = CollectionFactory.newList();
237
238        for (String name : split)
239        {
240            if (name.length() > 0)
241            {
242                result.add(name);
243            }
244        }
245
246        return result;
247    }
248
249    private String joinPath(List<String> path)
250    {
251        if (path.isEmpty())
252        {
253            return "";
254        }
255
256        StringBuilder builder = new StringBuilder(100);
257        String sep = "";
258
259        for (String term : path)
260        {
261            builder.append(sep).append(term);
262            sep = "/";
263        }
264
265        return builder.toString();
266    }
267
268    public ComponentEventRequestParameters decodeComponentEventRequest(Request request)
269    {
270        String explicitLocale = null;
271
272        // Split the path around slashes into a mutable list of terms, which will be consumed term by term.
273
274        String requestPath = request.getPath();
275
276        if (applicationFolderPrefix != null)
277        {
278            requestPath = removeApplicationPrefix(requestPath);
279        }
280
281        List<String> path = splitPath(requestPath);
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            path = removeApplicationPrefix(path);
412        }
413
414
415        // TAPESTRY-1343: Sometimes path is the empty string (it should always be at least a slash,
416        // but Tomcat may return the empty string for a root context request).
417
418        String extendedName = path.length() == 0 ? path : path.substring(1);
419
420        // Ignore trailing slashes in the path.
421        while (extendedName.endsWith("/"))
422        {
423            extendedName = extendedName.substring(0, extendedName.length() - 1);
424        }
425
426        int slashx = extendedName.indexOf('/');
427
428        // So, what can we have left?
429        // 1. A page name
430        // 2. A locale followed by a page name
431        // 3. A page name followed by activation context
432        // 4. A locale name, page name, activation context
433        // 5. Just activation context (for root Index page)
434        // 6. A locale name followed by activation context
435
436        String possibleLocaleName = slashx > 0 ? extendedName.substring(0, slashx) : extendedName;
437
438        if (localizationSetter.setLocaleFromLocaleName(possibleLocaleName))
439        {
440            extendedName = slashx > 0 ? extendedName.substring(slashx + 1) : "";
441            explicitLocale = true;
442        }
443
444        slashx = extendedName.length();
445        boolean atEnd = true;
446
447        while (slashx > 0)
448        {
449            String pageName = extendedName.substring(0, slashx);
450            String pageActivationContext = atEnd ? "" : extendedName.substring(slashx + 1);
451
452            PageRenderRequestParameters parameters = checkIfPage(request, pageName, pageActivationContext);
453
454            if (parameters != null)
455            {
456                return parameters;
457            }
458
459            // Work backwards, splitting at the next slash.
460            slashx = extendedName.lastIndexOf('/', slashx - 1);
461
462            atEnd = false;
463        }
464
465        // OK, maybe its all page activation context for the root Index page.
466
467        PageRenderRequestParameters result = checkIfPage(request, "", extendedName);
468
469        if (result != null && !explicitLocale)
470        {
471            setLocaleFromRequest(request);
472        }
473
474        return result;
475    }
476
477    private String removeApplicationPrefix(String path) {
478        int prefixLength = applicationFolderPrefix.length();
479
480        assert path.substring(0, prefixLength).equalsIgnoreCase(applicationFolderPrefix);
481
482        // This checks that the character after the prefix is a slash ... the extra complexity
483        // only seems to occur in Selenium. There's some ambiguity about what to do with a request for
484        // the application folder that doesn't end with a slash. Manuyal with Chrome and IE 8 shows that such
485        // requests are passed through with a training slash,  automated testing with Selenium and FireFox
486        // can include requests for the folder without the trailing slash.
487
488        assert path.length() <= prefixLength || path.charAt(prefixLength) == '/';
489
490        // Strip off the folder prefix (i.e., "/foldername"), leaving the rest of the path (i.e., "/en/pagename").
491
492        path = path.substring(prefixLength);
493        return path;
494    }
495
496    private PageRenderRequestParameters checkIfPage(Request request, String pageName, String pageActivationContext)
497    {
498        if (!componentClassResolver.isPageName(pageName))
499        {
500            return null;
501        }
502        String canonicalized = componentClassResolver.canonicalizePageName(pageName);
503
504        // If the page is only visible to the whitelist, but the request is not on the whitelist, then
505        // pretend the page doesn't exist!
506        if (isWhitelistOnlyAndNotValid(canonicalized))
507        {
508            return null;
509        }
510        try
511        {
512            EventContext activationContext = contextPathEncoder.decodePath(pageActivationContext);
513
514            boolean loopback = request.getParameter(TapestryConstants.PAGE_LOOPBACK_PARAMETER_NAME) != null;
515
516            return new PageRenderRequestParameters(canonicalized, activationContext, loopback);
517        } catch (IllegalArgumentException e)
518        {
519            // TAP5-2436
520            return null;
521        }
522    }
523
524    private boolean isWhitelistOnlyAndNotValid(String canonicalized)
525    {
526        return metaDataLocator.findMeta(MetaDataConstants.WHITELIST_ONLY_PAGE, canonicalized, boolean.class) &&
527                !clientWhitelist.isClientRequestOnWhitelist();
528    }
529
530    public void appendContext(boolean seperatorRequired, EventContext context, StringBuilder builder)
531    {
532        String encoded = contextPathEncoder.encodeIntoPath(context);
533
534        if (encoded.length() > 0)
535        {
536            if (seperatorRequired)
537            {
538                builder.append(SLASH);
539            }
540
541            builder.append(encoded);
542        }
543    }
544}