blob: aacefc0fe8601f48605c9e89a838aeee1ae3f097 [file] [log] [blame]
// Copyright 2009, 2010 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry5.internal.services;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.tapestry5.EventConstants;
import org.apache.tapestry5.EventContext;
import org.apache.tapestry5.Link;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.TapestryConstants;
import org.apache.tapestry5.internal.InternalConstants;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.services.*;
public class ComponentEventLinkEncoderImpl implements ComponentEventLinkEncoder
{
private final ComponentClassResolver componentClassResolver;
private final ContextPathEncoder contextPathEncoder;
private final LocalizationSetter localizationSetter;
private final Request request;
private final Response response;
private final RequestSecurityManager requestSecurityManager;
private final BaseURLSource baseURLSource;
private final PersistentLocale persistentLocale;
private final boolean encodeLocaleIntoPath;
private static final int BUFFER_SIZE = 100;
private static final char SLASH = '/';
// A beast that recognizes all the elements of a path in a single go.
// We skip the leading slash, then take the next few terms (until a dot or a colon)
// as the page name. Then there's a sequence that sees a dot
// and recognizes the nested component id (which may be missing), which ends
// at the colon, or at the slash (or the end of the string). The colon identifies
// the event name (the event name is also optional). A valid path will always have
// a nested component id or an event name (or both) ... when both are missing, then the
// path is most likely a page render request. After the optional event name,
// the next piece is the action context, which is the remainder of the path.
private final Pattern PATH_PATTERN = Pattern.compile(
"^/" + // The leading slash is recognized but skipped
"(((\\w+)/)*(\\w+))" + // A series of folder names leading up to the page name, forming
// the logical page name
"(\\.(\\w+(\\.\\w+)*))?" + // The first dot separates the page name from the nested
// component id
"(\\:(\\w+))?" + // A colon, then the event type
"(/(.*))?", // A slash, then the action context
Pattern.COMMENTS);
// Constants for the match groups in the above pattern.
private static final int LOGICAL_PAGE_NAME = 1;
private static final int NESTED_ID = 6;
private static final int EVENT_NAME = 9;
private static final int CONTEXT = 11;
public ComponentEventLinkEncoderImpl(ComponentClassResolver componentClassResolver,
ContextPathEncoder contextPathEncoder, LocalizationSetter localizationSetter, Request request,
Response response, RequestSecurityManager requestSecurityManager, BaseURLSource baseURLSource,
PersistentLocale persistentLocale, @Symbol(SymbolConstants.ENCODE_LOCALE_INTO_PATH)
boolean encodeLocaleIntoPath)
{
this.componentClassResolver = componentClassResolver;
this.contextPathEncoder = contextPathEncoder;
this.localizationSetter = localizationSetter;
this.request = request;
this.response = response;
this.requestSecurityManager = requestSecurityManager;
this.baseURLSource = baseURLSource;
this.persistentLocale = persistentLocale;
this.encodeLocaleIntoPath = encodeLocaleIntoPath;
}
public Link createPageRenderLink(PageRenderRequestParameters parameters)
{
StringBuilder builder = new StringBuilder(BUFFER_SIZE);
// Build up the absolute URI.
String activePageName = parameters.getLogicalPageName();
builder.append(request.getContextPath());
encodeLocale(builder);
builder.append(SLASH);
String encodedPageName = encodePageName(activePageName);
builder.append(encodedPageName);
appendContext(encodedPageName.length() > 0, parameters.getActivationContext(), builder);
Link link = new LinkImpl(builder.toString(), false, requestSecurityManager.checkPageSecurity(activePageName),
response, contextPathEncoder, baseURLSource);
if (parameters.isLoopback())
link.addParameter(TapestryConstants.PAGE_LOOPBACK_PARAMETER_NAME, "t");
return link;
}
private String encodePageName(String pageName)
{
if (pageName.equalsIgnoreCase("index"))
return "";
String encoded = pageName.toLowerCase();
if (!encoded.endsWith("/index"))
return encoded;
return encoded.substring(0, encoded.length() - 6);
}
private void encodeLocale(StringBuilder builder)
{
if (encodeLocaleIntoPath)
{
Locale locale = persistentLocale.get();
if (locale != null)
{
builder.append(SLASH);
builder.append(locale.toString());
}
}
}
public Link createComponentEventLink(ComponentEventRequestParameters parameters, boolean forForm)
{
StringBuilder builder = new StringBuilder(BUFFER_SIZE);
// Build up the absolute URI.
String activePageName = parameters.getActivePageName();
String containingPageName = parameters.getContainingPageName();
String eventType = parameters.getEventType();
String nestedComponentId = parameters.getNestedComponentId();
boolean hasComponentId = InternalUtils.isNonBlank(nestedComponentId);
builder.append(request.getContextPath());
encodeLocale(builder);
builder.append(SLASH);
builder.append(activePageName.toLowerCase());
if (hasComponentId)
{
builder.append('.');
builder.append(nestedComponentId);
}
if (!hasComponentId || !eventType.equals(EventConstants.ACTION))
{
builder.append(":");
builder.append(encodePageName(eventType));
}
appendContext(true, parameters.getEventContext(), builder);
Link result = new LinkImpl(builder.toString(), forForm,
requestSecurityManager.checkPageSecurity(activePageName), response, contextPathEncoder, baseURLSource);
EventContext pageActivationContext = parameters.getPageActivationContext();
if (pageActivationContext.getCount() != 0)
{
// Reuse the builder
builder.setLength(0);
appendContext(true, pageActivationContext, builder);
// Omit that first slash
result.addParameter(InternalConstants.PAGE_CONTEXT_NAME, builder.substring(1));
}
// TAPESTRY-2044: Sometimes the active page drags in components from another page and we
// need to differentiate that.
if (!containingPageName.equalsIgnoreCase(activePageName))
result.addParameter(InternalConstants.CONTAINER_PAGE_NAME, encodePageName(containingPageName));
return result;
}
public ComponentEventRequestParameters decodeComponentEventRequest(Request request)
{
Matcher matcher = PATH_PATTERN.matcher(request.getPath());
if (!matcher.matches())
return null;
String nestedComponentId = matcher.group(NESTED_ID);
String eventType = matcher.group(EVENT_NAME);
if (nestedComponentId == null && eventType == null)
return null;
String activePageName = matcher.group(LOGICAL_PAGE_NAME);
int slashx = activePageName.indexOf('/');
String possibleLocaleName = slashx > 0 ? activePageName.substring(0, slashx) : "";
if (localizationSetter.setLocaleFromLocaleName(possibleLocaleName))
activePageName = activePageName.substring(slashx + 1);
if (!componentClassResolver.isPageName(activePageName))
return null;
activePageName = componentClassResolver.canonicalizePageName(activePageName);
EventContext eventContext = contextPathEncoder.decodePath(matcher.group(CONTEXT));
EventContext activationContext = contextPathEncoder.decodePath(request
.getParameter(InternalConstants.PAGE_CONTEXT_NAME));
// The event type is often omitted, and defaults to "action".
if (eventType == null)
eventType = EventConstants.ACTION;
if (nestedComponentId == null)
nestedComponentId = "";
String containingPageName = request.getParameter(InternalConstants.CONTAINER_PAGE_NAME);
if (containingPageName == null)
containingPageName = activePageName;
else
containingPageName = componentClassResolver.canonicalizePageName(containingPageName);
return new ComponentEventRequestParameters(activePageName, containingPageName, nestedComponentId, eventType,
activationContext, eventContext);
}
public PageRenderRequestParameters decodePageRenderRequest(Request request)
{
// The extended name may include a page activation context. The trick is
// to figure out where the logical page name stops and where the
// activation context begins. Further, strip out the leading slash.
String path = request.getPath();
// TAPESTRY-1343: Sometimes path is the empty string (it should always be at least a slash,
// but Tomcat may return the empty string for a root context request).
String extendedName = path.length() == 0 ? path : path.substring(1);
// Ignore trailing slashes in the path.
while (extendedName.endsWith("/"))
extendedName = extendedName.substring(0, extendedName.length() - 1);
int slashx = extendedName.indexOf('/');
// So, what can we have left?
// 1. A page name
// 2. A locale followed by a page name
// 3. A page name followed by activation context
// 4. A locale name, page name, activation context
// 5. Just activation context (for root Index page)
// 6. A locale name followed by activation context
String possibleLocaleName = slashx > 0 ? extendedName.substring(0, slashx) : extendedName;
if (localizationSetter.setLocaleFromLocaleName(possibleLocaleName))
{
extendedName = slashx > 0 ? extendedName.substring(slashx + 1) : "";
}
slashx = extendedName.length();
boolean atEnd = true;
while (slashx > 0)
{
String pageName = extendedName.substring(0, slashx);
String pageActivationContext = atEnd ? "" : extendedName.substring(slashx + 1);
PageRenderRequestParameters parameters = checkIfPage(request, pageName, pageActivationContext);
if (parameters != null)
return parameters;
// Work backwards, splitting at the next slash.
slashx = extendedName.lastIndexOf('/', slashx - 1);
atEnd = false;
}
// OK, maybe its all page activation context for the root Index page.
return checkIfPage(request, "", extendedName);
}
private PageRenderRequestParameters checkIfPage(Request request, String pageName, String pageActivationContext)
{
if (!componentClassResolver.isPageName(pageName))
return null;
EventContext activationContext = contextPathEncoder.decodePath(pageActivationContext);
String canonicalized = componentClassResolver.canonicalizePageName(pageName);
boolean loopback = request.getParameter(TapestryConstants.PAGE_LOOPBACK_PARAMETER_NAME) != null;
return new PageRenderRequestParameters(canonicalized, activationContext, loopback);
}
public void appendContext(boolean seperatorRequired, EventContext context, StringBuilder builder)
{
String encoded = contextPathEncoder.encodeIntoPath(context);
if (encoded.length() > 0)
{
if (seperatorRequired)
builder.append(SLASH);
builder.append(encoded);
}
}
}