001    // Copyright 2009, 2010, 2011 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.ioc.annotations.Symbol;
020    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
021    import org.apache.tapestry5.services.*;
022    import org.apache.tapestry5.services.security.ClientWhitelist;
023    
024    import java.util.Locale;
025    import java.util.regex.Matcher;
026    import java.util.regex.Pattern;
027    
028    public class ComponentEventLinkEncoderImpl implements ComponentEventLinkEncoder
029    {
030        private final ComponentClassResolver componentClassResolver;
031    
032        private final ContextPathEncoder contextPathEncoder;
033    
034        private final LocalizationSetter localizationSetter;
035    
036        private final Request request;
037    
038        private final Response response;
039    
040        private final RequestSecurityManager requestSecurityManager;
041    
042        private final BaseURLSource baseURLSource;
043    
044        private final PersistentLocale persistentLocale;
045    
046        private final boolean encodeLocaleIntoPath;
047    
048        private final MetaDataLocator metaDataLocator;
049    
050        private final ClientWhitelist clientWhitelist;
051    
052        private final String applicationFolder;
053    
054        private final String applicationFolderPrefix;
055    
056        private static final int BUFFER_SIZE = 100;
057    
058        private static final char SLASH = '/';
059    
060        // A beast that recognizes all the elements of a path in a single go.
061        // We skip the leading slash, then take the next few terms (until a dot or a colon)
062        // as the page name. Then there's a sequence that sees a dot
063        // and recognizes the nested component id (which may be missing), which ends
064        // at the colon, or at the slash (or the end of the string). The colon identifies
065        // the event name (the event name is also optional). A valid path will always have
066        // a nested component id or an event name (or both) ... when both are missing, then the
067        // path is most likely a page render request. After the optional event name,
068        // the next piece is the action context, which is the remainder of the path.
069    
070        private final Pattern COMPONENT_EVENT_REQUEST_PATH_PATTERN;
071    
072        // Constants for the match groups in the above pattern.
073        private static final int LOGICAL_PAGE_NAME = 1;
074        private static final int NESTED_ID = 6;
075        private static final int EVENT_NAME = 9;
076        private static final int CONTEXT = 11;
077    
078        public ComponentEventLinkEncoderImpl(ComponentClassResolver componentClassResolver,
079                                             ContextPathEncoder contextPathEncoder, LocalizationSetter localizationSetter, Request request,
080                                             Response response, RequestSecurityManager requestSecurityManager, BaseURLSource baseURLSource,
081                                             PersistentLocale persistentLocale, @Symbol(SymbolConstants.ENCODE_LOCALE_INTO_PATH)
082        boolean encodeLocaleIntoPath, @Symbol(SymbolConstants.APPLICATION_FOLDER) String applicationFolder, MetaDataLocator metaDataLocator, ClientWhitelist clientWhitelist)
083        {
084            this.componentClassResolver = componentClassResolver;
085            this.contextPathEncoder = contextPathEncoder;
086            this.localizationSetter = localizationSetter;
087            this.request = request;
088            this.response = response;
089            this.requestSecurityManager = requestSecurityManager;
090            this.baseURLSource = baseURLSource;
091            this.persistentLocale = persistentLocale;
092            this.encodeLocaleIntoPath = encodeLocaleIntoPath;
093            this.applicationFolder = applicationFolder;
094            this.metaDataLocator = metaDataLocator;
095            this.clientWhitelist = clientWhitelist;
096    
097            boolean hasAppFolder = applicationFolder.equals("");
098    
099            applicationFolderPrefix = hasAppFolder ? null : SLASH + applicationFolder;
100    
101            String applicationFolderPattern = hasAppFolder ? "" : applicationFolder + SLASH;
102    
103            COMPONENT_EVENT_REQUEST_PATH_PATTERN = Pattern.compile(
104    
105                    "^/" + // The leading slash is recognized but skipped
106                            applicationFolderPattern + // The folder containing the application (TAP5-743)
107                            "(((\\w(?:\\w|-)*)/)*(\\w+))" + // A series of folder names (which allow dashes) leading up to the page name, forming
108                            // the logical page name (may include the locale name)
109                            "(\\.(\\w+(\\.\\w+)*))?" + // The first dot separates the page name from the nested
110                            // component id
111                            "(\\:(\\w+))?" + // A colon, then the event type
112                            "(/(.*))?", // A slash, then the action context
113                    Pattern.COMMENTS);
114        }
115    
116        public Link createPageRenderLink(PageRenderRequestParameters parameters)
117        {
118            StringBuilder builder = new StringBuilder(BUFFER_SIZE);
119    
120            // Build up the absolute URI.
121    
122            String activePageName = parameters.getLogicalPageName();
123    
124            builder.append(request.getContextPath());
125    
126            encodeAppFolderAndLocale(builder);
127    
128            builder.append(SLASH);
129    
130            String encodedPageName = encodePageName(activePageName);
131    
132            builder.append(encodedPageName);
133    
134            appendContext(encodedPageName.length() > 0, parameters.getActivationContext(), builder);
135    
136            Link link = new LinkImpl(builder.toString(), false, requestSecurityManager.checkPageSecurity(activePageName),
137                    response, contextPathEncoder, baseURLSource);
138    
139            if (parameters.isLoopback())
140            {
141                link.addParameter(TapestryConstants.PAGE_LOOPBACK_PARAMETER_NAME, "t");
142            }
143    
144            return link;
145        }
146    
147        private void encodeAppFolderAndLocale(StringBuilder builder)
148        {
149            if (!applicationFolder.equals(""))
150            {
151                builder.append(SLASH).append(applicationFolder);
152            }
153    
154            if (encodeLocaleIntoPath)
155            {
156                Locale locale = persistentLocale.get();
157    
158                if (locale != null)
159                {
160                    builder.append(SLASH);
161                    builder.append(locale.toString());
162                }
163            }
164        }
165    
166        private String encodePageName(String pageName)
167        {
168            if (pageName.equalsIgnoreCase("index"))
169                return "";
170    
171            String encoded = pageName.toLowerCase();
172    
173            if (!encoded.endsWith("/index"))
174                return encoded;
175    
176            return encoded.substring(0, encoded.length() - 6);
177        }
178    
179        public Link createComponentEventLink(ComponentEventRequestParameters parameters, boolean forForm)
180        {
181            StringBuilder builder = new StringBuilder(BUFFER_SIZE);
182    
183            // Build up the absolute URI.
184    
185            String activePageName = parameters.getActivePageName();
186            String containingPageName = parameters.getContainingPageName();
187            String eventType = parameters.getEventType();
188    
189            String nestedComponentId = parameters.getNestedComponentId();
190            boolean hasComponentId = InternalUtils.isNonBlank(nestedComponentId);
191    
192            builder.append(request.getContextPath());
193    
194            encodeAppFolderAndLocale(builder);
195    
196            builder.append(SLASH);
197            builder.append(activePageName.toLowerCase());
198    
199            if (hasComponentId)
200            {
201                builder.append('.');
202                builder.append(nestedComponentId);
203            }
204    
205            if (!hasComponentId || !eventType.equals(EventConstants.ACTION))
206            {
207                builder.append(":");
208                builder.append(encodePageName(eventType));
209            }
210    
211            appendContext(true, parameters.getEventContext(), builder);
212    
213            Link result = new LinkImpl(builder.toString(), forForm,
214                    requestSecurityManager.checkPageSecurity(activePageName), response, contextPathEncoder, baseURLSource);
215    
216            EventContext pageActivationContext = parameters.getPageActivationContext();
217    
218            if (pageActivationContext.getCount() != 0)
219            {
220                // Reuse the builder
221                builder.setLength(0);
222                appendContext(true, pageActivationContext, builder);
223    
224                // Omit that first slash
225                result.addParameter(InternalConstants.PAGE_CONTEXT_NAME, builder.substring(1));
226            }
227    
228            // TAPESTRY-2044: Sometimes the active page drags in components from another page and we
229            // need to differentiate that.
230    
231            if (!containingPageName.equalsIgnoreCase(activePageName))
232                result.addParameter(InternalConstants.CONTAINER_PAGE_NAME, encodePageName(containingPageName));
233    
234            return result;
235        }
236    
237        public ComponentEventRequestParameters decodeComponentEventRequest(Request request)
238        {
239            boolean explicitLocale = false;
240    
241            Matcher matcher = COMPONENT_EVENT_REQUEST_PATH_PATTERN.matcher(request.getPath());
242    
243            if (!matcher.matches())
244                return null;
245    
246            String nestedComponentId = matcher.group(NESTED_ID);
247    
248            String eventType = matcher.group(EVENT_NAME);
249    
250            if (nestedComponentId == null && eventType == null)
251                return null;
252    
253            String activePageName = matcher.group(LOGICAL_PAGE_NAME);
254    
255            int slashx = activePageName.indexOf('/');
256    
257            String possibleLocaleName = slashx > 0 ? activePageName.substring(0, slashx) : "";
258    
259            if (localizationSetter.setLocaleFromLocaleName(possibleLocaleName))
260            {
261                activePageName = activePageName.substring(slashx + 1);
262                explicitLocale = true;
263            }
264    
265            if (!componentClassResolver.isPageName(activePageName))
266                return null;
267    
268            activePageName = componentClassResolver.canonicalizePageName(activePageName);
269    
270            if (isWhitelistOnlyAndNotValid(activePageName))
271            {
272                return null;
273            }
274    
275            EventContext eventContext = contextPathEncoder.decodePath(matcher.group(CONTEXT));
276    
277            EventContext activationContext = contextPathEncoder.decodePath(request
278                    .getParameter(InternalConstants.PAGE_CONTEXT_NAME));
279    
280            // The event type is often omitted, and defaults to "action".
281    
282            if (eventType == null)
283                eventType = EventConstants.ACTION;
284    
285            if (nestedComponentId == null)
286                nestedComponentId = "";
287    
288            String containingPageName = request.getParameter(InternalConstants.CONTAINER_PAGE_NAME);
289    
290            if (containingPageName == null)
291                containingPageName = activePageName;
292            else
293                containingPageName = componentClassResolver.canonicalizePageName(containingPageName);
294    
295            if (!explicitLocale)
296            {
297                setLocaleFromRequest(request);
298            }
299    
300            return new ComponentEventRequestParameters(activePageName, containingPageName, nestedComponentId, eventType,
301                    activationContext, eventContext);
302        }
303    
304        private void setLocaleFromRequest(Request request)
305        {
306            Locale locale = request.getLocale();
307    
308            // And explicit locale will have invoked setLocaleFromLocaleName().
309    
310            localizationSetter.setNonPeristentLocaleFromLocaleName(locale.toString());
311        }
312    
313        public PageRenderRequestParameters decodePageRenderRequest(Request request)
314        {
315            boolean explicitLocale = false;
316    
317            // The extended name may include a page activation context. The trick is
318            // to figure out where the logical page name stops and where the
319            // activation context begins. Further, strip out the leading slash.
320    
321            String path = request.getPath();
322    
323            if (applicationFolderPrefix != null)
324            {
325                int prefixLength = applicationFolderPrefix.length();
326    
327                assert path.substring(0, prefixLength).equalsIgnoreCase(applicationFolderPrefix);
328    
329                // This checks that the character after the prefix is a slash ... the extra complexity
330                // only seems to occur in Selenium. There's some ambiguity about what to do with a request for
331                // the application folder that doesn't end with a slash. Manuyal with Chrome and IE 8 shows that such
332                // requests are passed through with a training slash,  automated testing with Selenium and FireFox
333                // can include requests for the folder without the trailing slash.
334    
335                assert path.length() <= prefixLength || path.charAt(prefixLength) == '/';
336    
337                // Strip off the folder prefix (i.e., "/foldername"), leaving the rest of the path (i.e., "/en/pagename").
338    
339                path = path.substring(prefixLength);
340            }
341    
342    
343            // TAPESTRY-1343: Sometimes path is the empty string (it should always be at least a slash,
344            // but Tomcat may return the empty string for a root context request).
345    
346            String extendedName = path.length() == 0 ? path : path.substring(1);
347    
348            // Ignore trailing slashes in the path.
349            while (extendedName.endsWith("/"))
350            {
351                extendedName = extendedName.substring(0, extendedName.length() - 1);
352            }
353    
354            int slashx = extendedName.indexOf('/');
355    
356            // So, what can we have left?
357            // 1. A page name
358            // 2. A locale followed by a page name
359            // 3. A page name followed by activation context
360            // 4. A locale name, page name, activation context
361            // 5. Just activation context (for root Index page)
362            // 6. A locale name followed by activation context
363    
364            String possibleLocaleName = slashx > 0 ? extendedName.substring(0, slashx) : extendedName;
365    
366            if (localizationSetter.setLocaleFromLocaleName(possibleLocaleName))
367            {
368                extendedName = slashx > 0 ? extendedName.substring(slashx + 1) : "";
369                explicitLocale = true;
370            }
371    
372            slashx = extendedName.length();
373            boolean atEnd = true;
374    
375            while (slashx > 0)
376            {
377                String pageName = extendedName.substring(0, slashx);
378                String pageActivationContext = atEnd ? "" : extendedName.substring(slashx + 1);
379    
380                PageRenderRequestParameters parameters = checkIfPage(request, pageName, pageActivationContext);
381    
382                if (parameters != null)
383                {
384                    return parameters;
385                }
386    
387                // Work backwards, splitting at the next slash.
388                slashx = extendedName.lastIndexOf('/', slashx - 1);
389    
390                atEnd = false;
391            }
392    
393            // OK, maybe its all page activation context for the root Index page.
394    
395            PageRenderRequestParameters result = checkIfPage(request, "", extendedName);
396    
397            if (result != null && !explicitLocale)
398            {
399                setLocaleFromRequest(request);
400            }
401    
402            return result;
403        }
404    
405        private PageRenderRequestParameters checkIfPage(Request request, String pageName, String pageActivationContext)
406        {
407            if (!componentClassResolver.isPageName(pageName))
408            {
409                return null;
410            }
411    
412            String canonicalized = componentClassResolver.canonicalizePageName(pageName);
413    
414            // If the page is only visible to the whitelist, but the request is not on the whitelist, then
415            // pretend the page doesn't exist!
416            if (isWhitelistOnlyAndNotValid(canonicalized))
417            {
418                return null;
419            }
420    
421            EventContext activationContext = contextPathEncoder.decodePath(pageActivationContext);
422    
423            boolean loopback = request.getParameter(TapestryConstants.PAGE_LOOPBACK_PARAMETER_NAME) != null;
424    
425            return new PageRenderRequestParameters(canonicalized, activationContext, loopback);
426        }
427    
428        private boolean isWhitelistOnlyAndNotValid(String canonicalized)
429        {
430            return metaDataLocator.findMeta(MetaDataConstants.WHITELIST_ONLY_PAGE, canonicalized, boolean.class) &&
431                    !clientWhitelist.isClientRequestOnWhitelist();
432        }
433    
434        public void appendContext(boolean seperatorRequired, EventContext context, StringBuilder builder)
435        {
436            String encoded = contextPathEncoder.encodeIntoPath(context);
437    
438            if (encoded.length() > 0)
439            {
440                if (seperatorRequired)
441                {
442                    builder.append(SLASH);
443                }
444    
445                builder.append(encoded);
446            }
447        }
448    }