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    public ComponentEventRequestParameters decodeComponentEventRequest(Request request)
256    {
257        String explicitLocale = null;
258
259        // Split the path around slashes into a mutable list of terms, which will be consumed term by term.
260
261        String requestPath = request.getPath();
262
263        if (applicationFolderPrefix != null)
264        {
265            requestPath = removeApplicationPrefix(requestPath);
266        }
267
268        List<String> path = splitPath(requestPath);
269
270
271
272        if (path.isEmpty())
273        {
274            return null;
275        }
276
277        // Next up: the locale (which is optional)
278
279        String potentialLocale = path.get(0);
280
281        if (localizationSetter.isSupportedLocaleName(potentialLocale))
282        {
283            explicitLocale = potentialLocale;
284            path.remove(0);
285        }
286
287        StringBuilder pageName = new StringBuilder(100);
288        String sep = "";
289
290        while (!path.isEmpty())
291        {
292            String name = path.remove(0);
293            String eventType = EventConstants.ACTION;
294            String nestedComponentId = "";
295
296            boolean found = false;
297
298            // First, look for an explicit action name.
299
300            int colonx = name.lastIndexOf(':');
301
302            if (colonx > 0)
303            {
304                found = true;
305                eventType = name.substring(colonx + 1);
306                name = name.substring(0, colonx);
307            }
308
309            int dotx = name.indexOf('.');
310
311            if (dotx > 0)
312            {
313                found = true;
314                nestedComponentId = name.substring(dotx + 1);
315                name = name.substring(0, dotx);
316            }
317
318            pageName.append(sep).append(name);
319
320            if (found)
321            {
322                ComponentEventRequestParameters result = validateAndConstructComponentEventRequest(request, pageName.toString(), nestedComponentId, eventType, path);
323
324                if (result == null)
325                {
326                    return result;
327                }
328
329                if (explicitLocale == null)
330                {
331                    setLocaleFromRequest(request);
332                } else
333                {
334                    localizationSetter.setLocaleFromLocaleName(explicitLocale);
335                }
336
337                return result;
338            }
339
340            // Continue on to the next name in the path
341            sep = "/";
342        }
343
344        // Path empty before finding something that looks like a component id or event name, so
345        // it is not a component event request.
346
347        return null;
348    }
349
350    private ComponentEventRequestParameters validateAndConstructComponentEventRequest(Request request, String pageName, String nestedComponentId, String eventType, List<String> remainingPath)
351    {
352        if (!componentClassResolver.isPageName(pageName))
353        {
354            return null;
355        }
356
357        String activePageName = componentClassResolver.canonicalizePageName(pageName);
358
359        if (isWhitelistOnlyAndNotValid(activePageName))
360        {
361            return null;
362        }
363
364        String value = request.getParameter(InternalConstants.CONTAINER_PAGE_NAME);
365
366        String containingPageName = value == null
367                ? activePageName
368                : componentClassResolver.canonicalizePageName(value);
369
370        EventContext eventContext = contextPathEncoder.decodePath(joinPath(remainingPath));
371        EventContext activationContext = contextPathEncoder.decodePath(request.getParameter(InternalConstants.PAGE_CONTEXT_NAME));
372
373        return new ComponentEventRequestParameters(activePageName, containingPageName, nestedComponentId, eventType,
374                activationContext, eventContext);
375    }
376
377    private void setLocaleFromRequest(Request request)
378    {
379        Locale locale = request.getLocale();
380
381        // And explicit locale will have invoked setLocaleFromLocaleName().
382
383        localizationSetter.setNonPersistentLocaleFromLocaleName(locale.toString());
384    }
385
386    public PageRenderRequestParameters decodePageRenderRequest(Request request)
387    {
388        boolean explicitLocale = false;
389
390        // The extended name may include a page activation context. The trick is
391        // to figure out where the logical page name stops and where the
392        // activation context begins. Further, strip out the leading slash.
393
394        String path = request.getPath();
395
396        if (applicationFolderPrefix != null)
397        {
398            path = removeApplicationPrefix(path);
399        }
400
401
402        // TAPESTRY-1343: Sometimes path is the empty string (it should always be at least a slash,
403        // but Tomcat may return the empty string for a root context request).
404
405        String extendedName = path.length() == 0 ? path : path.substring(1);
406
407        // Ignore trailing slashes in the path.
408        while (extendedName.endsWith("/"))
409        {
410            extendedName = extendedName.substring(0, extendedName.length() - 1);
411        }
412
413        int slashx = extendedName.indexOf('/');
414
415        // So, what can we have left?
416        // 1. A page name
417        // 2. A locale followed by a page name
418        // 3. A page name followed by activation context
419        // 4. A locale name, page name, activation context
420        // 5. Just activation context (for root Index page)
421        // 6. A locale name followed by activation context
422
423        String possibleLocaleName = slashx > 0 ? extendedName.substring(0, slashx) : extendedName;
424
425        if (localizationSetter.setLocaleFromLocaleName(possibleLocaleName))
426        {
427            extendedName = slashx > 0 ? extendedName.substring(slashx + 1) : "";
428            explicitLocale = true;
429        }
430
431        slashx = extendedName.length();
432        boolean atEnd = true;
433
434        while (slashx > 0)
435        {
436            String pageName = extendedName.substring(0, slashx);
437            String pageActivationContext = atEnd ? "" : extendedName.substring(slashx + 1);
438
439            PageRenderRequestParameters parameters = checkIfPage(request, pageName, pageActivationContext);
440
441            if (parameters != null)
442            {
443                return parameters;
444            }
445
446            // Work backwards, splitting at the next slash.
447            slashx = extendedName.lastIndexOf('/', slashx - 1);
448
449            atEnd = false;
450        }
451
452        // OK, maybe its all page activation context for the root Index page.
453
454        PageRenderRequestParameters result = checkIfPage(request, "", extendedName);
455
456        if (result != null && !explicitLocale)
457        {
458            setLocaleFromRequest(request);
459        }
460
461        return result;
462    }
463
464    private String removeApplicationPrefix(String path) {
465        int prefixLength = applicationFolderPrefix.length();
466
467        assert path.substring(0, prefixLength).equalsIgnoreCase(applicationFolderPrefix);
468
469        // This checks that the character after the prefix is a slash ... the extra complexity
470        // only seems to occur in Selenium. There's some ambiguity about what to do with a request for
471        // the application folder that doesn't end with a slash. Manuyal with Chrome and IE 8 shows that such
472        // requests are passed through with a training slash,  automated testing with Selenium and FireFox
473        // can include requests for the folder without the trailing slash.
474
475        assert path.length() <= prefixLength || path.charAt(prefixLength) == '/';
476
477        // Strip off the folder prefix (i.e., "/foldername"), leaving the rest of the path (i.e., "/en/pagename").
478
479        path = path.substring(prefixLength);
480        return path;
481    }
482
483    private PageRenderRequestParameters checkIfPage(Request request, String pageName, String pageActivationContext)
484    {
485        if (!componentClassResolver.isPageName(pageName))
486        {
487            return null;
488        }
489        String canonicalized = componentClassResolver.canonicalizePageName(pageName);
490
491        // If the page is only visible to the whitelist, but the request is not on the whitelist, then
492        // pretend the page doesn't exist!
493        if (isWhitelistOnlyAndNotValid(canonicalized))
494        {
495            return null;
496        }
497        try
498        {
499            EventContext activationContext = contextPathEncoder.decodePath(pageActivationContext);
500
501            boolean loopback = request.getParameter(TapestryConstants.PAGE_LOOPBACK_PARAMETER_NAME) != null;
502
503            return new PageRenderRequestParameters(canonicalized, activationContext, loopback);
504        } catch (IllegalArgumentException e)
505        {
506            // TAP5-2436
507            return null;
508        }
509    }
510
511    private boolean isWhitelistOnlyAndNotValid(String canonicalized)
512    {
513        return metaDataLocator.findMeta(MetaDataConstants.WHITELIST_ONLY_PAGE, canonicalized, boolean.class) &&
514                !clientWhitelist.isClientRequestOnWhitelist();
515    }
516
517    public void appendContext(boolean seperatorRequired, EventContext context, StringBuilder builder)
518    {
519        String encoded = contextPathEncoder.encodeIntoPath(context);
520
521        if (encoded.length() > 0)
522        {
523            if (seperatorRequired)
524            {
525                builder.append(SLASH);
526            }
527
528            builder.append(encoded);
529        }
530    }
531}