0001: /*
0002: * Licensed to the Apache Software Foundation (ASF) under one or more
0003: * contributor license agreements. See the NOTICE file distributed with
0004: * this work for additional information regarding copyright ownership.
0005: * The ASF licenses this file to You under the Apache License, Version 2.0
0006: * (the "License"); you may not use this file except in compliance with
0007: * the License. You may obtain a copy of the License at
0008: *
0009: * http://www.apache.org/licenses/LICENSE-2.0
0010: *
0011: * Unless required by applicable law or agreed to in writing, software
0012: * distributed under the License is distributed on an "AS IS" BASIS,
0013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0014: * See the License for the specific language governing permissions and
0015: * limitations under the License.
0016: */
0017: package org.apache.wicket.protocol.http.request;
0018:
0019: import java.util.Collection;
0020: import java.util.Comparator;
0021: import java.util.Iterator;
0022: import java.util.Map;
0023: import java.util.TreeMap;
0024: import java.util.TreeSet;
0025: import java.util.Map.Entry;
0026:
0027: import org.apache.wicket.Application;
0028: import org.apache.wicket.Component;
0029: import org.apache.wicket.IPageMap;
0030: import org.apache.wicket.IRedirectListener;
0031: import org.apache.wicket.IRequestTarget;
0032: import org.apache.wicket.Page;
0033: import org.apache.wicket.PageMap;
0034: import org.apache.wicket.PageParameters;
0035: import org.apache.wicket.Request;
0036: import org.apache.wicket.RequestCycle;
0037: import org.apache.wicket.RequestListenerInterface;
0038: import org.apache.wicket.Session;
0039: import org.apache.wicket.WicketRuntimeException;
0040: import org.apache.wicket.protocol.http.UnitTestSettings;
0041: import org.apache.wicket.request.IRequestCodingStrategy;
0042: import org.apache.wicket.request.IRequestTargetMountsInfo;
0043: import org.apache.wicket.request.RequestParameters;
0044: import org.apache.wicket.request.target.coding.IRequestTargetUrlCodingStrategy;
0045: import org.apache.wicket.request.target.coding.WebRequestEncoder;
0046: import org.apache.wicket.request.target.component.BookmarkableListenerInterfaceRequestTarget;
0047: import org.apache.wicket.request.target.component.IBookmarkablePageRequestTarget;
0048: import org.apache.wicket.request.target.component.IPageRequestTarget;
0049: import org.apache.wicket.request.target.component.listener.IListenerInterfaceRequestTarget;
0050: import org.apache.wicket.request.target.resource.ISharedResourceRequestTarget;
0051: import org.apache.wicket.util.string.AppendingStringBuffer;
0052: import org.apache.wicket.util.string.PrependingStringBuffer;
0053: import org.apache.wicket.util.string.Strings;
0054: import org.slf4j.Logger;
0055: import org.slf4j.LoggerFactory;
0056:
0057: /**
0058: * Request parameters factory implementation that uses http request parameters
0059: * and path info to construct the request parameters object.
0060: *
0061: * @author Eelco Hillenius
0062: * @author Jonathan Locke
0063: */
0064: public class WebRequestCodingStrategy implements
0065: IRequestCodingStrategy, IRequestTargetMountsInfo {
0066: /** Name of interface target query parameter */
0067: public static final String NAME_SPACE = "wicket:";
0068:
0069: /** Name of interface target query parameter */
0070: public static final String INTERFACE_PARAMETER_NAME = NAME_SPACE
0071: + "interface";
0072:
0073: /** AJAX query parameter name */
0074: public static final String BEHAVIOR_ID_PARAMETER_NAME = NAME_SPACE
0075: + "behaviorId";
0076:
0077: /** Parameter name used all over the place */
0078: public static final String BOOKMARKABLE_PAGE_PARAMETER_NAME = NAME_SPACE
0079: + "bookmarkablePage";
0080:
0081: /** Pagemap parameter constant */
0082: public static final String PAGEMAP = NAME_SPACE + "pageMapName";
0083:
0084: /**
0085: * Url name of the default pagemap
0086: *
0087: * When we encode the default pagemap name in a url we cannot always use
0088: * null or "" because it breaks urls which are encoded with /param1/value1/
0089: * eg /product/14/wicket:pageMapName/ split on / will split into
0090: * {product,14,wicket:pageMapName}
0091: */
0092: public static final String DEFAULT_PAGEMAP_NAME = "wicketdef";
0093:
0094: /** The URL path prefix expected for (so called) resources (not html pages). */
0095: public static final String RESOURCES_PATH_PREFIX = "resources/";
0096:
0097: /**
0098: * Parameter name that tells decode to ignore this request if the
0099: * page+version encoded in the url is not on top of the stack. The value of
0100: * this parameter is not important, it simply has to be present to enable
0101: * the behavior
0102: */
0103: public static final String IGNORE_IF_NOT_ACTIVE_PARAMETER_NAME = NAME_SPACE
0104: + "ignoreIfNotActive";
0105:
0106: /**
0107: * Various settings used to configure this strategy
0108: *
0109: * @author ivaynberg
0110: */
0111: public static class Settings {
0112: /** whether or not mount paths are case sensitive */
0113: private boolean mountsCaseSensitive = true;
0114:
0115: /**
0116: * Construct.
0117: */
0118: public Settings() {
0119: }
0120:
0121: /**
0122: * Sets mountsCaseSensitive.
0123: *
0124: * @param mountsCaseSensitive
0125: * mountsCaseSensitive
0126: */
0127: public void setMountsCaseSensitive(boolean mountsCaseSensitive) {
0128: this .mountsCaseSensitive = mountsCaseSensitive;
0129: }
0130:
0131: /**
0132: * Gets caseSensitive.
0133: *
0134: * @return caseSensitive
0135: */
0136: public boolean areMountsCaseSensitive() {
0137: return mountsCaseSensitive;
0138: }
0139: }
0140:
0141: /** log. */
0142: private static final Logger log = LoggerFactory
0143: .getLogger(WebRequestCodingStrategy.class);
0144:
0145: /**
0146: * map of path mounts for mount encoders on paths.
0147: * <p>
0148: * mountsOnPath is sorted by longest paths first to improve resolution of
0149: * possible path conflicts. <br />
0150: * For example: <br/> we mount Page1 on /page and Page2 on /page/test <br />
0151: * Page1 uses a parameters encoder that only encodes parameter values <br />
0152: * now suppose we want to access Page1 with a single paramter param="test".
0153: * we have a url collision since both pages can be access with /page/test
0154: * <br />
0155: * the sorting by longest path first guarantees that the iterator will
0156: * return the mount /page/test before it returns mount /page therefore
0157: * giving deterministic behavior to path resolution by always trying to
0158: * match the longest possible path first.
0159: * </p>
0160: */
0161: private final MountsMap mountsOnPath;
0162:
0163: /**
0164: * Construct.
0165: */
0166: public WebRequestCodingStrategy() {
0167: this (new Settings());
0168: }
0169:
0170: /**
0171: * Construct.
0172: *
0173: * @param settings
0174: */
0175: public WebRequestCodingStrategy(Settings settings) {
0176: if (settings == null) {
0177: throw new IllegalArgumentException(
0178: "Argument [[settings]] cannot be null");
0179: }
0180: mountsOnPath = new MountsMap(settings.areMountsCaseSensitive());
0181: }
0182:
0183: /**
0184: * @see org.apache.wicket.request.IRequestCodingStrategy#decode(org.apache.wicket.Request)
0185: */
0186: public final RequestParameters decode(final Request request) {
0187: final RequestParameters parameters = new RequestParameters();
0188: final String pathInfo = getRequestPath(request);
0189: parameters.setPath(pathInfo);
0190: parameters.setPageMapName(WebRequestCodingStrategy
0191: .decodePageMapName(request.getParameter(PAGEMAP)));
0192: addInterfaceParameters(request, parameters);
0193: addBookmarkablePageParameters(request, parameters);
0194: addResourceParameters(request, parameters);
0195: if (request.getParameter(IGNORE_IF_NOT_ACTIVE_PARAMETER_NAME) != null) {
0196: parameters.setOnlyProcessIfPathActive(true);
0197: }
0198:
0199: Map map = request.getParameterMap();
0200: Iterator iterator = map.keySet().iterator();
0201: while (iterator.hasNext()) {
0202: String key = (String) iterator.next();
0203: if (key.startsWith(NAME_SPACE)) {
0204: iterator.remove();
0205: }
0206: }
0207: parameters.setParameters(map);
0208: return parameters;
0209: }
0210:
0211: /**
0212: * Encode the given request target. If a mount is found, that mounted url
0213: * will be returned. Otherwise, one of the delegation methods will be
0214: * called. In case you are using custom targets that are not part of the
0215: * default target hierarchy, you need to override
0216: * {@link #doEncode(RequestCycle, IRequestTarget)}, which will be called
0217: * after the defaults have been tried. When that doesn't provide a url
0218: * either, and exception will be thrown saying that encoding could not be
0219: * done.
0220: *
0221: * @see org.apache.wicket.request.IRequestCodingStrategy#encode(org.apache.wicket.RequestCycle,
0222: * org.apache.wicket.IRequestTarget)
0223: */
0224: public final CharSequence encode(final RequestCycle requestCycle,
0225: final IRequestTarget requestTarget) {
0226: // First check to see whether the target is mounted
0227: CharSequence url = pathForTarget(requestTarget);
0228:
0229: if (url != null) {
0230: // Do nothing - we've found the URL and it's mounted.
0231: } else if (requestTarget instanceof IBookmarkablePageRequestTarget) {
0232: url = encode(requestCycle,
0233: (IBookmarkablePageRequestTarget) requestTarget);
0234: } else if (requestTarget instanceof ISharedResourceRequestTarget) {
0235: url = encode(requestCycle,
0236: (ISharedResourceRequestTarget) requestTarget);
0237: } else if (requestTarget instanceof IListenerInterfaceRequestTarget) {
0238: url = encode(requestCycle,
0239: (IListenerInterfaceRequestTarget) requestTarget);
0240: } else if (requestTarget instanceof IPageRequestTarget) {
0241: // This calls page.urlFor(IRedirectListener.INTERFACE), which calls
0242: // the function we're in again. We therefore need to jump out here
0243: // and return the url immediately, otherwise we end up prefixing it
0244: // with relative path or absolute prefixes twice.
0245: return encode(requestCycle,
0246: (IPageRequestTarget) requestTarget);
0247: }
0248: // fallthough for non-default request targets
0249: else {
0250: url = doEncode(requestCycle, requestTarget);
0251: }
0252:
0253: if (url != null) {
0254: // Add the actual URL. This will be relative to the Wicket
0255: // Servlet/Filter, with no leading '/'.
0256: PrependingStringBuffer prepender = new PrependingStringBuffer(
0257: url.toString());
0258:
0259: // Prepend prefix to the URL to make it relative to the current
0260: // request.
0261: prepender.prepend(requestCycle.getRequest()
0262: .getRelativePathPrefixToWicketHandler());
0263:
0264: String result = prepender.toString();
0265: // We need to special-case links to the home page if we're at the
0266: // same level.
0267: if (result.length() == 0) {
0268: result = "./";
0269: }
0270: return requestCycle.getOriginalResponse().encodeURL(result);
0271: }
0272:
0273: // Just return null intead of throwing an exception. So that it can be
0274: // handled better
0275: return null;
0276: }
0277:
0278: /**
0279: * @see org.apache.wicket.request.IRequestTargetMountsInfo#listMounts()
0280: */
0281: public IRequestTargetUrlCodingStrategy[] listMounts() {
0282: return (IRequestTargetUrlCodingStrategy[]) mountsOnPath
0283: .strategies()
0284: .toArray(
0285: new IRequestTargetUrlCodingStrategy[mountsOnPath
0286: .size()]);
0287: }
0288:
0289: /**
0290: * @see org.apache.wicket.request.IRequestTargetMounter#urlCodingStrategyForPath(java.lang.String)
0291: */
0292: public final IRequestTargetUrlCodingStrategy urlCodingStrategyForPath(
0293: String path) {
0294: if (path == null) {
0295: return mountsOnPath.strategyForMount(null);
0296: } else {
0297: IRequestTargetUrlCodingStrategy strategy = mountsOnPath
0298: .strategyForPath(path);
0299: if (strategy != null) {
0300: return strategy;
0301: }
0302: }
0303: return null;
0304: }
0305:
0306: /**
0307: * @see org.apache.wicket.request.IRequestTargetMounter#mount(java.lang.String,
0308: * org.apache.wicket.request.target.coding.IRequestTargetUrlCodingStrategy)
0309: */
0310: public final void mount(IRequestTargetUrlCodingStrategy encoder) {
0311: if (encoder == null) {
0312: throw new IllegalArgumentException(
0313: "Argument encoder must not be null");
0314: }
0315:
0316: String path = encoder.getMountPath();
0317: if (path == null) {
0318: throw new IllegalArgumentException(
0319: "Argument path must not be null");
0320: }
0321:
0322: if (path.equals("/") || path.equals("")) {
0323: throw new IllegalArgumentException(
0324: "The mount path '/' is reserved for the application home page");
0325: }
0326:
0327: // Sanity check in case someone doesn't read the javadoc while
0328: // implementing IRequestTargetUrlCodingStrategy
0329: if (path.startsWith("/")) {
0330: path = path.substring(1);
0331: }
0332:
0333: if (mountsOnPath.strategyForMount(path) != null) {
0334: throw new WicketRuntimeException(path
0335: + " is already mounted for "
0336: + mountsOnPath.strategyForMount(path));
0337: }
0338: mountsOnPath.mount(path, encoder);
0339: }
0340:
0341: /**
0342: * @see org.apache.wicket.request.IRequestCodingStrategy#pathForTarget(org.apache.wicket.IRequestTarget)
0343: */
0344: public final CharSequence pathForTarget(IRequestTarget requestTarget) {
0345: // first check whether the target was mounted
0346: IRequestTargetUrlCodingStrategy encoder = getMountEncoder(requestTarget);
0347: if (encoder != null) {
0348: return encoder.encode(requestTarget);
0349: }
0350: return null;
0351: }
0352:
0353: /**
0354: * @see org.apache.wicket.request.IRequestCodingStrategy#targetForRequest(org.apache.wicket.request.RequestParameters)
0355: */
0356: public final IRequestTarget targetForRequest(
0357: RequestParameters requestParameters) {
0358: IRequestTargetUrlCodingStrategy encoder = urlCodingStrategyForPath(requestParameters
0359: .getPath());
0360: if (encoder == null) {
0361: return null;
0362: }
0363: return encoder.decode(requestParameters);
0364: }
0365:
0366: /**
0367: * @see org.apache.wicket.request.IRequestCodingStrategy#unmount(java.lang.String)
0368: */
0369: public final void unmount(String path) {
0370: if (path == null) {
0371: throw new IllegalArgumentException(
0372: "Argument path must be not-null");
0373: }
0374:
0375: // sanity check
0376: if (path.startsWith("/")) {
0377: path = path.substring(1);
0378: }
0379:
0380: mountsOnPath.unmount(path);
0381: }
0382:
0383: /**
0384: * Adds bookmarkable page related parameters (page alias and optionally page
0385: * parameters). Any bookmarkable page alias mount will override this method;
0386: * hence if a mount is found, this method will not be called.
0387: *
0388: * If you override this method to behave differently then
0389: * {@link #encode(RequestCycle, IBookmarkablePageRequestTarget)} should also be
0390: * overridden to be in sync with that behaviour.
0391: *
0392: * @param request
0393: * the incoming request
0394: * @param parameters
0395: * the parameters object to set the found values on
0396: */
0397: protected void addBookmarkablePageParameters(final Request request,
0398: final RequestParameters parameters) {
0399: final String requestString = request
0400: .getParameter(WebRequestCodingStrategy.BOOKMARKABLE_PAGE_PARAMETER_NAME);
0401: if (requestString != null) {
0402: final String[] components = Strings.split(requestString,
0403: Component.PATH_SEPARATOR);
0404: if (components.length != 2) {
0405: throw new WicketRuntimeException(
0406: "Invalid bookmarkablePage parameter: "
0407: + requestString
0408: + ", expected: 'pageMapName:pageClassName'");
0409: }
0410:
0411: // Extract any pagemap name
0412: final String pageMapName = components[0];
0413: parameters
0414: .setPageMapName(pageMapName.length() == 0 ? PageMap.DEFAULT_NAME
0415: : pageMapName);
0416:
0417: // Extract bookmarkable page class name
0418: final String pageClassName = components[1];
0419: parameters.setBookmarkablePageClass(pageClassName);
0420: }
0421: }
0422:
0423: /**
0424: * Adds page related parameters (path and pagemap and optionally version and
0425: * interface).
0426: *
0427: * If you override this method to behave different then also
0428: * {@link #encode(RequestCycle, IListenerInterfaceRequestTarget)} should be
0429: * overridden to by in sync with that behaviour.
0430: *
0431: * @param request
0432: * the incoming request
0433: * @param parameters
0434: * the parameters object to set the found values on
0435: */
0436: protected void addInterfaceParameters(final Request request,
0437: final RequestParameters parameters) {
0438: addInterfaceParameters(request
0439: .getParameter(INTERFACE_PARAMETER_NAME), parameters);
0440: }
0441:
0442: /**
0443: * Analyses the passed in interfaceParameter for the relevant parts and puts
0444: * the parts as parameters in the provided request parameters object.
0445: *
0446: * @param interfaceParameter
0447: * The format of the interfaceParameter is: <code>
0448: * page-map-name:path:version:interface:behaviourId:urlDepth
0449: * </code>
0450: * @param parameters
0451: * parameters object to set the found parts in
0452: */
0453: public static void addInterfaceParameters(
0454: final String interfaceParameter,
0455: final RequestParameters parameters) {
0456: if (interfaceParameter == null) {
0457: return;
0458: }
0459:
0460: // Split into array of strings
0461: String[] pathComponents = Strings.split(interfaceParameter,
0462: Component.PATH_SEPARATOR);
0463:
0464: // There must be 6 components
0465: // pagemap:(pageid:componenta:componentb:...):version:interface:behavior:depth
0466: if (pathComponents.length < 6) {
0467: throw new WicketRuntimeException("Internal error parsing "
0468: + INTERFACE_PARAMETER_NAME + " = "
0469: + interfaceParameter);
0470: }
0471:
0472: // Extract version
0473: String versionNumberString = null;
0474: try {
0475: versionNumberString = pathComponents[pathComponents.length - 4];
0476: final int versionNumber = Strings
0477: .isEmpty(versionNumberString) ? 0 : Integer
0478: .parseInt(versionNumberString);
0479: parameters.setVersionNumber(versionNumber);
0480: } catch (NumberFormatException e) {
0481: throw new WicketRuntimeException(
0482: "Internal error parsing "
0483: + INTERFACE_PARAMETER_NAME
0484: + " = "
0485: + interfaceParameter
0486: + "; wrong format for page version argument. Expected a number but was '"
0487: + versionNumberString + "'", e);
0488: }
0489:
0490: // Set pagemap name
0491: final String pageMapName = pathComponents[0];
0492: parameters
0493: .setPageMapName(pageMapName.length() == 0 ? PageMap.DEFAULT_NAME
0494: : pageMapName);
0495:
0496: // Extract URL depth after last colon
0497: final String urlDepthString = pathComponents[pathComponents.length - 1];
0498: final int urlDepth = Strings.isEmpty(urlDepthString) ? -1
0499: : Integer.parseInt(urlDepthString);
0500: parameters.setUrlDepth(urlDepth);
0501:
0502: // Extract behaviour ID after last colon
0503: final String behaviourId = pathComponents[pathComponents.length - 2];
0504: parameters
0505: .setBehaviorId(behaviourId.length() != 0 ? behaviourId
0506: : null);
0507:
0508: // Extract interface name after second-to-last colon
0509: final String interfaceName = pathComponents[pathComponents.length - 3];
0510: parameters
0511: .setInterfaceName(interfaceName.length() != 0 ? interfaceName
0512: : IRedirectListener.INTERFACE.getName());
0513:
0514: // Component path is everything after pageMapName and before version
0515: final int start = pageMapName.length() + 1;
0516: final int end = interfaceParameter.length()
0517: - behaviourId.length() - interfaceName.length()
0518: - versionNumberString.length()
0519: - urlDepthString.length() - 4;
0520: final String componentPath = interfaceParameter.substring(
0521: start, end);
0522: parameters.setComponentPath(componentPath);
0523: }
0524:
0525: /**
0526: * Adds (shared) resource related parameters (resource key). Any shared
0527: * resource key mount will override this method; hence if a mount is found,
0528: * this method will not be called.
0529: *
0530: * If you override this method to behave different then also
0531: * {@link #encode(RequestCycle, ISharedResourceRequestTarget)} should be
0532: * overridden to by in sync with that behaviour.
0533: *
0534: * @param request
0535: * the incomming request
0536: * @param parameters
0537: * the parameters object to set the found values on
0538: */
0539: protected void addResourceParameters(Request request,
0540: RequestParameters parameters) {
0541: String pathInfo = request.getPath();
0542: if (pathInfo != null
0543: && pathInfo.startsWith(RESOURCES_PATH_PREFIX)) {
0544: int ix = RESOURCES_PATH_PREFIX.length();
0545: if (pathInfo.length() > ix) {
0546: StringBuffer path = new StringBuffer(pathInfo
0547: .substring(ix));
0548: int ixSemiColon = path.indexOf(";");
0549: // strip off any jsession id
0550: if (ixSemiColon != -1) {
0551: int ixEnd = path.indexOf("?");
0552: if (ixEnd == -1) {
0553: ixEnd = path.length();
0554: }
0555: path.delete(ixSemiColon, ixEnd);
0556: }
0557: parameters.setResourceKey(path.toString());
0558: }
0559: }
0560: }
0561:
0562: /**
0563: * In case you are using custom targets that are not part of the default
0564: * target hierarchy, you need to override this method, which will be called
0565: * after the defaults have been tried. When this doesn't provide a url
0566: * either (returns null), an exception will be thrown by the encode method
0567: * saying that encoding could not be done.
0568: *
0569: * @param requestCycle
0570: * the current request cycle (for efficient access)
0571: *
0572: * @param requestTarget
0573: * the request target
0574: * @return the url to the provided target, as a relative path from the
0575: * filter root.
0576: */
0577: protected String doEncode(RequestCycle requestCycle,
0578: IRequestTarget requestTarget) {
0579: return null;
0580: }
0581:
0582: /**
0583: * Encode a page class target.
0584: *
0585: * If you override this method to behave different then also
0586: * {@link #addBookmarkablePageParameters(Request, RequestParameters)} should
0587: * be overridden to by in sync with that behaviour.
0588: *
0589: * @param requestCycle
0590: * the current request cycle
0591: * @param requestTarget
0592: * the target to encode
0593: * @return the encoded url
0594: */
0595: protected CharSequence encode(RequestCycle requestCycle,
0596: IBookmarkablePageRequestTarget requestTarget) {
0597: // Begin encoding URL
0598: final AppendingStringBuffer url = new AppendingStringBuffer(64);
0599:
0600: // Get page Class
0601: final Class pageClass = requestTarget.getPageClass();
0602: final Application application = Application.get();
0603:
0604: // Find pagemap name
0605: String pageMapName = requestTarget.getPageMapName();
0606: if (pageMapName == null) {
0607: IRequestTarget currentTarget = requestCycle
0608: .getRequestTarget();
0609: if (currentTarget instanceof IPageRequestTarget) {
0610: Page currentPage = ((IPageRequestTarget) currentTarget)
0611: .getPage();
0612: final IPageMap pageMap = currentPage.getPageMap();
0613: if (pageMap.isDefault()) {
0614: pageMapName = "";
0615: } else {
0616: pageMapName = pageMap.getName();
0617: }
0618: } else {
0619: pageMapName = "";
0620: }
0621: }
0622:
0623: WebRequestEncoder encoder = new WebRequestEncoder(url);
0624: if (!application.getHomePage().equals(pageClass)
0625: || !"".equals(pageMapName)
0626: || (application.getHomePage().equals(pageClass) && requestTarget instanceof BookmarkableListenerInterfaceRequestTarget)) {
0627: /*
0628: * Add <page-map-name>:<bookmarkable-page-class>
0629: *
0630: * Encode the url so it is correct even for class names containing
0631: * non ASCII characters, like ä, æ, ø, å etc.
0632: *
0633: * The reason for this is that when redirecting to these
0634: * bookmarkable pages, we need to have the url encoded correctly
0635: * because we can't rely on the browser to interpret the unencoded
0636: * url correctly.
0637: */
0638: encoder
0639: .addValue(
0640: WebRequestCodingStrategy.BOOKMARKABLE_PAGE_PARAMETER_NAME,
0641: pageMapName + Component.PATH_SEPARATOR
0642: + pageClass.getName());
0643: }
0644:
0645: // Get page parameters
0646: final PageParameters parameters = requestTarget
0647: .getPageParameters();
0648: if (parameters != null) {
0649: final Iterator iterator;
0650: if (UnitTestSettings.getSortUrlParameters()) {
0651: iterator = new TreeSet(parameters.keySet()).iterator();
0652: } else {
0653: iterator = parameters.keySet().iterator();
0654: }
0655: while (iterator.hasNext()) {
0656: final String key = (String) iterator.next();
0657: final String values[] = parameters.getStringArray(key);
0658: if (values != null) {
0659: for (int i = 0; i < values.length; i++) {
0660: encoder.addValue(key, values[i]);
0661: }
0662: }
0663: }
0664: }
0665: return url;
0666: }
0667:
0668: /**
0669: * Encode a shared resource target.
0670: *
0671: * If you override this method to behave different then also
0672: * {@link #addResourceParameters(Request, RequestParameters)} should be
0673: * overridden to by in sync with that behaviour.
0674: *
0675: * @param requestCycle
0676: * the current request cycle
0677: * @param requestTarget
0678: * the target to encode
0679: * @return the encoded url
0680: */
0681: protected CharSequence encode(RequestCycle requestCycle,
0682: ISharedResourceRequestTarget requestTarget) {
0683: final String sharedResourceKey = requestTarget.getResourceKey();
0684: if ((sharedResourceKey == null)
0685: || (sharedResourceKey.trim().length() == 0)) {
0686: return "";
0687: } else {
0688: final AppendingStringBuffer buffer = new AppendingStringBuffer(
0689: sharedResourceKey.length());
0690: buffer.append("resources/");
0691: buffer.append(sharedResourceKey);
0692: Map map = requestTarget.getRequestParameters()
0693: .getParameters();
0694: if (map != null && map.size() > 0) {
0695: buffer.append('?');
0696: Iterator it = map.entrySet().iterator();
0697: while (it.hasNext()) {
0698: Map.Entry entry = (Entry) it.next();
0699: buffer.append(entry.getKey());
0700: buffer.append('=');
0701: buffer.append(entry.getValue());
0702: if (it.hasNext()) {
0703: buffer.append("&");
0704: }
0705: }
0706: }
0707: return buffer;
0708: }
0709: }
0710:
0711: /**
0712: * Encode a listener interface target.
0713: *
0714: * If you override this method to behave different then also
0715: * {@link #addInterfaceParameters(Request, RequestParameters)} should be
0716: * overridden to by in sync with that behaviour.
0717: *
0718: * @param requestCycle
0719: * the current request cycle
0720: * @param requestTarget
0721: * the target to encode
0722: * @return the encoded url
0723: */
0724: protected CharSequence encode(RequestCycle requestCycle,
0725: IListenerInterfaceRequestTarget requestTarget) {
0726: final RequestListenerInterface rli = requestTarget
0727: .getRequestListenerInterface();
0728:
0729: // Start string buffer for url
0730: final AppendingStringBuffer url = new AppendingStringBuffer(64);
0731: url.append('?');
0732: url.append(INTERFACE_PARAMETER_NAME);
0733: url.append('=');
0734:
0735: // Get component and page for request target
0736: final Component component = requestTarget.getTarget();
0737: final Page page = component.getPage();
0738:
0739: // Add pagemap
0740: final IPageMap pageMap = page.getPageMap();
0741: if (!pageMap.isDefault()) {
0742: url.append(pageMap.getName());
0743: }
0744: url.append(Component.PATH_SEPARATOR);
0745:
0746: // Add path to component
0747: url.append(component.getPath());
0748: url.append(Component.PATH_SEPARATOR);
0749:
0750: // Add version
0751: final int versionNumber = component.getPage()
0752: .getCurrentVersionNumber();
0753: if (!rli.getRecordsPageVersion()) {
0754: url.append(Page.LATEST_VERSION);
0755: } else if (versionNumber > 0) {
0756: url.append(versionNumber);
0757: }
0758: url.append(Component.PATH_SEPARATOR);
0759:
0760: // Add listener interface
0761: final String listenerName = rli.getName();
0762: if (!IRedirectListener.INTERFACE.getName().equals(listenerName)) {
0763: url.append(listenerName);
0764: }
0765: url.append(Component.PATH_SEPARATOR);
0766:
0767: // Add behaviourId
0768: RequestParameters params = requestTarget.getRequestParameters();
0769: if (params != null && params.getBehaviorId() != null) {
0770: url.append(params.getBehaviorId());
0771: }
0772: url.append(Component.PATH_SEPARATOR);
0773:
0774: // Add URL depth
0775: if (params != null && params.getUrlDepth() != 0) {
0776: url.append(params.getUrlDepth());
0777: }
0778:
0779: return url;
0780: }
0781:
0782: /**
0783: * Encode a page target.
0784: *
0785: * @param requestCycle
0786: * the current request cycle
0787: * @param requestTarget
0788: * the target to encode
0789: * @return the encoded url
0790: */
0791: protected CharSequence encode(RequestCycle requestCycle,
0792: IPageRequestTarget requestTarget) {
0793: // Get the page we want a url from:
0794: Page page = requestTarget.getPage();
0795:
0796: // A url to a page is the IRedirectListener interface:
0797: CharSequence urlRedirect = page
0798: .urlFor(IRedirectListener.INTERFACE);
0799:
0800: // Touch the page once because it could be that it did go from stateless
0801: // to statefull or it was a internally made page where just a url must
0802: // be made for (frames)
0803: Session.get().touch(page);
0804: return urlRedirect;
0805: }
0806:
0807: /**
0808: * Gets the mount encoder for the given request target if any.
0809: *
0810: * @param requestTarget
0811: * the request target to match
0812: * @return the mount encoder if any
0813: */
0814: protected IRequestTargetUrlCodingStrategy getMountEncoder(
0815: IRequestTarget requestTarget) {
0816: // TODO Post 1.2: Performance: Optimize algorithm if possible and/ or
0817: // cache lookup results
0818: for (Iterator i = mountsOnPath.strategies().iterator(); i
0819: .hasNext();) {
0820: IRequestTargetUrlCodingStrategy encoder = (IRequestTargetUrlCodingStrategy) i
0821: .next();
0822: if (encoder.matches(requestTarget)) {
0823: return encoder;
0824: }
0825: }
0826:
0827: return null;
0828: }
0829:
0830: /**
0831: * Gets the request info path. This is an overridable method in order to
0832: * provide users with a means to implement e.g. a path encryption scheme.
0833: * This method by default returns {@link Request#getPath()}.
0834: *
0835: * @param request
0836: * the request
0837: * @return the path info object, possibly processed
0838: */
0839: protected String getRequestPath(Request request) {
0840: return request.getPath();
0841: }
0842:
0843: /**
0844: * Map used to store mount paths and their corresponding url coding
0845: * strategies.
0846: *
0847: * @author ivaynberg
0848: */
0849: private static class MountsMap {
0850: private static final long serialVersionUID = 1L;
0851:
0852: /** case sensitive flag */
0853: private final boolean caseSensitiveMounts;
0854:
0855: /** backing map */
0856: private final TreeMap map;
0857:
0858: /**
0859: * Constructor
0860: *
0861: * @param caseSensitiveMounts
0862: * whether or not keys of this map are case-sensitive
0863: */
0864: public MountsMap(boolean caseSensitiveMounts) {
0865: map = new TreeMap(LENGTH_COMPARATOR);
0866: this .caseSensitiveMounts = caseSensitiveMounts;
0867: }
0868:
0869: /**
0870: * Checks if the specified path matches any mount, and if so returns the
0871: * coding strategy for that mount. Returns null if the path doesnt match
0872: * any mounts.
0873: *
0874: * NOTE: path here is not the mount - it is the full url path
0875: *
0876: * @param path
0877: * non-null url path
0878: * @return coding strategy or null
0879: */
0880: public IRequestTargetUrlCodingStrategy strategyForPath(
0881: String path) {
0882: if (path == null) {
0883: throw new IllegalArgumentException(
0884: "Argument [[path]] cannot be null");
0885: }
0886: if (caseSensitiveMounts == false) {
0887: path = path.toLowerCase();
0888: }
0889: for (final Iterator it = map.entrySet().iterator(); it
0890: .hasNext();) {
0891: final Map.Entry entry = (Entry) it.next();
0892: final String key = (String) entry.getKey();
0893: if (path.startsWith(key)) {
0894: IRequestTargetUrlCodingStrategy strategy = (IRequestTargetUrlCodingStrategy) entry
0895: .getValue();
0896: if (strategy.matches(path)) {
0897: return strategy;
0898: }
0899: }
0900: }
0901: return null;
0902: }
0903:
0904: /**
0905: * @return number of mounts in the map
0906: */
0907: public int size() {
0908: return map.size();
0909: }
0910:
0911: /**
0912: * @return collection of coding strategies associated with every mount
0913: */
0914: public Collection strategies() {
0915: return map.values();
0916: }
0917:
0918: /**
0919: * Removes mount from the map
0920: *
0921: * @param mount
0922: */
0923: public void unmount(String mount) {
0924: if (caseSensitiveMounts == false && mount != null) {
0925: mount = mount.toLowerCase();
0926: }
0927:
0928: map.remove(mount);
0929: }
0930:
0931: /**
0932: * Gets the coding strategy for the specified mount path
0933: *
0934: * @param mount
0935: * mount paht
0936: * @return associated coding strategy or null if none
0937: */
0938: public IRequestTargetUrlCodingStrategy strategyForMount(
0939: String mount) {
0940: if (caseSensitiveMounts == false && mount != null) {
0941: mount = mount.toLowerCase();
0942: }
0943:
0944: return (IRequestTargetUrlCodingStrategy) map.get(mount);
0945: }
0946:
0947: /**
0948: * Associates a mount with a coding strategy
0949: *
0950: * @param mount
0951: * @param encoder
0952: * @return previous coding strategy associated with the mount, or null
0953: * if none
0954: */
0955: public IRequestTargetUrlCodingStrategy mount(String mount,
0956: IRequestTargetUrlCodingStrategy encoder) {
0957: if (caseSensitiveMounts == false && mount != null) {
0958: mount = mount.toLowerCase();
0959: }
0960: return (IRequestTargetUrlCodingStrategy) map.put(mount,
0961: encoder);
0962: }
0963:
0964: /** Comparator implementation that sorts longest strings first */
0965: private static final Comparator LENGTH_COMPARATOR = new Comparator() {
0966: public int compare(Object o1, Object o2) {
0967: // longer first
0968: if (o1 == o2) {
0969: return 0;
0970: } else if (o1 == null) {
0971: return 1;
0972: } else if (o2 == null) {
0973: return -1;
0974: } else {
0975: final String lhs = (String) o1;
0976: final String rhs = (String) o2;
0977: return rhs.compareTo(lhs);
0978: }
0979: }
0980: };
0981: }
0982:
0983: /**
0984: * Makes page map name url safe.
0985: *
0986: * Since the default page map name in wicket is null and null does not
0987: * encode well into urls this method will substitute null for a known token.
0988: * If the <code>pageMapName</code> passed in is not null it is returned
0989: * without modification.
0990: *
0991: * @param pageMapName
0992: * page map name
0993: * @return encoded pagemap name
0994: */
0995: public static final String encodePageMapName(String pageMapName) {
0996: if (Strings.isEmpty(pageMapName)) {
0997: return DEFAULT_PAGEMAP_NAME;
0998: } else {
0999: return pageMapName;
1000: }
1001: }
1002:
1003: /**
1004: * Undoes the effect of {@link #encodePageMapName(String)}
1005: *
1006: * @param pageMapName
1007: * page map name
1008: * @return decoded page map name
1009: */
1010: public static String decodePageMapName(String pageMapName) {
1011: if (DEFAULT_PAGEMAP_NAME.equals(pageMapName)) {
1012: return null;
1013: } else {
1014: return pageMapName;
1015: }
1016: }
1017: }
|